Проблема каламбура в C++

У меня есть класс шаблона с bool в качестве параметра шаблона Dynamic<bool>. Независимо от того, является ли параметр истинным или ложным, он имеет точно такие же элементы данных. они просто отличаются своими функциями-членами.

Есть одна ситуация, когда мне нужно временно преобразовать одно в другое вместо использования конструктора копирования/перемещения. Поэтому я прибег к каламбуру. Чтобы убедиться, что это вызывает проблему, я использовал два static_asserts:

d_true=Dynamic<true>(...);
...
static_assert(sizeof(Dynamic<true>)==sizeof(Dynamic<false>),"Dynamic size mismatch");
static_assert(alignof(Dynamic<true>)==alignof(Dynamic<false>),"Dynamic align mismatch");
Dynamic<false>& d_false=*reinterpret_cast<Dynamic<false>*>(&d_true);
...

Поэтому я думаю, что то, что я делаю, безопасно, и если что-то пойдет не так, компилятор выдаст мне ошибку static_assert. Однако gcc выдает предупреждение:

warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]

Мой вопрос двоякий: то, что я делаю, лучший способ добиться этого? Если это так, как убедить gcc, что это безопасно, и избавиться от предупреждения?


person Lawless    schedule 02.08.2019    source источник
comment
Вам не разрешено делать такие каламбуры в C++.   -  person druckermanly    schedule 02.08.2019
comment
Нет, то, что вы делаете, не лучший способ добиться этого. Лучший способ добиться этого — не использовать каламбуры, а перепроектировать ваши шаблоны и связанные с ними классы, чтобы такие упражнения не требовались.   -  person Sam Varshavchik    schedule 02.08.2019


Ответы (4)


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

struct Common {
// ...
};

template <bool b>
class Dynamic { 
    Common c;
public:
    Common &get_data() { return c; }
    // ...
};

Отсюда остальное кажется довольно очевидным — когда вам нужны данные из Dynamic<whatever>, вы звоните get_data() и уходите.

Конечно, есть и вариации на общую тему — например, вместо этого вы можете использовать наследование:

struct Common { /* ... */ };

template <bool t>
class Dynamic : public Common {
    // ...
};

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

person Jerry Coffin    schedule 02.08.2019

В стандарте «запрещено» переинтерпретировать область памяти из типа A в тип B. Это называется алиасингом. Есть 3 исключения для псевдонимов, одинаковые типы с разной квалификацией CV, базовые типы и регионы char[]. (а для char отступление работает только однонаправленно в направлении char)

Если вы используете std::aligned_storage и новое размещение, вы можете переинтерпретировать этот регион во что угодно, и компилятор не сможет жаловаться. Вот как работает variant.

РЕДАКТИРОВАТЬ: Хорошо, вышесказанное на самом деле верно (если вы не забудете std::launder), но вводит в заблуждение из-за «срока службы». Одновременно поверх области хранения может находиться только один объект. Таким образом, это неопределенное поведение — интерпретировать его через представление другого типа, пока оно живо. Ключ — это конструкция.

Если я могу предложить, перейдите на cppreference, возьмите их static_vector пример, упростите его. к случаю 1. Добавьте несколько геттеров, поздравляю, вы заново изобрели bitcast :) (предложение http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0476r2.html).

Вероятно, это будет выглядеть так:

#include <type_traits>
#include <string>
#include <new>
#include <cstring>
#include <iostream>

using namespace std;

template< bool B >
struct Dynamic
{
    template <bool B2 = B>
    void ConditionalMethod(typename enable_if<B2>::type** = 0)
    {}

    string m_sharedObject = "stuff";
};

int main()
{
    using D0 = Dynamic<false>;
    using D1 = Dynamic<true>;
    aligned_storage<sizeof(D0), alignof(D0)>::type store[1];

    D0* inst0 = new (&store[0]) D0 ;

    // mutate
    inst0->m_sharedObject = "thing";

    // pune to D1
    D1* inst1 = std::launder(reinterpret_cast<D1*>(&store[0]));

    // observe aliasing
    cout << inst1->m_sharedObject;

    inst0->~D0();
}

