С++ пример кодирования ужасов или блестящей идеи?

На предыдущем работодателе мы писали бинарные сообщения, которые приходилось "прокладывать" на другие компьютеры. Каждое сообщение имело стандартный заголовок:

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];
}

Вначале кажется довольно тревожным, возможно, слишком странным - но очень компактным. Было много дебатов о том, было ли это блестящим или мерзостью.

Итак, что это - преступление против кодирования, или хорошее решение? У вас когда-либо был подобный компромисс?

-Обновление:

Мы попробовали массив нулевого размера, но в то время компиляторы выдавали предупреждения. В конце концов мы перешли к унаследованному методу: Сообщение происходит от заголовка. Он отлично работает на практике, но в принципе вы говорите сообщение IsA Header - что кажется немного неудобным.

Ответ 1

Я бы пошел за преступлением против кодирования.

Оба метода будут генерировать точный код объекта. Первое делает это ясно. Второй очень запутанный, с единственным преимуществом, что он экономит пару нажатий клавиш. (Просто научитесь подраться).

Кроме того, обратите внимание, что метод НИАТ гарантированно работает. Объект sizeof() включает в себя дополнение для выравнивания слов, так что если заголовок был:

class Header
{
    int type;
    int payloadLength;
    char  status;
};

Оба описанных вами метода будут иметь полезную нагрузку, начиная с Header + 12, когда, скорее всего, она начинается с Header + 9.

Ответ 2

В зависимости от того, какой компилятор должен компоновать ваши классы определенным образом. Я бы определил сообщение как структуру (со мной определил макет) и имел класс, который инкапсулирует сообщение и предоставляет ему интерфейс. Очистить код = хороший код. Код "Симпатичный" = плохой (трудно поддерживать).

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;
   }
}

Прошло некоторое время с тех пор, как я написал любой С++, поэтому, пожалуйста, извините за любые проблемы с синтаксисом. Просто пытаюсь передать общую идею.

Ответ 3

Лично я считаю, что если есть преступление, он спрашивает заголовок для полезной нагрузки.

Но пока вы собираетесь это делать, "этот + 1" так же хорош, как и любой.

Обоснование: '& this [1]' - это код общего назначения, который не требует, чтобы вы копались в определениях классов, чтобы полностью понять и не нуждаетесь в исправлении, когда кто-то меняет имя или содержимое класса.

Кстати, первым примером является истинное преступление против человечества. Добавьте участника в конец класса, и он не удастся. Перемещайте участников вокруг класса, и он потерпит неудачу. Если компилятор загружает класс, он не работает.

Кроме того, если вы предполагаете, что макет компилятора классов/структур соответствует макету пакета, вы должны понимать, как работает этот компилятор. Например. на MSVC вы, вероятно, захотите узнать о #pragma pack.

PS: Немного страшно, сколько людей считают "это + 1" или "& this [1]" трудно читать или понимать.

Ответ 4

Это общая проблема, но то, что вы на самом деле хотите, - это.

class Header
{
    int type;
    int payloadLength;
    char payload[0];

};

char* Header::GetPayload()
{
    return payload;
}

Ответ 5

Мое голосование - ужас кодирования. Не поймите меня неправильно, это умно, но вы сэкономите себе всю операцию сложения за счет того, что код намного сложнее понять и прочитать. Я не вижу компромисса, как того стоит.

Ответ 6

Я думаю, что это неправильно с самого начала, если заголовок должен "возвращать" данные, которые не включены в него.

Как вы уже положили себя на эти хакерские уголки, мне очень нравится то, с чем вы пришли.

Но обратите внимание, что это не конкурс красоты. Вы должны найти совершенно другое решение. Для всех трех версий GetPayload(), которые вы представили, я не понимаю, что, черт возьми, происходит там без вашего дальнейшего объяснения.

Ответ 7

Вы считали трюк "пустой массив"? Я помню, как часто видел это и даже использовал его один или два раза, но я не могу найти каких-либо действительно хороших ссылок (за исключением, может быть, ссылки, приведенной ниже).

Трюк состоит в том, чтобы объявить вашу структуру как

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

Ответ 8

Если это работает - последовательно - тогда это элегантное решение.

Он обычно работает в памяти, потому что компилятор будет заниматься проблемами выравнивания, и вы можете предположить, что полезная нагрузка следует за заголовком в правильно выровненном пространстве памяти.

