Предисловие
Концептуальный барьер, который не может преодолеть даже 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 байта. Внезапно производительность системы рухнет, и вам понадобится мистер шаблон и эксперт в реальном времени, чтобы объяснить бедняге, что то, что он сделал, убило много котят.
Что еще хуже, деградация системы может быть прогрессирующей или незаметной в во-первых, так что в один прекрасный день вы можете проснуться с тысячами строк кода, которые выполняли динамическое распределение в течение многих лет, и никто этого не замечал, и глобальная переработка для исправления проблемы может быть огромной.
Если, с другой стороны, все люди в вашей компании жуют шаблоны и мьютексы на завтрак, синтаксический сахар вообще не требуется.
person
kuroi neko
schedule
01.02.2014
Message w(id, in_place<MyStruct>(a,b,c));
делать? Да иstd::forward<T>(src)
вnew
не помешало бы. - person Yakk - Adam Nevraumont   schedule 01.02.2014template<typename T, typename... Args> Message(id_t messageId, Args&&... args)
? По-другому не компилируется. - person   schedule 01.02.2014Message(id, *this);
, потому что он не разрешает [неконстантный] ссылочный тип. - person kfsone   schedule 01.02.2014T
? - person Yakk - Adam Nevraumont   schedule 01.02.2014template
ed? - person Yakk - Adam Nevraumont   schedule 01.02.2014uint32_t maxID; /*...*/ Message outgoing(MessageID::X, maxID);
,Message outgoing(MessageID::Y, *this);
иMessage outgoing(MessageID::Z, SomeStruct(args);
. Проблема заключалась в том, что я указывалT&& src
в аргументахMessage::Message
. - person kfsone   schedule 01.02.2014