繁体   English   中英

转发到就地构造函数

[英]Forwarding to in-place constructor

我有一个消息类,以前有点痛苦,你必须构造消息类,告诉它为你的对象分配空间,然后通过构造或成员填充空间。

我想使用结果对象的直接内联new构造消息对象成为可能,但是在调用站点使用简单语法同时确保复制省略。

#include <cstdint>

typedef uint8_t id_t;
enum class MessageID { WorldPeace };

class Message
{
    uint8_t* m_data;         // current memory
    uint8_t m_localData[64]; // upto 64 bytes.
    id_t m_messageId;
    size_t m_size; // amount of data used
    size_t m_capacity; // amount of space available
    // ...

public:
    Message(size_t requestSize, id_t messageId)
        : m_data(m_localData)
        , m_messageId(messageId)
        , m_size(0), m_capacity(sizeof(m_localData))
    {
        grow(requestSize);
    }

    void grow(size_t newSize)
    {
        if (newSize > m_capacity)
        {
            m_data = realloc((m_data == m_localData) ? nullptr : m_data, newSize);
            assert(m_data != nullptr); // my system uses less brutal mem mgmt
            m_size = newSize;
        }
    }

    template<typename T>
    T* allocatePtr()
    {
        size_t offset = size;
        grow(offset + sizeof(T));
        return (T*)(m_data + offset);
    }

#ifdef USE_CPP11
    template<typename T, typename Args...>
    Message(id_t messageId, Args&&... args)
        : Message(sizeof(T), messageID)
    {
        // we know m_data points to a large enough buffer
        new ((T*)m_data) T (std::forward<Args>(args)...);
    }
#endif
};

Pre-C ++ 11我有一个讨厌的宏,CONSTRUCT_IN_PLACE,它做了:

#define CONSTRUCT_IN_PLACE(Message, Typename, ...) \
    new ((Message).allocatePtr<Typename>()) Typename (__VA_ARGS__)

你会说:

Message outgoing(sizeof(MyStruct), MessageID::WorldPeace);
CONSTRUCT_IN_PLACE(outgoing, MyStruct, wpArg1, wpArg2, wpArg3);

使用C ++ 11,您可以使用

Message outgoing<MyStruct>(MessageID::WorldPeace, wpArg1, wpArg2, wpArg3);

但我觉得这很混乱。 我想要实现的是:

    template<typename T>
    Message(id_t messageId, T&& src)
        : Message(sizeof(T), messageID)
    {
        // we know m_data points to a large enough buffer
        new ((T*)m_data) T (src);
    }

这样用户就可以使用了

Message outgoing(MessageID::WorldPeace, MyStruct(wpArg1, wpArg2, wpArg3));

但似乎这首先在堆栈上构造一个临时的MyStruct ,将就地new转换为对T的移动构造函数的调用。

其中许多消息很简单,通常是POD,它们经常处于这样的编组功能:

void dispatchWorldPeace(int wpArg1, int wpArg2, int wpArg3)
{
    Message outgoing(MessageID::WorldPeace, MyStruct(wpArg1, wpArg2, wpArg3));
    outgoing.send(g_listener);
}

所以我想避免创建一个需要后续移动/复制的中间临时。

似乎编译器应该能够消除临时和移动并将构造一直向前转移到就地new

我在做什么导致它不? (GCC 4.8.1,Clang 3.5,MSVC 2013)

您将无法在放置新位置中删除复制/移动:复制省略完全基于编译器在构造时知道对象最终将最终结束的想法。 此外,由于复制省略实际上改变了程序的行为(毕竟,它不会调用相应的构造函数和析构函数,即使它们有副作用)复制省略仅限于几个非常特殊的情况(列于12.8 [ class.copy]第31段:主要是当按名称返回局部变量时,按名称抛出局部变量,按值捕获正确类型的异常,以及复制/移动临时变量时;请参阅子句以获取确切的详细信息)。 由于[placement] new不是可以省略副本的上下文,并且构造函数的参数显然不是临时的(它被命名),因此永远不会省略复制/移动。 即使将缺少的std::forward<T>(...)到构造函数中也会导致复制/移动被省略:

template<typename T>
Message(id_t messageId, T&& src)
    : Message(sizeof(T), messageID)
{
    // placement new take a void* anyway, i.e., no need to cast
    new (m_data) T (std::forward<T>(src));
}

我不认为您在调用构造函数时可以显式指定模板参数。 因此,我认为如果不提前构建对象并将其复制/移动,您可能得到的最接近的是这样的:

template <typename>
struct Tag {};

template <typename T, typename A>
Message::Message(Tag<T>, id_t messageId, A... args)
    : Message(messageId, sizeof(T)) {
    new(this->m_data) T(std::forward<A>(args)...);
}

一种可能使事情变得更好的方法是使用id_t映射到相关类型,假设存在从消息ID到相关类型的映射:

typedef uint8_t id_t;
template <typename T, id_t id> struct Tag {};
struct MessageId {
    static constexpr Tag<MyStruct, 1> WorldPeace;
    // ...
};
template <typename T, id_t id, typename... A>
Message::Message(Tag<T, id>, A&&... args)
    Message(id, sizeof(T)) {
    new(this->m_data) T(std::forward<A>)(args)...);
}

前言

甚至C ++ 2049无法跨越的概念障碍是您需要组成消息的所有位在一个连续的内存块中对齐。

C ++可以通过使用placement new运算符给你的唯一方法。 否则,对象将根据其存储类(在堆栈上或通过您定义为新运算符的任何内容)构建。

这意味着您传递给有效负载构造函数的任何对象将首先构建(在堆栈上),然后由构造函数使用(最有可能复制构造它)。

完全避免这个副本是不可能的。 您可能有一个正向构造函数执行最小量的复制,但仍然可能会复制传递给初始化程序的标量参数,初始化程序的构造函数认为记忆和/或生成所需的任何数据也是如此。

如果您希望能够将参数自由地传递给构建完整消息所需的每个构造函数,而不将它们首先存储在参数对象中,则需要

  • 对构成消息的每个子对象使用placement new运算符,
  • 记忆传递给各个子构造函数的每个单标量参数,
  • 每个对象的特定代码,用适当的地址提供放置new运算符并调用子对象的构造函数。

您将得到一个顶级消息构造函数,它获取所有可能的初始参数并将它们分派给各个子对象构造函数。

我甚至不知道这是否可行,但结果将非常脆弱,无论如何都容易出错。

这是你想要的,只是为了一点语法糖的好处?

如果您要提供API,则无法涵盖所有​​情况。 恕我直言,最好的做法是做出一些降级很好的东西。

简单的解决方案是将有效负载构造函数参数限制为标量值,或者为可以控制的有限消息有效负载集实现“就地子构造”。 在您的级别,您不能做更多的事情,以确保消息构建没有额外的副本。

现在,应用程序软件可以自由定义将对象作为参数的构造函数,然后支付的价格将是这些额外的副本。

此外,这可能是最有效的方法,如果参数构造成本高(即构造时间大于复制时间,因此创建静态对象并在每个消息之间稍微修改它)或者如果它因任何原因而具有比你的功能更长的寿命。

一个有效,丑陋的解决方案

首先,让我们从一个老式的,无模板的解决方案开始,进行就地构建。

这个想法是让消息根据对象的大小预先分配正确类型的内存(动态的本地缓冲区)。
然后将正确的基址传递给新的位置以构建消息内容。

#include <cstdint>
#include <cstdio>
#include <new>

typedef uint8_t id_t;
enum class MessageID { WorldPeace, Armaggedon };

#define SMALL_BUF_SIZE 64

class Message {
    id_t     m_messageId;
    uint8_t* m_data;
    uint8_t  m_localData[SMALL_BUF_SIZE];

public:

    // choose the proper location for contents
    Message (MessageID messageId, size_t size)
    {
        m_messageId = (id_t)messageId;
        m_data = size <= SMALL_BUF_SIZE ? m_localData : new uint8_t[size];
    }

