Массив байтов в POD

Скажем, у меня есть массив неподписанных символов, который представляет собой пучок объектов POD (например, читается из сокета или через mmap). Какие типы они представляют и в какой позиции определяются во время выполнения, но мы предполагаем, что каждый из них уже правильно выровнен.

Каков наилучший способ "бросить" эти байты в соответствующий тип POD?

Решение должно либо соответствовать стандарту С++ (пусть говорят >= С++ 11), либо, по крайней мере, будет гарантировано работать с g++ >= 4.9, clang++ >= 3.5 и MSVC >= 2015U3. РЕДАКТИРОВАТЬ: В linux, windows, работает под управлением x86/x64 или 32/64-бит.

В идеале я хотел бы сделать что-то вроде этого:

uint8_t buffer[100]; //filled e.g. from network

switch(buffer[0]) {
    case 0: process(*reinterpret_cast<Pod1*>(&buffer[4]); break;
    case 1: process(*reinterpret_cast<Pod2*>(&buffer[8+buffer[1]*4]); break;
    //...
}

или

switch(buffer[0]) {
    case 0: {
         auto* ptr = new(&buffer[4]) Pod1; 
         process(*ptr); 
    }break;
    case 1: {
         auto* ptr = new(&buffer[8+buffer[1]*4]) Pod2; 
         process(*ptr); 
    }break;
    //...
}

Оба, похоже, работают, но оба являются действиями AFAIK undefined в С++ 1). И только для полноты: я знаю о "обычном" решении просто скопировать материал в соответствующую локальную переменную:

 Pod1 tmp;
 std::copy_n(&buffer[4],sizeof(tmp), reinterpret_cast<uint8_t*>(&tmp));             
 process(tmp); 

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


Несколько сумасшедшее решение, которое я придумал, следующее:

template<class T>
T* inplace_cast(uint8_t* data) {
    //checks omitted for brevity
    T tmp;
    std::memmove((uint8_t*)&tmp, data, sizeof(tmp));
    auto ptr = new(data) T;
    std::memmove(ptr, (uint8_t*)&tmp,  sizeof(tmp));
    return ptr;

}

g++ и clang++, похоже, могут оптимизировать эти копии, но я думаю, что это накладывает большую нагрузку на оптимизатор и может привести к сбою других оптимизаций, не работает с const uint8_t* (хотя я не хочу на самом деле изменить его) и выглядит просто ужасно (не думайте, что вы получите этот обзор прошлого кода).


1) Первый - это UB, потому что он нарушает строгий псевдоним, второй, вероятно, UB (обсуждается здесь), потому что стандарт просто говорит, что результирующий объект не инициализирован и имеет неопределенное значение (вместо того, чтобы гарантировать, что базовая память не затронута). Я считаю, что первый эквивалентный c-код хорошо определен, поэтому компиляторы могут разрешить это для совместимости с c-заголовками, но я не уверен в этом.

Ответ 1

Наиболее правильным способом является создание (временная) переменной желаемого класса POD и использование memcpy() для копирования данных из буфера в эту переменную:

switch(buffer[0]) {
    case 0: {
        Pod1 var;
        std::memcpy(&var, &buffer[4], sizeof var);
        process(var);
        break;
    }
    case 1: {
        Pod2 var;
        std::memcpy(&var, &buffer[8 + buffer[1] * 4], sizeof var);
        process(var);
        break;
    }
    //...
}

Основная причина для этого - из-за проблем с выравниванием: данные в буфере могут быть неправильно выровнены для используемого типа POD. Создание копии устраняет эту проблему. Он также позволяет вам использовать переменную, даже если сетевой буфер больше не доступен.

Только если вы абсолютно уверены, что данные правильно выровнены, вы можете использовать первое предоставленное вами решение.

(Если вы читаете данные из сети, вы всегда должны проверить, что данные действительны в первую очередь и что вы не будете читать за пределами своего буфера. Например, с помощью &buffer[8 + buffer[1] * 4] вы должны проверить, что начало этого адреса плюс размер Pod2 не превышает длину буфера. К счастью, вы используете uint8_t, иначе вам также нужно будет проверить, что buffer[1] не является отрицательным.)

Ответ 2

Использование объединения позволяет избежать правила сглаживания. На самом деле для этого нужны профсоюзы. Таким образом, указывание на тип объединения из типа, являющегося частью объединения, явно разрешено в стандарте С++ (пункт 3.10.10.6). То же самое допустимо в стандарте C (6.5.7).

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

union to_pod {
    uint8_t buffer[100];
    Pod1 pod1;
    Pod1 pod2;
    //...
};

uint8_t buffer[100]; //filled e.g. from network

switch(buffer[0]) {
    case 0: process(reinterpret_cast<to_pod*>(buffer + 4)->pod1); break;
    case 1: process(reinterpret_cast<to_pod*>(buffer + 8 + buffer[1]*4)->pod2); break;
    //...
}