Подтвердить что ты не робот

Общее хранилище на основе char [] и исключение ограничений UB с жестким сглаживанием

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

Если программа пытается получить доступ к сохраненному значению объекта с помощью glvalue, отличного от одного из следующих типов, поведение undefined:

  • динамический тип объекта,
  • cv-квалифицированная версия динамического типа объекта,
  • тип, аналогичный (как определено в 4.4) для динамического типа объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим динамическому типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квитанционной версии динамического типа объекта,
  • тип агрегата или объединения, который включает один из вышеупомянутых типов среди его элементов или нестатических членов данных (включая рекурсивно элемент или нестатический элемент данных субагрегата или содержащегося объединения),
  • тип, который является (возможно, cv-квалифицированным) типом базового класса динамического типа объекта,
  • a char или unsigned char.

Учитывая формулировку выделенной точки, я придумал следующую идею 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;
}

(Вышеуказанный тестовый код, особенно класс Data, является просто демонстрацией идеи, поэтому, пожалуйста, не указывайте, как я должен использовать std::pair или std::tuple. Шаблон alias_cast также должен быть расширен для обработки квалифицированных типов, и его можно безопасно использовать только в том случае, если требования к выравниванию выполнены, но я надеюсь, что этого фрагмента достаточно, чтобы продемонстрировать идею.)

Этот трюк заглушает предупреждения 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: при создании "T & Данные:: first() [с T = int; U = короткий unsigned int]: aliastest-so-1.cpp: 28: 16:
требуется отсюда aliastest-so-1.cpp: 21: 58: предупреждение: разыменование type-punned pointer будет нарушать правила строгого сглаживания [-Wstrict сглаживание]

4b9b3361

Ответ 1

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

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

// 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 - это динамический тип объекта здесь. Хранилище было повторно использовано, когда новое выражение создало объект T, действие которого неявно закончило время жизни storage (что тривиально происходит как unsigned char - это, ну, тривиальный тип).

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

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

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

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

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

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