C++ пример кодирования ужаса или блестящей идеи?
на предыдущем работодателе мы писали двоичные сообщения, которые должны были идти "по проводам" на другие компьютеры. Каждое сообщение имело стандартный заголовок, что-то вроде:
class Header
{
int type;
int payloadLength;
};
все данные были смежными (заголовок, сразу за которым следуют данные). Мы хотели добраться до полезной нагрузки, учитывая, что у нас был указатель на заголовок. Традиционно вы можете сказать что-то вроде:
char* Header::GetPayload()
{
return ((char*) &payloadLength) + sizeof(payloadLength);
}
или еще:
char* Header::GetPayload()
{
return ((char*) this) + sizeof(Header);
}
это казалось многословным, поэтому я подошел с:
char* Header::GetPayload()
{
return (char*) &this[1];
}
сначала это кажется довольно тревожным, возможно, слишком странным для использования , но очень компактным. Было много споров о том, было ли это блестящим или отвратительным.
так что это - преступление против кодирования, или хорошее решение? У вас когда-нибудь был подобный компромисс?
-обновление:
мы попробовали массив нулевого размера, но в то время компиляторы давали предупреждения. В конце концов, мы пошли на прием inhertited: сообщение исходит от Заголовок. Он отлично работает на практике, но в priciple вы говорите заголовок ISA сообщения - что кажется немного неудобным.
17 ответов
Я бы пошел на преступление против кодирования.
оба метода будут генерировать один и тот же объектный код. В первом случае намерение становится ясным. Второй очень запутанный, с единственным преимуществом, что он экономит пару нажатий клавиш. (Просто учись долбанному шрифту).
кроме того, обратите внимание, что ни один из методов не гарантируется. Объект sizeof () включает заполнение для выравнивания слов, так что если заголовок был:
class Header
{
int type;
int payloadLength;
char status;
};
оба метода вы описали бы полезная нагрузка, начинающаяся с заголовка+12, когда, скорее всего, она фактически начинается с заголовка+9.
вы зависите от компилятора для компоновки ваших классов определенным образом. Я бы определил сообщение как структуру (с определением макета) и имел класс, который инкапсулирует сообщение и предоставляет ему интерфейс. Очистить код = хороший код. "Милый" код = плохой (трудно поддерживать) код.
struct Header
{
int type;
int payloadlength;
}
struct MessageBuffer
{
struct Header header;
char[MAXSIZE] payload;
}
class Message
{
private:
MessageBuffer m;
public:
Message( MessageBuffer buf ) { m = buf; }
struct Header GetHeader( )
{
return m.header;
}
char* GetPayLoad( )
{
return &m.payload;
}
}
прошло некоторое время с тех пор, как я написал любой C++, поэтому, пожалуйста, извините за любые проблемы с синтаксисом. Просто пытаюсь передать общую идею.
лично я думаю, что если есть преступление, оно запрашивает заголовок для полезной нагрузки.
но пока вы собираетесь делать это таким образом, "это+1" так же хорошо, как и любой другой.
обоснование: '& this[1] ' -это универсальный фрагмент кода, который не требует, чтобы вы копались в определениях классов, чтобы полностью понять, и не требует фиксации, когда кто-то меняет имя или содержимое класса.
кстати, первый пример-истинное преступление против человечества. Добавить элемент в конец класса и он потерпит неудачу. Переместите членов по классу, и он потерпит неудачу. Если компилятор заполнит класс, он потерпит неудачу.
кроме того, если вы собираетесь предположить, что макет компилятора классов/структур соответствует вашему макету пакета, то вы должны понять, как работает данный компилятор. Например. на MSVC вы, вероятно, захотите узнать о #pragma pack
.
PS: немного страшно, сколько людей считают " это+1 "или" &this[1]" трудно читать или понимать.
это обычная проблема, но на самом деле вы хотите этого.
class Header
{
int type;
int payloadLength;
char payload[0];
};
char* Header::GetPayload()
{
return payload;
}
мой голос кодирует ужас. Не поймите меня неправильно, это умно, но вы экономите себе целую операцию добавления за счет того, что код намного сложнее понять и прочитать. Я не считаю, что компромисс того стоит.
Я думаю, что это неправильно с самого начала, если заголовок должен "возвращать" данные, которые не включены в него.
поскольку вы уже поставили себя на эти хакерские основания, мне очень нравится то, что вы придумали.
но учтите, что это не конкурс красоты. Вы должны найти совершенно другое решение. На alle трех версиях GetPayload() вы представили, я не понимаю, Какого черта происходит без дальнейшего объяснения.
вы рассматривали трюк пустой элемент массива'? Я помню, что часто видел его и даже использовал его один или два раза, но я не могу найти никаких действительно хороших ссылок (кроме, возможно, того, на который ссылаются ниже).
фокус в том, чтобы объявить struct как
struct bla {
int i;
int j;
char data[0];
}
затем член "data" просто указывает на то, что находится за заголовками. Я не уверен, насколько это портативно; я видел его с " 1 " в качестве размера массива.
(используя URL ниже в качестве рефереса, используя синтаксис " [1]", казалось, не работает, потому что он слишком длинный. Вот ссылка:)
http://developer.apple.com/documentation/DeveloperTools/gcc-4.0.1/gcc/Zero-Length.html
Если он работает-постоянно, тогда это элегантное решение.
Он обычно будет работать в памяти, потому что компилятор будет заниматься проблемами выравнивания, и вы можете предположить, что полезная нагрузка следует за заголовком В правильно выровненном пространстве памяти.
Я мог видеть, что это разваливается, когда объекты заголовка/полезной нагрузки передаются "по проводу", потому что механизм потоковой передачи, который вы используете, вероятно, не будет заботиться о выравнивании объектов на какой-либо конкретной границе. Следовательно, Полезная нагрузка может непосредственно следовать за заголовком без заполнения, чтобы принудить его к определенному выравниванию.
монета Фраза, элегантный, как элегантный делает. Таким образом, это элегантно, пока вы осторожны, как Вы поток его.
заголовок хранитель ее полезной?
Это фундаментальная проблема здесь - и заголовок, и полезная нагрузка должны управляться другим объектом, который содержит все сообщение, и это подходящее место для запроса полезной нагрузки. И это можно было бы сделать без арифметики указателя или индексирования.
дали это, я бы предпочел второе решение, так как это яснее, что происходит.
но то, что мы находимся в этой ситуации для начала, кажется, указывает на то, что культура вашей команды ценит ум над ясностью, поэтому я думаю, что все ставки сделаны.
Если вы действительно хотите быть милым, вы можете обобщить.
template<typename T. typename RetType>
RetType JustPast(const T* pHeader)
{
return reinterpret_cast<RetType>(pHeader + sizeof(T));
}
они в основном то же самое, что и я. Оба являются формами байтового жонглирования, что всегда рискованно, но не невозможно получить право. Первая форма немного более приемлема и узнаваема. Я бы лично написал:
char* Header::GetPayload()
{
return ((char*) this) + sizeof(*this);
}
не забывайте, что VC++ может наложить прокладки на sizeof()
значение в классе. Поскольку в приведенном примере ожидается 8 байт, он автоматически выравнивается по DWORD, поэтому должен быть в порядке. Проверка #pragma pack
.
хотя, я согласен, приведенные примеры являются некоторой степенью кодирования ужаса. Многие структуры данных Win32 включают заполнитель указателя в структуру заголовка, когда следуют данные переменной длины. Это, пожалуй, самый простой способ ссылаться на эти данные один раз она загружена в память. В МАПИ SRowSet
структура является одним из примеров такого подхода.
Я действительно делаю что-то подобное, и так делает почти каждый MMO или онлайн-видеоигры, когда-либо написанные. Хотя у них есть концепция, называемая "пакет", и каждый пакет имеет свой собственный макет. Так что вы могли бы:
struct header
{
short id;
short size;
}
struct foo
{
header hd;
short hit_points;
}
short get_foo_data(char *packet)
{
return reinterpret_cast<foo*>(packet)->hit_points;
}
void handle_packet(char *packet)
{
header *hd = reinterpret_cast<header*>(packet);
switch(hd->id)
{
case FOO_PACKET_ID:
short val = get_foo_data(packet);
//snip
}
}
и они делают это для большинства своих пакетов. Некоторые пакеты, очевидно, имеют динамические размеры, и для этих членов они используют поля с префиксами длины и некоторую логику для анализа этих данных.
Я думаю, что в этот день и в этом возрасте, в C++, C-style cast to char* дисквалифицирует вас от любой награды "блестящая идея дизайна", не получив много слуха.
Я мог бы пойти на:
#include <stdint.h>
#include <arpa/inet.h>
class Header {
private:
uint32_t type;
uint32_t payloadlength;
public:
uint32_t getType() { return ntohl(type); }
uint32_t getPayloadLength() { return ntohl(payloadlength); }
};
class Message {
private:
Header head;
char payload[1]; /* or maybe std::vector<char>: see below */
public:
uint32_t getType() { return head.getType(); }
uint32_t getPayloadLength() { return head.getPayloadLength(); }
const char *getPayload() { return payload; }
};
это предполагает C99-ish POSIX, конечно: для порта на платформы, отличные от POSIX, вам нужно будет определить один или оба uint32_t и ntohl самостоятельно, с точки зрения того, что платформа предлагает. Обычно это не трудно.
теоретически вам могут понадобиться прагмы макета в обоих занятия. На практике я был бы удивлен, учитывая фактические поля в этом случае. Проблему можно избежать, читая / записывая данные из / в iostreams по одному полю за раз, а не пытаясь построить байты сообщения в памяти, а затем записать его за один раз. Это также означает, что вы можете представить полезную нагрузку с чем-то более полезным, чем char [], что, в свою очередь, означает, что вам не нужно будет иметь максимальный размер сообщения или возиться с malloc и/или размещением нового или что-то еще. Конечно вводит немного накладных расходов.
возможно, Вам следовало использовать подробный метод, но заменить его макросом #define? Таким образом, вы можете использовать стенографию при наборе текста, но любой, кто нуждается в отладке кода, может следовать без проблем.
Я голосую за & this[1]. Я видел, что он используется довольно много при разборе файлов, которые были загружены в память (которые могут одинаково включать полученные пакеты). Это может показаться немного странным, когда вы видите его в первый раз, но я думаю, что это означает, что это должно быть сразу очевидно: это явно адрес памяти сразу за этим объектом. Это хорошо, потому что трудно ошибиться.
Я не люблю использовать такие слова, как "преступление". Я бы предпочел указать, что &this[1], похоже, делает предположения о макете памяти, с которыми компилятор может не согласиться. Например, любой компилятор может по своим собственным причинам (например, выравнивание) вставлять фиктивные байты в любую структуру. Я бы предпочел метод, который имеет больше гарантии получения правильного смещения, если компиляторы или параметры будут изменены.
в дополнение к вышесказанному, я бы сказал, что это преступление против совместимости и принципов проектирования хорошего проводного протокола. Действительно удивительно, сколько программистов не могут / не хотят проводить четкое различие между определением проводного протокола и его реализацией. Если ваш протокол должен выжить более двух дней, он, скорее всего, должен выжить более двух лет/ОС/компиляторов / языков/endiannesses и в какой-то момент он сломается, скорее раньше, чем позже. Итак, сделайте жизнь других людей проще, запишите спецификацию протокола провода плюс напишите правильные (de)процедуры сериализации. В противном случае люди будут продолжать упоминать ваше имя в не очень приятных контекстах.