Вопрос по c++, strict-aliasing, type-punning, c++11, undefined-behavior – Общее хранилище на основе char [] и исключение UB, связанных со строгим псевдонимом

11

Я пытаюсь создать шаблон класса, который объединяет несколько типов в достаточно большой массив символов и позволяет получить доступ к данным в виде отдельных правильно типизированных ссылок. Теперь в соответствии со стандартом это может привести к нарушению строгого псевдонима и, следовательно, к неопределенному поведению, так как мы получаем доступ кchar[] данные через объект, который не совместим с ним. В частности, стандарт гласит:

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:

the dynamic type of the object, a cv-qualified version of the dynamic type of the object, a type similar (as defined in 4.4) to the dynamic type of the object, a type that is the signed or unsigned type corresponding to the dynamic type of the object, a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object, an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union), a type that is a (possibly cv-qualified) base class type of the dynamic type of the object, a char or unsigned char type.

Учитывая формулировку выделенного пункта, я пришел к следующемуalias_cast идея:

#include <iostream>
#include <type_traits>

template <typename T>
T alias_cast(void *p) {
    typedef typename std::remove_reference<T>::type BaseType;
    union UT {
        BaseType t;
    };
    return reinterpret_cast<UT*>(p)->t;
}

template <typename T, typename U>
class Data {
    union {
        long align_;
        char data_[sizeof(T) + sizeof(U)];
    };
public:
    Data(T t = T(), U u = U()) { first() = t; second() = u; }
    T& first() { return alias_cast<T&>(data_); }
    U& second() { return alias_cast<U&>(data_ + sizeof(T)); }
};


int main() {
    Data<int, unsigned short> test;
    test.first() = 0xdead;
    test.second() = 0xbeef;
    std::cout << test.first() << ", " << test.second() << "\n";
    return 0;
}

(The above test code, especially the Data class is just a dumbed-down demonstration of the idea, so please don't point out how I should use std::pair or std::tuple. The alias_cast template should also be extended to handle cv qualified types and it can only be safely used if the alignment requirements are met, but I hope this snippet is enough to demonstrate the idea.)

Этот трюк заставляет замолчать предупреждения g ++ (при компиляции сg++ -std=c++11 -Wall -Wextra -O2 -fstrict-aliasing -Wstrict-aliasing), и код работает, но действительно ли это верный способ указать компилятору пропустить оптимизацию на основе строгого алиасинга?

Если он недействителен, то как можно реализовать подобный класс общего хранения на основе массива char без нарушения правил наложения имен?

Редактировать: заменяяalias_cast с простымreinterpret_cast как это:

T& first() { return reinterpret_cast<T&>(*(data_ + 0)); }
U& second() { return reinterpret_cast<U&>(*(data_ + sizeof(T))); }

выдает следующее предупреждение при компиляции с g ++:

aliastest-so-1.cpp: In instantiation of ‘T& Data::first() [with T = int; U = short unsigned int]’: aliastest-so-1.cpp:28:16:
required from here aliastest-so-1.cpp:21:58: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]

@mitchnull: я не понимаю, почему должна быть проблема с псевдонимами, посколькуchar* а такжеvoid* специально в этом отношении. Matthieu M.
@mitchnull: интересно, может быть, легче будет разработать легкий кортеж. Интересно, если это возможно, хотя. В противном случае, вы знаете оstd::aligned_storage ? Он был введен в C ++ 11 для манипулирования необработанной памятью и по-прежнему имеет правильный размер и выравнивание :) Matthieu M.
Два примечания: 1) не долженalias_cast обрабатывать арифметику указателя самостоятельно? 2) Вы можете получить доступ к отдельным байтам, а затем, возможно, создать конечный объект через что-то вродеmemcpy, Конечно, это не будет работать ни для чего с нетривиальным ctor. Вы ищете поддержку универсальных типов или просто фундаментальных типов? Это для некоторого класса упакованного кортежа, не так ли? dirkgently
@mitchnull: реальный вопрос в том, что если вы уважаете выравнивание и размеры, то вы переопределяете логику, которую компилятор уже обеспечивает для регулярной структуры. Что вы получаете? Matthieu M.
@ Да, это что-то вроде упакованного кортежа, иmemcopy будет "безопасным" Держу пари, я просто стараюсь избегать этого из-за соображений производительности, если мне это сойдет с рук :) mitchnull