см. действие в wandbox

РЕДАКТИРОВАТЬ: после продолжительного обсуждения есть и другие части нового стандарта, кроме раздела «Типы 8.2.1.11», которые лучше объясняют, почему это не является строго действительным. Я рекомендую обратиться к главе «пожизненной».
https://en.cppreference.com/w/cpp/language/lifetime
Из комментария Майлза Буднека:

по этому адресу нет объекта Dynamic<true>, доступ к нему через Dynamic<false> является поведением undefined.

person v.oddou    schedule 02.08.2019
comment
std::variant не использует каламбур. И союзы afaik не позволяют каламбурить типы в C ++ (они делают это в C). - person bolov; 02.08.2019
comment
@bolov переинтерпретирует область char в тип, который вы получаете после проверки того, что текущий индекс соответствует этому типу. это не каламбур, но если вы хотите каламбурить, вам придется пройти через область чар. - person v.oddou; 02.08.2019
comment
@v.oddou Нет, он не интерпретирует массив char как другой тип. Он использует place-new для создания нового объекта в неинициализированном хранилище. Это очень разные вещи. Первое приводит к неопределенному поведению, а второе — к четко определенному. - person Miles Budnek; 02.08.2019
comment
comment
@v.oddou, что эта ссылка должна доказать? - person M.M; 02.08.2019
comment
@M.M, что унифицированное хранилище не сильно отличается от массива символов. массив символов - единственный способ создать унифицированное пространство для хранения (в автоматическом хранилище продолжительности). Я не понимаю смысла сильно различать их. variant использует storage_t, который по сути является std::aligned_storage до того, как он был стандартизирован. И как это storage_t реализовано, это unsigned char[sizeof(T) + alignof(T)] space и функция address() получает первый выровненный указатель этого пространства. И это вовсе не УБ, так как стандарт допускает отступление от алиасинга для регионов char - person v.oddou; 02.08.2019
comment
Дело в том, что содержимое массива char никогда не переинтерпретируется (что было бы неопределенным поведением). Цель aligned_storage состоит в том, чтобы вызывающая сторона могла использовать новое размещение для создания новых объектов в той же области памяти (что завершает время жизни массива символов). Также стандарт НЕ позволяет использовать псевдоним массива char как другой тип. Это позволяет использовать псевдонимы других типов в виде массива символов. - person M.M; 02.08.2019
comment
@ М.М.? за что меня здесь критикуют, за переосмысление терминологии? источник действительно использует static_cast, что делает его другой концепцией? aligned_storage это не примитив языка, это библиотечная штуковина, согласитесь. мысленно замените его массивом char (+ малюсенькая подсказка по выравниванию для типа T). Что вы называете прекращением жизни массива символов? Вы имеете в виду что-то вроде стандартной формулировки для союзов: максимум один из нестатических элементов данных может быть активен в любое время? - person v.oddou; 02.08.2019
comment
@MM Также стандарт НЕ позволяет использовать псевдоним массива символов в качестве источника другого типа? - person v.oddou; 02.08.2019
comment
Это находится в строгом правиле псевдонимов в стандарте, в нем перечислены разрешенные типы псевдонимов, и ни один из них не использует псевдоним char как общий тип. - person M.M; 02.08.2019
comment
Placement-new завершает жизнь любого объекта, который ранее существовал в этом месте, так говорит стандарт. - person M.M; 02.08.2019
comment
@M.M этот список прав stackoverflow.com/a/7005988/893406? Placement-new заканчивает жизнь массива char › хорошо, я пойду прочитаю это. - person v.oddou; 02.08.2019
comment
Источник @v.oddou: open-std. org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf §8.2.1 Категория значения [basic.lval], параграф 11. Вы можете получить доступ к объекту через glvalue типа char, но не наоборот. - person bolov; 02.08.2019
comment
@bolov OOOOH вот и все, я вижу здесь проблему! Хорошо, хорошо, хорошо, в случае OP его объект уже существует и, следовательно, не был переинтерпретирован из массива символов, поэтому его нельзя каламбурить без копирования. Вот почему bit_cast выполняет memcopy. Хорошо, и это объясняет, почему variant и optional работают, потому что их хранилищем является область char с самого начала. Теперь остающийся вопрос: если вы уже прокачали своего персонажа до Т, достаточно ли у вас оставшейся маны, чтобы прокачать его до Т2, пока Т жив? Если массив символов мертв из-за new T(p), это будет скомпрометировано. - person v.oddou; 02.08.2019
comment
подождите минуту. но это то, что я рекомендовал в своем ответе. так в чем проблема ? - person v.oddou; 02.08.2019
comment
@v.oddou Дело в том, что variant никогда ничего не переосмысливает. Если он изменяет тип, который он хранит, он сначала уничтожает старый объект, а затем создает новый. Оба объекта занимают одно и то же хранилище, но только один за другим. Ничто не каламбурно или переосмыслено. OP хочет обрабатывать тот же объект как другой тип, не копируя его, и вы просто не можете этого сделать (за исключением очень ограниченных обстоятельств, ни одно из которых не связано с функциями-членами). - person Miles Budnek; 02.08.2019
comment
@MilesBudnek хорошо, значит, вы говорите, что идея, представленная в коде в моем ответе, незаконна, верно? Я готова принять это, но это не кажется слишком очевидным. Напротив, формулировка в §8.2.1 является первоначальным толчком к моей идее использования char для использования допустимых псевдонимов. И в моем примере нет ретро-преобразования из T в char (незаконный способ, на который указал Болов). Если об этом методе действительно не может быть и речи, мне придется удалить свой ответ. Но не ясно, что это. - person v.oddou; 02.08.2019
comment
@v.oddou У тебя все наоборот. Доступ к байтовому представлению любого объекта можно получить, приведя указатель на этот объект к указателю на char (или unsigned char, или std::byte). Обратное неверно. Вы не можете привести указатель к char к несвязанному типу и получить доступ к объекту с помощью указателя с обозначением типа. char* cp = &someObject; char c = *cp; допустимо, SomeType* someObjectPtr = someCharArray; SomeType someObject = *someObjectPtr; поведение не определено. См. [basic.lval]. - person Miles Budnek; 02.08.2019
comment
@MilesBudnek, давай, вот как работает aligned_storage; он делает именно это. Если это UB, то подобные optional основаны на UB, не так ли. Или вы говорите мне, что новое размещение посередине делает его внезапно не сглаживающим? - person v.oddou; 02.08.2019
comment
Стандарт говорит, что «доступ к сохраненному значению объекта через char действителен». Я интерпретирую ваш второй пример как попадающий в этот случай. Неясно, как вы интерпретируете полученное тогда представление char, ограничено этой формулировкой. - person v.oddou; 02.08.2019
comment
@v.oddou Это разница между объектом и значением. Вы можете получить доступ к объекту любого типа через значение типа char. В моем первом примере доступ к объекту типа SomeType осуществляется через значение типа char (ОК). Второй пытается получить доступ к объекту типа char через значение типа SomeType (не определено). aligned_storage работает не иначе. Чтобы использовать aligned_storage, вы должны создать новый объект в памяти, ранее занятой объектом типа aligned_storage<>::type, используя выражение-новое размещение. - person Miles Budnek; 02.08.2019
comment
@MilesBudnek спасибо за вашу помощь, но я думаю, что мы можем остановиться на этом, лол :) Я утверждаю, что создание нового объекта в хранилище волшебным образом не снимает ограничения, которые вы хотите наложить на параграф 8.2.1.11. . Потому что new не является конструкцией абстрактной машины. У него не больше привилегий, чем необработанный доступ. А создание объекта — это написание материала типа T поверх объектного представления этого пространства, которое должно быть UB согласно вашей интерпретации, как только мы интерпретируем это пространство как T* (возвращаемое значение new). И когда вы программируете класс variant (я так и сделал), вы не можете... - person v.oddou; 02.08.2019
comment
... сохраните возвращаемый результат new, но просто используйте базовое хранилище при запросе клиентов через get<T>(). И трудно доказать, что между этими двумя подходами есть разница в пути статического анализа компиляторами. - person v.oddou; 02.08.2019
comment
Кроме того, в параграфе 6.7.4 упоминается, что разница между представлением значения и представлением объекта является потенциальным дополнением - person v.oddou; 02.08.2019
comment
@v.oddou Что значит, что у new больше нет привилегий? У него есть собственный целый раздел стандарта! [basic.stc.dynamic] явно говорит < i>новые выражения создают объекты. Да, вы можете указать указатель на aligned_storage<>::type. Поскольку для C++17 для этого требуется std::launder, поскольку нет объекта тип aligned_storage<>::type там больше не существует. Его время жизни закончилось, когда хранилище было повторно использовано. - person Miles Budnek; 02.08.2019
comment
Давайте продолжим обсуждение в чате. - person Miles Budnek; 02.08.2019
comment
@MilesBudnek и v.oddou, не мог бы кто-нибудь из вас подвести итоги ваших дискуссий и чата? Я немного запутался в ваших разговорах, но я хотел бы включить соответствующие части вашего обсуждения в ответ на вопрос. - person Lawless; 02.08.2019
comment
@Lawless Я сделал это в части РЕДАКТИРОВАТЬ своего ответа. В обсуждении упоминается, что до С++ 17 он был активным источником интерпретационных войн (в отличие от приведенных выше комментариев. Проблема также упоминается в ответе Альфа stackoverflow.com/a/27492569/893406) Но дух, который комитет хотел передать, был разъяснен в С++ 17 понятием времени жизни: в основном невозможно интерпретировать одно пространство памяти через 2 разных типа в в то же время, в духе нового стандарта. Единственный возможный выход для вас — использовать решение Джерри Коффина; или memcpy (решение bit_cast). - person v.oddou; 02.08.2019
comment
Другим интересным побочным моментом было то, что, анализируя, как variant реализован в gcc, Майлз Буднек считает, что двойное статическое приведение через void* и к T* — это раздражающий момент, но, возможно, разработчики библиотек могут быть на стороне UB, если они знают. как реализован gcc и что он на самом деле будет делать. - person v.oddou; 02.08.2019
comment
Наконец, что я нахожу захватывающим, так это введение новой языковой хитрости под названием pointer optimization barrier, которая является std::launder. Это маленькое средство не похоже на что-то особенное в cppreference, и я обязательно прочитаю о нем больше. Но это та же концепция, что и барьеры предотвращения переупорядочения загрузки/сохранения для мьютекса. И мне кажется, что это имеет большое значение, особенно потому, что это звучит точно так же, как примитив, который сделал бы возможным все эти псевдонимы char для T *, нарушив уверенность компилятора в актуальности задействованных регистров, и принудительно перезагрузить. - person v.oddou; 02.08.2019
comment
@v.oddou Я очень старался понять, почему код, который вы разместили выше, незаконен. К сожалению, я не спец в сборке. Буду признателен, если вы дадите мне краткое объяснение. - person Lawless; 02.08.2019
comment
@Lawless Ну, незаконно, это не значит, что это не работает. Я протестировал этот код в MSVC, и он делает то, что ожидалось. Что является незаконным, так это то, что у нас нет гарантии, что обновление компилятора сохранит это хорошее поведение. Или, например, строительство с помощью clang, насколько нам известно. Если бы вы написали эту интерпретацию на ассемблере, у вас не было бы такой проблемы нелегальности. Проблема связана с оптимизацией, которую разрешено выполнять при компиляции в сборку. - person v.oddou; 02.08.2019
comment
@v.oddou Понятно. Но не сделает ли это reinterpret_cast полностью бесполезным? Это не может быть проблемой на всю жизнь, потому что вы используете указатель для каламбура. - person Lawless; 02.08.2019
comment
@Lawless да, с такими правилами интерпретировать звучит бесполезно. Неплохо подмечено. Время жизни в моем примере начинается после new и заканчивается при уничтожении ~D0(). Может быть, пока вы получаете доступ к inst1 только для чтения, у вас все будет хорошо? Я понятия не имею, честно говоря. Этот язык испорчен до неузнаваемости. - person v.oddou; 02.08.2019
comment
Посмотрев на это во второй раз, я думаю, что мой пример неверен. Я должен переставить мутацию на другую строку и вызов отмыть. Потому что после отмывания компилятор может предварительно выполнить путь, который инициализирует строку, чтобы заполнить и сохранить то, на что указывает inst1, как содержимое. launder вызывает перезагрузку, поэтому она должна произойти после задания. Позвольте мне исправить это. - person v.oddou; 02.08.2019
comment
Проблема не в доступе к строке. Доступ к Dynamic<true> через указатель на Dynamic<false> приводит к неопределенному поведению. std::launder не может вам здесь помочь, так как по этому адресу нет объекта Dynamic<true>, на который он мог бы вернуть указатель. Это не может работать, так как нет определенного правильного поведения. Он может делать то, что вы хотите, но это только по стечению обстоятельств, и он может перестать делать то, что вы хотите, по любой причине в любое время. - person Miles Budnek; 02.08.2019
comment
Что касается reinterpret_cast бесполезности: его полезность ограничена. Основная полезность reinterpert_cast заключается в доступе к байтовому представлению объекта путем приведения указателя на него к указателю на char. - person Miles Budnek; 02.08.2019

