转发到就地构造函数

时间:2014-01-31 23:54:42

标签: c++ c++11 perfect-forwarding

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

我希望能够使用生成的对象的直接内联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)

2 个答案:

答案 0 :(得分:3)

您将无法在放置新位置中删除复制/移动:复制省略完全基于编译器在构造时知道对象最终将最终结束的想法。此外,由于复制省略实际上改变了程序的行为(毕竟,它不会调用相应的构造函数和析构函数,即使它们有副作用)复制省略仅限于几个非常特殊的情况(列于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)...);
}

答案 1 :(得分:-1)

前言

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

C ++可以通过使用展示位置新运算符为您提供的唯一方式。否则,对象将根据其存储类(在堆栈上或通过您定义为新运算符的任何内容)构建。

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

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

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

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

最终会得到一个顶层消息构造函数,它接受所有可能的初始参数并将它们分派给各个子对象构造函数。

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

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

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

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

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

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

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

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

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

#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字节的静态分配,如果消息太大,将无用。

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

关于转发和施工

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

一旦您调用了对象创建,就会分配内存并且&amp;&amp;&amp;只会允许你使用该地址将所述内存的所有权传递给另一个对象,而无需使用无用的副本。

您可以使用模板来识别对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字节的屏障。系统性能突然崩溃,你需要先生模板&amp;实时专家解释这个可怜的家伙,他做了什么杀死了很多小猫 更糟糕的是,一开始系统性能下降可能是渐进的或不明显的,所以有一天你可能会忘记数千行代码,这些代码在没有任何人注意的情况下进行动态分配,并且纠正问题的全局改革可能是巨大的。 / p>

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