4

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

我希望可以使用生成的对象的即时、内联 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
};

在 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)

4

2 回答 2

3

您将无法在放置 new 中省略复制/移动:复制省略完全基于编译器在构建时知道对象最终会在哪里结束的想法。此外,由于复制省略实际上改变了程序的行为(毕竟,即使它们有副作用,它也不会调用相应的构造函数和析构函数)复制省略仅限于一些非常具体的情况(列在 12.8 [ class.copy] 第 31 段:本质上是在按名称返回局部变量时,按名称抛出局部变量时,按值捕获正确类型的异常时,以及复制/移动临时变量时;有关详细信息,请参阅该子句)。自[安置]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_tto 映射到相关类型,假设存在从消息 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)...);
}
于 2014-02-01T00:44:38.693 回答
0

前言

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

C++ 可以为您提供的唯一方法是使用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 字节的静态分配,如果消息太大,这将无用。

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

关于转发和就地施工

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

一旦你调用了一个对象创建,内存就被分配了, && 东西只会允许你使用该地址将所述内存的所有权传递给另一个对象,而无需求助于无用的副本。

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

我看不到一种在类之上创建模板的方法,该模板Message会延迟对象构造,直到您决定要在哪个位置构造它。

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

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

为此,您需要通过放置 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 字节的障碍。突然系统性能会崩溃,你需要模板先生和实时专家来解释这个可怜的家伙,他的所作所为杀死了很多小猫。
更糟糕的是,系统退化一开始可能是渐进的或不明显的,所以有一天你可能会发现数千行代码在没有任何人注意到的情况下进行了多年的动态分配,而纠正问题的全球大修可能是巨大的。

另一方面,如果您公司中的所有人都在吃早饭模板和互斥锁,那么一开始甚至不需要语法糖。

于 2014-02-01T04:46:44.077 回答