Переосмысление союза с другим союзом

У меня есть стандартный макет, в котором есть целая куча типов:

union Big {
    Hdr h;

    A a;
    B b;
    C c;
    D d;
    E e;
    F f;
};

Каждый из типов A через F является стандартным макетом и имеет в качестве первого члена объект типа Hdr. Hdr определяет, что является активным членом союза, так что это вариант. Теперь я в ситуации, когда я точно знаю (потому что я проверил), что активный член является либо B либо C Фактически я сократил пространство до:

union Little {
    Hdr h;

    B b;
    C c;
};

Итак, следующее четко определенное или неопределенное поведение?

void given_big(Big const& big) {
    switch(big.h.type) {
    case B::type: // fallthrough
    case C::type:
        given_b_or_c(reinterpret_cast<Little const&>(big));
        break;
    // ... other cases here ...
    }
}

void given_b_or_c(Little const& little) {
    if (little.h.type == B::type) {
        use_a_b(little.b);
    } else {
        use_a_c(little.c);
    }
}

Цель Little - эффективно служить в качестве документации, что я уже проверил, что это B или C поэтому в будущем никто не добавляет код, чтобы проверить, что это A или что-то в этом роде.

Является ли тот факт, что я читаю подобъект B как B достаточно, чтобы сделать это хорошо сформированным? Можно ли использовать здесь общее правило исходной последовательности?

Ответы

Ответ 1

Чтобы иметь возможность указывать указатель на A и переинтерпретировать его как указатель на B, они должны быть взаимно конвертируемыми.

Поинтер-конвертируемый - это объекты, а не типы объектов.

В C++ есть объекты в местах. Если у вас есть Big в определенном месте с хотя бы одним существующим членом, в этом же месте есть Hdr из-за взаимной Hdr указателя.

Однако в этом месте нет объекта Little. Если там нет Little объекта, он не может быть конвертируемым указателем с Little объектом, которого нет.

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

Это означает, что вы можете скопировать их представление байтов, и оно работает. На самом деле оптимизаторы, похоже, понимают, что memcpy для локального буфера стека, новое место размещения (с тривиальным конструктором), то memcpy back на самом деле является noop.

template<class T>
T* laundry_pod( void* data ) {
  static_assert( std::is_pod<Data>{}, "POD only" ); // could be relaxed a bit
  char buff[sizeof(T)];
  std::memcpy( buff, data, sizeof(T) );
  T* r = ::new( data ) T;
  std::memcpy( data, buff, sizeof(T) );
  return r;
}

вышеприведенная функция является noop во время выполнения (в оптимизированной сборке), но при этом она преобразует данные, совместимые с T-макетами, в data до фактического T

Итак, если я прав, а Big and Little совместимы с макетами, когда Big является подтипом типов в Little, вы можете сделать это:

Little* inplace_to_little( Big* big ) {
  return laundry_pod<Little>(big);
}
Big* inplace_to_big( Little* big ) {
  return laundry_pod<Big>(big);
}

или же

void given_big(Big& big) { // cannot be const
  switch(big.h.type) {
  case B::type: // fallthrough
  case C::type:
    auto* little = inplace_to_little(&big); // replace Big object with Little inplace
    given_b_or_c(*little); 
    inplace_to_big(little); // revive Big object.  Old references are valid, barring const data or inheritance
    break;
  // ... other cases here ...
  }
}

если Big имеет не-плоские данные (например, ссылки или const данные), то вышеизложенное прерывается ужасно.

Обратите внимание, что laundry_pod не выполняет никакого распределения памяти; он использует новое место размещения, которое создает T в том месте, где data указывают байты data. И хотя похоже, что он делает много вещей (копируя память вокруг), он оптимизируется для noop.


имеет понятие "объект существует". Существование объекта почти не имеет никакого отношения к тому, какие биты или байты написаны на физическом или абстрактном компьютере. В вашем двоичном коде нет инструкции, которая соответствует "теперь объект существует".

Но язык имеет эту концепцию.

Объекты, которые не существуют, не могут взаимодействовать. Если вы это сделаете, стандарт C++ не определяет поведение вашей программы.

Это позволяет оптимизатору делать предположения о том, что делает ваш код и что он не делает, и какие ветки не могут быть достигнуты, и которые могут быть достигнуты. Это позволяет компилятору делать допущения без сглаживания; изменение данных с помощью указателя или ссылки на A не может изменить данные, достигнутые с помощью указателя или ссылки на B, если каким-либо образом оба A и B не находятся в одном и том же месте.

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

Представьте, если given_b_or_c изменяет поле. Ну компилятор мог given_big и given_b_or_c и use_a_b, заметить, что ни один экземпляр Big не был изменен (просто экземпляр Little), и докажите, что поля данных из Big он кэшировал до вызова вашего кода, не могут быть изменены.

Это сохраняет инструкцию по загрузке, и оптимизатор вполне доволен. Но теперь у вас есть код, который гласит:

Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
if (b.foo!=4) exit(-1);

который оптимизирован для

Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
exit(-1);

потому что он может доказать, что b.foo должен быть 7 он был установлен один раз и никогда не был изменен. Доступ через Little не мог изменить Big из-за правил псевдонимов.

Теперь сделайте следующее:

Big b = whatever;
b.foo = 7;
(*laundry_pod<Little>(&b)).foo = 4;
Big& b2 = *laundry_pod<Big>(&b);
if (b2.foo!=4) exit(-1);

и предполагается, что большой там не изменился, потому что есть memcpy и a ::new которые могли бы законным образом изменить состояние данных. Нет строгого нарушения псевдонимов.

Он все еще может следовать за memcpy и устранять его.

Живой пример оптимизации laundry_pod. Обратите внимание, что если он не был оптимизирован, код должен иметь условный и printf. Но поскольку он был, он был оптимизирован в пустую программу.

Ответ 2

Я не могу найти формулировки в n4296 (проект стандарта С++ 14), который сделает это законным. Более того, я даже не могу найти формулировки, которые даны:

union Big2 {
    Hdr h;

    A a;
    B b;
    C c;
    D d;
    E e;
    F f;
};

мы можем reinterpret_cast ссылку на Big в ссылку на Big2 а затем использовать ссылку. (Обратите внимание, что Big и Big2 совместимы с Big2.)

Ответ 3

Это UB бездействием. [Expr.ref]/4.2:

Если E2 - нестатический элемент данных, а тип E1 - " cq1 vq1 X ", а тип E2 - " cq2 vq2 T ", выражение [ E1.E2 ] обозначает именованный член объекта, обозначенного первое выражение.

Во время оценки вызова given_b_or_c в given_big выражение объекта в little.h фактически не обозначает объект Little, а ergo нет такого члена. Поскольку стандарт "опускает какое-либо явное определение поведения" для этого случая, поведение не определено.

Ответ 4

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

И из [basic.compound]/4:

Два объекта a и b являются взаимопереключаемыми с указателем, если:

  • они являются одним и тем же объектом или
  • один представляет собой объект объединения, а другой - нестатический элемент данных этого объекта или
  • один является объектом класса стандартного макета, а второй является первым нестатическим членом данных этого объекта или, если у объекта нет нестатических элементов данных, первый подобъект базового класса этого объекта или
  • существует объект c такой, что a и c являются взаимно обратимыми для указателей, а c и b являются взаимно обратимыми.

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

В этом случае имеем Hdr h; (c) как нестатический элемент данных в обоих объединениях, что должно позволить (из-за второй и последней точки маркера)

Big* (a) -> Hdr* (c) -> Little* (b)