Ваш Ответ

1   ответ
3

если вы хотите придерживаться строгого соответствия, у них есть строгие правила, когда речь идет о чтении активного члена (и только этого). Хотя нужно сказать, что реализации любят использовать союзы в качестве хуков для надежного поведения, и, возможно, это то, что вам нужно. Если это так, я обращаюсь к Майку Актону, который написалхорошая (и длинная) статья о правилах алиасинга, где он комментирует кастинг через союз.

Насколько мне известно, вот как вы должны обращаться с массивами типов символов в качестве хранилища:

// char or unsigned char are both acceptable
alignas(alignof(T)) unsigned char storage[sizeof(T)];
::new (&storage) T;
T* p = static_cast<T*>(static_cast<void*>(&storage));

Причина, по которой это определено для работы заключается в том, чтоT is динамический тип объекта здесь. Хранилище было повторно использовано, когда новое выражение создалоT объект, операция которого неявно закончила время жизниstorage (что происходит тривиально, какunsigned char это, ну,trivial тип).

Вы все еще можете использовать, например,storage[0] читать байты объекта, так как это читает значение объекта через glvalueunsigned char тип, одно из перечисленных явных исключений. Если с другой стороныstorage были другого, но все же тривиального типа элемента, вы можете сделать приведенный выше фрагмент, но не сможетеstorage[0].

Последний фрагмент, который сделает фрагмент понятным, - это преобразование указателя. Обратите внимание, чтоreinterpret_cast являетсяnot подходит в общем случае. Это может быть действительным, учитывая, чтоT это стандартная компоновка (есть и дополнительные ограничения на выравнивание), но если это так, то используйтеreinterpret_cast будет эквивалентноstatic_castчерезvoid как и я Во-первых, имеет смысл использовать эту форму непосредственно, особенно если учесть, что использование хранилища часто происходит в общих контекстах. В любом случае преобразование в и изvoid является одним из стандартных преобразований (с четко определенным значением), и вы хотитеstatic_cast для тех.

Если вас вообще беспокоит преобразование указателя (которое, на мой взгляд, является самым слабым звеном, а не аргумент о повторном использовании хранилища), то альтернативой является

T* p = ::new (&storage) T;

который стоит дополнительный указатель в хранилище, если вы хотите отслеживать его.

Я от всей души рекомендую использоватьstd::aligned_storage.

@curiousguy C ++ не имеет понятия «встроенный тип». 3.8 параграф 1 очень четко описывает конец времени жизни: "Время жизни объекта типа T заканчивается, когда: & # x2014; если T является типом класса с нетривиальным деструктором (12.4), начинается вызов деструктора, или & # x2014; хранилище, которое занимает объект, используется повторно или освобождается. & quot;
Ах, я могу изменить динамический тип хранилища с новым размещением. Могу ли я сделать это "посередине" из массива тоже? Из примера, могу ли я изменить первую половину моегоdata_ массив для вводаT а вторая половина набратьU? mitchnull
Я изменил приведенный выше код, чтобы использовать размещение нового в конструкторе, и заменилalias_cast с броском черезvoid*, но это приведение все еще вызывает предупреждение строгого алиасинга в g ++ ... mitchnull
@underscore_d даже лучше, это точно означает, что при любых обстоятельствах в грядущем C ++ 1z (perishable link to draft)
"Отметим, что reinterpret_cast не подходит в общем случае. _ & Quot; Я абсолютно уверен, что это уместно в самом деле. Если в стандарте не сказано, значит, он явно нарушен.

Похожие вопросы