    // dispose of the contents if need be
    ~Message ()
    {
        if (m_data != m_localData) delete m_data;
    }

    // let placement new know about the contents location
    void * location (void)
    {
        return m_data;
    }
};

// a macro to do the in-place construction
#define BuildMessage(msg, id, obj, ...   )       \
        Message msg(MessageID::id, sizeof(obj)); \
        new (msg.location()) obj (__VA_ARGS__);  \

// example uses
struct small {
    int a, b, c;
    small (int a, int b, int c) :a(a),b(b),c(c) {}
};
struct big {
    int lump[1000];
};

int main(void)
{
    BuildMessage(msg1, WorldPeace, small, 1, 2, 3)
    BuildMessage(msg2, Armaggedon, big)
}

这只是初始代码的精简版本,根本没有模板。

我发现它相对干净,易于使用,但对每个人来说都是如此。

我在这里看到的唯一低效率是静态分配64字节,如果消息太大则无用。

当然,一旦构造了消息,所有类型信息都会丢失,因此之后访问它们的内容会很尴尬。

关于转发和建设到位

基本上,新的&&限定符没有任何魔力。 要进行就地构造,编译器需要在调用构造函数之前知道将用于对象存储的地址。

一旦你调用了一个对象创建,就已经分配了内存,而且&& thing只允许你使用那个地址将所述内存的所有权传递给另一个对象,而不需要使用无用的副本。

您可以使用模板来识别对Message构造函数的调用,该构造函数涉及作为消息内容传递的给定类,但是为时已晚:在构造函数可以对其内存位置执行任何操作之前,将构造该对象。

我无法看到在Message类之上创建模板的方法,该方法将推迟对象构造,直到您决定在哪个位置构建它。

但是,您可以处理定义对象内容的类,以使某些就地构造自动化。

这不会解决将对象传递给将在其中构建的对象的构造函数的一般问题。

要做到这一点,你需要通过一个placement new来构建子对象本身,这意味着为每个初始化器实现一个特定的模板接口,并让每个对象为每个子对象提供构造的地址。 。

现在是语法糖。

为了让丑陋的模板值得一试,你可以专门化你的消息类来以不同的方式处理大小信息。

这个想法是让一块内存传递给你的发送功能。 因此,在小消息的情况下,消息头和内容被定义为本地消息属性,而对于大消息,则分配额外的内存以包括消息头。

因此,用于通过系统推动消息的神奇DMA将有一个干净的数据块,可以使用任何一种方式。

每个大消息仍然会发生一次动态分配,而小型消息则不会发生。

#include <cstdint>
#include <new>

// ==========================================================================
// Common definitions
// ==========================================================================

// message header
enum class MessageID : uint8_t { WorldPeace, Armaggedon };
struct MessageHeader {
    MessageID id;
    uint8_t   __padding; // one free byte here
    uint16_t  size;
};

// small buffer size
#define SMALL_BUF_SIZE 64

// dummy send function
int some_DMA_trick(int destination, void * data, uint16_t size);

// ==========================================================================
// Macro solution
// ==========================================================================

// -----------------------------------------
// Message class
// -----------------------------------------
class mMessage {
    // local storage defined even for big messages
    MessageHeader   m_header;
    uint8_t         m_localData[SMALL_BUF_SIZE];

    // pointer to the actual message
    MessageHeader * m_head;
public:  
    // choose the proper location for contents
    mMessage (MessageID messageId, uint16_t size)
    {
        m_head = size <= SMALL_BUF_SIZE 
            ? &m_header
            : (MessageHeader *) new uint8_t[size + sizeof (m_header)];
        m_head->id   = messageId;
        m_head->size = size;
   }

    // dispose of the contents if need be
    ~mMessage ()
    {
        if (m_head != &m_header) delete m_head;
    }

    // let placement new know about the contents location
    void * location (void)
    {
        return m_head+1;
    }

    // send a message
    int send(int destination)
    {
        return some_DMA_trick (destination, m_head, (uint16_t)(m_head->size + sizeof (m_head)));
    }
};