Я мог видеть, что это разваливается, когда объекты Header/Payload передаются "по кабелю", потому что используемый вами механизм потоковой передачи, вероятно, не заботится о выравнивании объектов на какой-либо конкретной границе. Следовательно, полезная нагрузка может непосредственно следовать за заголовком без прокладки, чтобы принудительно установить его на конкретное выравнивание.

Чтобы выставить фразу, элегантная элегантность. Таким образом, он элегантен до тех пор, пока вы будете осторожны, как вы его передаете.

Ответ 9

Во-первых, там очень много места между "преступлением против кодирования" и "хорошим решением", но я бы сказал, что это ближе к первому.

Является ли заголовок его хранителем полезной нагрузки?

Что основная проблема здесь - как заголовок, так и полезная нагрузка должны управляться другим объектом, который содержит все сообщение, и это подходящее место для запроса полезной нагрузки. И он мог бы сделать это без арифметики указателей или индексирования.

Учитывая это, я бы предпочел второе решение, поскольку яснее, что происходит.

Но мы с этой ситуацией начинаем с того, что, по-видимому, культура вашей команды ценит умность над ясностью, поэтому я думаю, что все ставки отключены.

Если вы действительно хотите быть симпатичным, вы можете обобщить.

template<typename T. typename RetType>
RetType JustPast(const T* pHeader)
{
   return reinterpret_cast<RetType>(pHeader + sizeof(T));
}

Ответ 10

Они в основном то же самое, что и я. Оба являются формами манипулирования байтами, что всегда рискованно, но не невозможно. Первая форма немного более приемлема и узнаваема. Я лично напишу:

char* Header::GetPayload()
{
    return ((char*) this) + sizeof(*this);
}

Ответ 11

Не забывайте, что VС++ может накладывать дополнение на значение sizeof() в классе. Поскольку ожидаемый пример должен быть 8 байтов, он автоматически выравнивается по DWORD, поэтому должно быть хорошо. Проверьте #pragma pack.

Хотя, я согласен, приведенные примеры - это некоторая степень Coding Horror. Многие структуры данных Win32 включают указатель-заполнитель в структуре заголовка, когда следуют данные переменной длины. Это, пожалуй, самый простой способ ссылаться на эти данные после его загрузки в память. Одним из примеров такого подхода является структура MAPI SRowSet.

Ответ 12

Я действительно делаю что-то подобное, и поэтому почти каждая 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
    }
}

И они делают это для большинства своих пакетов. Некоторые пакеты, очевидно, имеют динамические размеры, и для этих членов они используют префиксные поля длины и некоторую логику для анализа этих данных.

Ответ 13

Я думаю, что в этот день и в возрасте, на С++, C-стиль, отбрасываемый в 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 и/или новым местом размещения или что-то еще. Конечно, это немного накладные расходы.

Ответ 14

Возможно, вы должны использовать подробный метод, но заменили его макросом #define? Таким образом, вы можете использовать свою стенографию при наборе текста, но любой, кто должен отлаживать код, может следовать без проблем.

Ответ 15

Я голосую за это [1]. Я видел, что это использовалось совсем немного, когда парсинг файлов, которые были загружены в память (которые могут в равной степени включать принятые пакеты). Это может показаться странным в первый раз, когда вы его видите, но я думаю, что то, что это значит, должно быть сразу же очевидным: это явно адрес памяти только за этим объектом. Это приятно, потому что трудно ошибиться.

Ответ 16

Я не люблю использовать такие слова, как "преступление". Я хотел бы указать, что & этот [1], похоже, делает предположения о макете памяти, с которыми компилятор может не согласиться. Например, любой компилятор может по своим собственным причинам (например, выравнивание) вставлять фиктивные байты в любом месте структуры. Я бы предпочел технику, которая имеет больше гарантии получения правильного смещения, если компиляторы или параметры будут изменены.

Ответ 17

В дополнение к вышесказанному, я бы сказал, что это преступление против совместимости и принципов построения проводных протоколов. Неудивительно, что многие программисты не могут/не хотят четко различать определение проводного протокола и его реализацию. Если ваш протокол должен выжить в течение более двух дней, он, скорее всего, должен выжить более двух лет/ОС/компиляторы/языки/суждения, и в какой-то момент он сломается, скорее, чем позже. Итак, облегчите жизнь другим людям, запишите спецификацию протокола проводов и напишите правильные (де) процедуры сериализации. В противном случае люди будут продолжать упоминать ваше имя в не очень приятных контекстах.