Існує (або, принаймні, було ще в C90) дві модифікації для здійснення цієї невизначеної поведінки. Перший з них полягав у тому, що компілятору було дозволено генерувати додатковий код, який відстежував те, що було в об'єднанні, і генерував сигнал, коли ви зверталися до неправильного члена. На практиці я не думаю, що хтось колись це робив (можливо, CenterLine?). Іншим було відкрито можливості оптимізації, які ми використовуємо. Я використовував компілятори, які відкладали запис до останнього можливого моменту на тій підставі, що це може бути не потрібно (оскільки змінна виходить за межі обсягу, або є подальша запис іншого значення). Логічно, можна було б очікувати, що цю оптимізацію буде вимкнено, коли об’єднання буде видно, але це не було в найдавніших версіях Microsoft C.
Питання покарання типу є складними. Комітет С (ще наприкінці 1980-х) більш-менш дотримувався позиції, згідно з якою для цього слід використовувати зліпки (на C ++, reinterpret_cast), а не профспілки, хоча обидва методи були широко поширені на той час. З тих пір деякі компілятори (наприклад, g ++) дотримуються протилежної точки зору, підтримуючи використання профспілок, але не використання литих. А на практиці не працюють ні ті, ні інші, якщо не відразу стає очевидним, що існує набивання шрифтів. Це може бути мотивацією точки зору g ++. Якщо ви звертаєтесь до члена профспілки, одразу стає очевидним, що може бути набір шрифтів. Але звичайно, враховуючи щось на зразок:
int f(const int* pi, double* pd)
{
int results = *pi;
*pd = 3.14159;
return results;
}
зателефонував із:
union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );
є абсолютно законним згідно з жорсткими правилами стандарту, але не працює з g ++ (і, мабуть, багатьма іншими компіляторами); при компіляції f
, компілятор припускає , що pi
і pd
може не псевдонім, і сортує запис , щоб *pd
і зчитує з *pi
. (Я вважаю, що це ніколи не було наміром гарантувати це. Але нинішня редакція стандарту це гарантує.)
РЕДАГУВАТИ:
Оскільки інші відповіді стверджують, що поведінка насправді визначена (в основному заснована на цитуванні ненормативної примітки, вирваної з контексту):
Правильна відповідь тут - pablo1977: стандарт не робить спроб визначити поведінку, коли задіяно типову програмування. Ймовірна причина цього полягає в тому, що не існує портативної поведінки, яку він міг би визначити. Це не заважає конкретній реалізації визначити її; хоча я не пам'ятаю жодних конкретних обговорень проблеми, я майже впевнений, що метою було те, що реалізації щось визначають (і більшість, якщо не всі, так роблять).
Що стосується використання об'єднання для набору шрифтів: коли комітет C розробляв C90 (наприкінці 1980-х), існував чіткий намір дозволити реалізацію налагодження, що проводило додаткову перевірку (наприклад, використання вказівників жиру для перевірки меж). З дискусій того часу було зрозуміло, що намір полягає у тому, що реалізація налагодження може кешувати інформацію про останнє значення, ініціалізоване в об’єднанні, і захоплювати, якщо ви намагалися отримати доступ до чогось іншого. Це чітко зазначено в §6.7.2.1 / 16: "Значення щонайбільше одного з членів може зберігатися в об'єкті об'єднання в будь-який час". Доступ до значення, якого немає, є невизначеною поведінкою; його можна уподібнити до доступу до неініціалізованої змінної. (Тоді були деякі дискусії щодо того, чи є доступ до іншого члена того самого типу законним чи ні. Однак я не знаю, якою була остаточна резолюція; приблизно після 1990 року я перейшов до C ++.)
Що стосується цитати з C89, то сказати, що поведінка визначається реалізацією: знайти її в розділі 3 (Терміни, визначення та символи) видається дуже дивним. Мені доведеться шукати це в моїй копії C90 вдома; той факт, що він був вилучений у пізніших версіях стандартів, свідчить про те, що комітет вважав його наявність помилкою.
Використання спілок, які підтримує стандарт, є засобом для імітації деривації. Ви можете визначити:
struct NodeBase
{
enum NodeType type;
};
struct InnerNode
{
enum NodeType type;
NodeBase* left;
NodeBase* right;
};
struct ConstantNode
{
enum NodeType type;
double value;
};
union Node
{
struct NodeBase base;
struct InnerNode inner;
struct ConstantNode constant;
};
і легально отримати доступ до base.type, хоча Node було ініціалізовано через inner
. (Той факт, що §6.5.2.3 / 6 починається з "Складається одна спеціальна гарантія ..." і надалі явно допускає це, є дуже вагомим свідченням того, що всі інші випадки мають бути невизначеною поведінкою. І звичайно, є є твердженням, що "невизначена поведінка в іншому випадку позначається в цьому міжнародному стандарті словами" 'невизначена поведінка' 'або пропуском будь-якого явного визначення поведінки "у §4 / 2; з метою аргументувати, що поведінка не визначена , ви повинні показати, де це визначено в стандарті.)
Нарешті, щодо набору шрифтів: усі (або принаймні всі, що я використовував) реалізації певним чином підтримують це. Тоді я склав враження, що намір полягав у тому, щоб кастинг покажчиків був способом, яким його підтримувала реалізація; у стандарті С ++ є навіть (ненормативний) текст, який передбачає, що результати reinterpret_cast
можуть бути "несподіваними" для когось, хто знайомий з базовою архітектурою. На практиці, однак, більшість реалізацій підтримують використання union для набору тексту, за умови, що доступ здійснюється через члена об'єднання. Більшість реалізацій (але не g ++) також підтримують прив'язку покажчиків за умови, що складання вказівника чітко видно компілятору (для деяких невизначених визначень прив'язки покажчика). А "стандартизація" базового обладнання означає, що такі речі:
int
getExponent( double d )
{
return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}
насправді досить портативні. (Звичайно, це не буде працювати на мейнфреймах.) Що не працює, - це речі, подібні до мого першого прикладу, де компілятор невидимий. (Я майже впевнений, що це дефект стандарту. Здається, я пам’ятаю, навіть бачивши АД щодо цього.)