После прочтения обсуждения в https://stackoverflow.com/a/57318684/2166857 и прочтения исходного кода для bit_cast и много исследований в Интернете, я думаю, что нашел самое безопасное решение моей проблемы. Это работает, только если

1) Выравнивание и размер обоих типов совпадают

2) оба типа легко копируются (https://en.cppreference.com/w/cpp/named_req/TriviallyCopyable)

Сначала определите тип памяти с помощьюalign_storage

typedef std::aligned_storage<sizeof(Dynamic<true>),alignof(Dynamic<true>)>::type DynamicMem;

затем введите переменную этого типа

DynamicMem dynamic_buff;

затем используйте новое размещение, чтобы инициировать объект в первичном классе (в моем случае Dynamic)

new (&dynamic_buff) Dynamic<true>();

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

{
    Dynamic<true>* dyn_ptr_true=reinterpret_cast<Dynamic<true>*>(&dynamic_buff)
    // do some stuff with dyn_ptr_true
}

Это решение ни в коем случае не идеально, но мне оно подходит. Я призываю всех прочитать тему https://stackoverflow.com/a/57318684/2166857 и следовать назад и четвертый между @Miles_Budnek и @v.oddou. Я, конечно, многому у него научился.

person Lawless    schedule 05.08.2019
comment
К сожалению, в стандарте не указано, что если T&, цель которого может быть доступна как тип T, повторно интерпретируется как U&, и на протяжении всего времени существования преобразованной ссылки доступ к объекту осуществляется исключительно через него, то ссылка должна использоваться для доступа объект как тип U, даже если типы T и U в противном случае были бы несовместимы. У нетупых компиляторов никогда не было причин не поддерживать такую ​​конструкцию, но сопровождающие clang и gcc используют тот факт, что это не требуется, в качестве предлога для того, чтобы не поддерживать ее, и блокируют любые попытки изменить стандарт, чтобы потребовать ее. . - person supercat; 30.11.2020

Единственный безопасный метод каламбура в стандартном C++ — через std::bit_cast. Однако это может включать копирование вместо обработки одного и того же представления памяти как другого типа, если компилятор не может его оптимизировать. Кроме того, в настоящее время std::bit_cast поддерживается только MSVC, хотя в Clang вы можете использовать __builtin_bit_cast

Поскольку вы используете GCC, вы можете использовать атрибут __may_alias__ чтобы сообщить, что псевдоним безопасен

template<int T>
struct Dynamic {};

template<>
struct Dynamic<true>
{
    uint32_t v;
} __attribute__((__may_alias__));

template<>
struct Dynamic<false>
{
    float v;
} __attribute__((__may_alias__));

float f(Dynamic<true>& d_true)
{
    auto& d_false = *reinterpret_cast<Dynamic<false>*>(&d_true);
    return d_false.v;
}

Clang и ICC также поддерживают этот атрибут. See demo on Godbolt , никаких предупреждений не выдается ивен

person phuclv    schedule 18.11.2020