// -----------------------------------------
// macro to do the in-place construction
// -----------------------------------------
#define BuildMessage(msg, obj, id, ...   )       \
        mMessage msg (MessageID::id, sizeof(obj)); \
        new (msg.location()) obj (__VA_ARGS__);  \

// ==========================================================================
// Template solution
// ==========================================================================
#include <utility>

// -----------------------------------------
// template to check storage capacity
// -----------------------------------------
template<typename T>
struct storage
{
    enum { local = sizeof(T)<=SMALL_BUF_SIZE };
};

// -----------------------------------------
// base message class
// -----------------------------------------
class tMessage {
protected:
    MessageHeader * m_head;
    tMessage(MessageHeader * head, MessageID id, uint16_t size) 
        : m_head(head)
    {
        m_head->id = id;
        m_head->size = size;
    }
public:
    int send(int destination)
    {
        return some_DMA_trick (destination, m_head, (uint16_t)(m_head->size + sizeof (*m_head)));
    }
};

// -----------------------------------------
// general message template
// -----------------------------------------
template<bool local_storage, typename message_contents>
class aMessage {};

// -----------------------------------------
// specialization for big messages
// -----------------------------------------
template<typename T>
class aMessage<false, T> : public tMessage
{
public:
    // in-place constructor
    template<class... Args>
    aMessage(MessageID id, Args...args) 
        : tMessage(
            (MessageHeader *)new uint8_t[sizeof(T)+sizeof(*m_head)], // dynamic allocation
            id, sizeof(T))
    {
        new (m_head+1) T(std::forward<Args>(args)...);
    }

    // destructor
    ~aMessage ()
    {
        delete m_head;
    }

    // syntactic sugar to access contents
    T& contents(void) { return *(T*)(m_head+1); }
};

// -----------------------------------------
// specialization for small messages
// -----------------------------------------
template<typename T>
class aMessage<true, T> : public tMessage
{
    // message body defined locally
    MessageHeader m_header;
    uint8_t       m_data[sizeof(T)]; // no need for 64 bytes here

public:
    // in-place constructor
    template<class... Args>
    aMessage(MessageID id, Args...args) 
        : tMessage(
            &m_header, // local storage
            id, sizeof(T))
    {
        new (m_head+1) T(std::forward<Args>(args)...);
    }

    // syntactic sugar to access contents
    T& contents(void) { return *(T*)(m_head+1); }
};


// -----------------------------------------
// helper macro to hide template ugliness
// -----------------------------------------
#define Message(T) aMessage<storage<T>::local, T>
// something like typedef aMessage<storage<T>::local, T> Message<T>

// ==========================================================================
// Example
// ==========================================================================
#include <cstdio>
#include <cstring>

// message sending
int some_DMA_trick(int destination, void * data, uint16_t size)
{
    printf("sending %d bytes @%p to %08X\n", size, data, destination);
    return 1;
}

// some dynamic contents
struct gizmo {
    char * s;
    gizmo(void) { s = nullptr; };
    gizmo (const gizmo&  g) = delete;

    gizmo (const char * msg)
    {
        s = new char[strlen(msg) + 3];
        strcpy(s, msg);
        strcat(s, "#");
    }

    gizmo (gizmo&& g)
    {
        s = g.s;
        g.s = nullptr;
        strcat(s, "*");
    }

    ~gizmo() 
    { 
        delete s;
    }

    gizmo& operator=(gizmo g)
    {
        std::swap(s, g.s);
        return *this;
    }
    bool operator!=(gizmo& g)
    {
        return strcmp (s, g.s) != 0;
    }

};

// some small contents
struct small {
    int a, b, c;
    gizmo g;
    small (gizmo g, int a, int b, int c)
        : a(a), b(b), c(c), g(std::move(g)) 
    {
    }

    void trace(void) 
    { 
        printf("small: %d %d %d %s\n", a, b, c, g.s);
    }
};

// some big contents
struct big {
    gizmo lump[1000];

    big(const char * msg = "?")
    { 
        for (size_t i = 0; i != sizeof(lump) / sizeof(lump[0]); i++)
            lump[i] = gizmo (msg);
    }

    void trace(void)
    {
        printf("big: set to ");
        gizmo& first = lump[0];
        for (size_t i = 1; i != sizeof(lump) / sizeof(lump[0]); i++)
            if (lump[i] != first) { printf(" Erm... mostly "); break; }
        printf("%s\n", first.s);
    }
};

int main(void)
{
    // macros
    BuildMessage(mmsg1, small, WorldPeace, gizmo("Hi"), 1, 2, 3);
    BuildMessage(mmsg2, big  , Armaggedon, "Doom");
    ((small *)mmsg1.location())->trace();
    ((big   *)mmsg2.location())->trace();
    mmsg1.send(0x1000);
    mmsg2.send(0x2000);

    // templates
    Message (small) tmsg1(MessageID::WorldPeace, gizmo("Hello"), 4, 5, 6);
    Message (big  ) tmsg2(MessageID::Armaggedon, "Damnation");
    tmsg1.contents().trace();
    tmsg2.contents().trace();
    tmsg1.send(0x3000);
    tmsg2.send(0x4000);
}

输出:

small: 1 2 3 Hi#*
big: set to Doom#
sending 20 bytes @0xbf81be20 to 00001000
sending 4004 bytes @0x9e58018 to 00002000
small: 4 5 6 Hello#**
big: set to Damnation#
sending 20 bytes @0xbf81be0c to 00003000
sending 4004 bytes @0x9e5ce50 to 00004000

参数转发

我认为在这里进行构造函数参数转发没什么意义。

消息内容引用的任何动态数据都必须是静态的或复制到消息体中,否则一旦消息创建者超出范围,引用的数据就会消失。

如果这个非常高效的库的用户开始在消息中传递魔术指针和其他全局数据,我想知道全局系统性能将如何。 但毕竟这不关我的事。

我使用宏来隐藏类型定义中的模板丑陋。

如果有人有想法摆脱它,我很感兴趣。

效率

模板变体需要额外转发内容参数才能到达构造函数。 我看不出如何避免这种情况。

宏版本为大消息浪费了68字节的内存,而对于小消息浪费了一些内存( 64 - sizeof (contents object) )。

性能方面,这些额外的内存是模板提供的唯一增益。 由于所有这些对象都被认为是在堆栈上构建并且存活了几微秒,因此它非常难以置信。

与初始版本相比,这个版本应该更有效地处理大消息的消息发送。 在这里,如果这些消息很少并且仅为方便起见而提供,则差异并不十分有用。

模板版本维护一个指向消息有效负载的指针,如果您实现了send功能的专用版本,则可以为小消息保留备用指针。
勉强值得代码重复,恕我直言。

最后一句话

我想我非常了解操作系统的工作原理以及可能存在的性能问题。 我写了很多实时应用程序,还有一些驱动程序和几个BSP。

我也不止一次地看到一个非常有效的系统层,它被一个过于宽松的接口破坏了,它允许应用软件程序员在不知情的情况下做最愚蠢的事情。
这就是我初步反应的原因。

如果我在全球系统设计中有发言权,我会禁止所有这些魔术指针和其他引人注目的对象引用混合,以限制非专业用户对系统层的无用使用,而不是让它们无意中传播蟑螂通过该系统。

除非这个界面的用户是模板和实时的问题,否则他们不会理解语法糖壳下面发生的事情,并且可能很快就会在脚下拍摄自己(以及他们的同事和应用软件) 。

假设一个糟糕的应用程序软件程序员在其中一个结构中添加了一个微弱的字段,并在不知不觉中跨越了64字节的障碍。 系统性能突然崩溃了,你需要先生模板和实时专家来解释这个可怜的家伙他杀了很多小猫。
更糟糕的是,一开始系统性能下降可能是渐进的或不明显的,所以有一天你可能会忘记数千行代码,这些代码在没有任何人注意的情况下进行动态分配,并且纠正问题的全局改革可能是巨大的。

另一方面,如果贵公司的所有人都在模板和互斥体上吃早餐,那么首先甚至不需要语法糖。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM