Яке правило суворого псевдоніму?


803

Запитуючи про загальну невизначену поведінку у С , люди іноді посилаються на суворе правило злучення.
Про що вони говорять?


12
@Ben Voigt: Правила згладжування різні для c ++ та c. Чому це питання позначене cі c++faq.
MikeMB

6
@MikeMB: Якщо ви перевірите історію, ви побачите, що я зберігав теги таким, яким вони були спочатку, незважаючи на спробу деяких експертів змінити питання з-під існуючих відповідей. Крім того, залежність від мови та залежність від версій є дуже важливою частиною відповіді на питання "Що є суворим правильним принципом?" і знати відмінності важливо для команд, що мігрують код між C і C ++, або писати макроси для використання в обох.
Бен Войгт

6
@Ben Voigt: Насправді - наскільки я можу сказати - більшість відповідей стосуються лише c, а не c ++; також формулювання питання вказує на фокус на C-правилах (або ОП просто не усвідомлював, що є різниця ). Здебільшого правила та загальна ідея звичайно однакові, але особливо, коли це стосується спілок, відповіді не стосуються c ++. Я трохи переживаю, що деякі програмісти c ++ шукатимуть суворого правила псевдоніму і просто припускатимуть, що все, що тут сказано, стосується і c ++.
MikeMB

З іншого боку, я погоджуюся, що змінити питання проблематично після того, як було розміщено багато хороших відповідей, і питання все одно є другорядним.
MikeMB

1
@MikeMB: Я думаю, ви побачите, що фокус C на прийнятій відповіді, що робить його неправильним для C ++, був відредагований стороною стороною. Мабуть, цю частину слід переглянути ще раз.
Бен Войгт

Відповіді:


562

Типова ситуація, коли ви стикаєтесь із суворими проблемами згладжування, - це накладання структури (наприклад, msg пристрою / мережі) на буфер розміру слова вашої системи (наприклад, вказівник на uint32_ts або uint16_ts). Якщо ви накладете структуру на такий буфер або буфер на таку структуру за допомогою введення покажчика, ви можете легко порушити суворі правила псевдоніму.

Тож у такому налаштуванні, якщо я хочу надіслати повідомлення на щось, мені доведеться мати два несумісні вказівники, що вказують на один і той же фрагмент пам'яті. Я можу тоді наївно кодувати щось подібне (у системі з sizeof(int) == 2):

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

Правило суворого псевдоніму робить цю установку незаконною: перенаправлення покажчика, який псевдонімує об'єкт, який не є сумісним типом, або одного з інших типів, дозволеного пунктом 7 C 2011 6.5, пункт 1 1 1, є невизначеною поведінкою. На жаль, ви все ще можете кодувати таким чином, можливо, отримайте деякі попередження , нехай він складеться добре, лише щоб мати дивну несподівану поведінку під час запуску коду.

(GCC виглядає дещо непослідовною у своїй здатності давати потворні попередження, іноді попереджаючи нас доброзичливо, а іноді ні.)

Щоб зрозуміти, чому така поведінка не визначена, ми повинні подумати про те, що суворе правило суворого псевдоніму купує компілятор. В основному, з цим правилом, не потрібно думати про вставлення інструкцій для оновлення вмісту buffкожного запуску циклу. Натомість, під час оптимізації, з деякими набридливо невимушеними припущеннями про псевдонім, він може опускати ці вказівки, завантажувати buff[0]і buff[1] в регістри процесора один раз до запуску циклу та пришвидшити тіло циклу. Перед тим, як було введено суворе псевдонім, компілятору довелося жити в стані параної, який вміст buffбудь-кого міг міняти в будь-який час. Таким чином, щоб отримати додаткові переваги в продуктивності та припускаючи, що більшість людей не набирають покажчиків, було введено суворе правило псевдоніму.

Майте на увазі, якщо ви вважаєте, що приклад надуманий, це може навіть статися, якщо ви передаєте буфер іншій функції, яка виконує надсилання для вас, якщо натомість у вас є.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

І переписав наш раніше цикл, щоб скористатися цією зручною функцією

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Компілятор може або не може бути або досить розумним, щоб спробувати вбудувати SendMessage, і він може, а може і не вирішити завантажувати або не завантажувати баф знову. Якщо SendMessageє частиною іншого API, який складається окремо, він, ймовірно, має інструкції щодо завантаження вмісту буфа. Знову ж таки, можливо, ви перебуваєте в C ++, і це лише деяка шаблонна реалізація заголовка, яку компілятор вважає, що вона може вбудовуватися. А може, це просто щось, що ви написали у вашому файлі .c для власної зручності. У будь-якому випадку невизначена поведінка може все-таки настати. Навіть коли ми знаємо, що відбувається під кришкою, це все ж є порушенням правила, тому жодна чітко визначена поведінка не гарантована. Тож просто введення функції, яка займає буфер з обмеженим словом, не обов'язково допомагає.

То як мені це обійти?

  • Використовуйте союз. Більшість компіляторів підтримують це, не нарікаючи на суворий псевдонім. Це дозволено в C99 і явно дозволено в C11.

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
  • Ви можете вимкнути суворий псевдонім у вашому компіляторі ( f [no-] строгий псевдонім у gcc))

  • Ви можете використовувати char*для згладжування замість слова вашої системи. Правила допускають виняток для char*(включаючи signed charта unsigned char). Завжди вважається, що char*псевдоніми інших типів. Однак це не буде працювати інакше: немає припущення, що ваша структура псевдонімує буфер символів.

Початківці остерігайтесь

Це лише одне потенційне мінне поле при накладанні двох типів один на одного. Ви також повинні дізнатися про витривалість , вирівнювання слів та про те, як правильно вирішувати проблеми вирівнювання через структури упаковки .

Зноска

1 Типи, до яких C 2011 6.5 7 дозволяє дозволити lvalue, це:

  • тип, сумісний з ефективним типом об'єкта,
  • кваліфікована версія типу, сумісна з ефективним типом об'єкта,
  • тип, який є типовим або непідписаним типом, що відповідає ефективному типу об'єкта,
  • тип, який є типовим або непідписаним типом, що відповідає кваліфікованій версії ефективного типу об'єкта,
  • сукупний або союзний тип, який включає один із вищезгаданих типів серед своїх членів (включаючи, рекурсивно, член субагрегату або об'єднання, що міститься), або
  • тип символів

16
Я приходжу після битви, здається .. може unsigned char*бути використаний далеко char*замість цього? Я, як правило, використовую, unsigned charа не charяк базовий тип, byteтому що мої байти не підписані, і я не хочу, щоб дивацтво підписаної поведінки (особливо Wrt переповнювалося)
Матьє М.

30
@Matthieu: Підписання не має значення для правил псевдоніму, тому використання unsigned char *нормально.
Томас Едінг

22
Чи не визначена поведінка, яку слід читати від члена профспілки, відрізняється від останнього, про який писали?
Р. Мартіньо Фернандес

23
Боллоки, ця відповідь повністю зворотна . Приклад, який він показує як незаконний, насправді є законним, а приклад, який він показує як законний, насправді є незаконним.
Р. Мартіньо Фернандес

7
Ваші uint32_t* buff = malloc(sizeof(Msg));та наступні unsigned int asBuffer[sizeof(Msg)];буферні декларації об'єднання матимуть різний розмір, і жодне з них не є правильним. mallocВиклик покладається на вирівнюванні 4 байта під капотом (не робити) , а об'єднання буде в 4 рази більше , ніж це повинно бути ... Я розумію , що це для ясності , але це помилка мені ні-the менше ...
безчувальна

233

Найкраще пояснення, яке я знайшов, - Майк Актон, Розуміння суворого згладжування . Він трохи зосереджений на розробці PS3, але це в основному лише GCC.

Зі статті:

"Строгий псевдонім - це припущення, зроблене компілятором C (або C ++), що перенаправлення покажчиків на об'єкти різних типів ніколи не буде посилатися на одне і те ж місце пам'яті (тобто псевдонім один одного.)"

Отже, якщо у вас є int*вказівка ​​на деяку пам'ять, що містить, intа потім ви вказуєте a float*на цю пам'ять і використовуєте її як floatви порушите правило. Якщо ваш код цього не поважає, оптимізатор компілятора, швидше за все, порушить ваш код.

Виняток із правила - це a char*, якому дозволено вказувати на будь-який тип.


6
То який канонічний спосіб легально використовувати ту саму пам'ять зі змінними двох різних типів? чи всі просто копіюють?
jiggunjer

4
Сторінка Майка Ектона недосконала. Принаймні, частина "Лиття через союз (2)" є прямо неправильною; код, на який він стверджує, законний, немає.
davmac

11
@davmac: Автори C89 ніколи не думали, що це змусить програмістів стрибати через обручі. Я вважаю дуже химерним уявлення про те, що правило, яке існує з єдиною метою оптимізації, слід тлумачити так, щоб вимагати від програмістів запису коду, який надмірно копіює дані, сподіваючись, що оптимізатор видалить зайвий код.
суперкарт

1
@curiousguy: "Не можна мати спілок"? По-перше, первісна / основна мета спілок взагалі жодним чином не пов'язана з псевдонімом. По-друге, сучасна мовна специфікація чітко дозволяє використовувати союзи для згладжування. Компілятор зобов'язаний помітити, що використовується союз, і розглянути ситуацію - особливим способом.
ANT

5
@curiousguy: Неправдиво. По-перше, первісна концептуальна ідея спілок полягала в тому, що в даний момент в даному об'єднанні об'єкта "активний" лише один об'єкт-член, а інших просто не існує. Отже, "різних об'єктів за однією адресою" немає, як ви, здається, вірите. По-друге, згладжування порушень, про які всі говорять, - це доступ до одного об’єкта як іншого об’єкта, а не просто наявність двох об’єктів з однаковою адресою. Поки немає доступу до типового набору , немає жодної проблеми. То була початкова ідея. Пізніше було дозволено типове покарання через спілки.
ANT

133

Це суворе правило псевдоніму, яке знайдено в розділі 3.10 стандарту C ++ 03 (інші відповіді дають хороші пояснення, але жодна не передбачає самого правила):

Якщо програма намагається отримати доступ до збереженого значення об'єкта через значення, яке відрізняється від одного з наступних типів, поведінка не визначена:

  • динамічний тип об'єкта,
  • версія, що відповідає кваліфікації динамічного типу об'єкта,
  • тип, який є типовим або непідписаним типом, що відповідає динамічному типу об'єкта,
  • тип, який є типовим або непідписаним типом, що відповідає версії, що відповідає кваліфікації cv, динамічного типу об'єкта,
  • сукупний або союзний тип, який включає один із вищезазначених типів серед своїх членів (включаючи, рекурсивно, член субагрегату або об'єднання, що міститься),
  • тип, який є типовим базовим класом (можливо, cv) динамічного типу об'єкта,
  • а charабо unsigned charтип.

Формулювання C ++ 11 та C ++ 14 (наголошено на змінах):

Якщо програма намагається отримати доступ до збереженого значення об'єкта через glvalue іншого, ніж одного з наступних типів, поведінка не визначена:

  • динамічний тип об'єкта,
  • версія, що відповідає кваліфікації динамічного типу об'єкта,
  • тип, подібний (як визначено в 4.4) динамічному типу об'єкта,
  • тип, який є типовим або непідписаним типом, що відповідає динамічному типу об'єкта,
  • тип, який є типовим або непідписаним типом, що відповідає версії, що відповідає кваліфікації cv, динамічного типу об'єкта,
  • сукупний або об'єднаний тип, який включає один із вищезазначених типів серед його елементів або нестатичних членів даних (включаючи, рекурсивно, елемент або нестатичний член даних субагрегату або міститься в об'єднанні),
  • тип, який є типовим базовим класом (можливо, cv) динамічного типу об'єкта,
  • а charабо unsigned charтип.

Дві зміни були невеликими: glvalue замість lvalue та уточнення сукупного / об'єднаного випадку.

Третя зміна дає більш надійну гарантію (послаблює чітке правило псевдоніму): Нова концепція подібних типів , які тепер безпечні для псевдоніму.


Також формулювання C (C99; ISO / IEC 9899: 1999 6.5 / 7; точне ж формулювання використовується в ISO / IEC 9899: 2011, § 6.5 ¶7):

Об'єкт має збережене значення, доступ до якого має лише вираження значення, яке має один із наступних типів 73) або 88) :

  • тип, сумісний з ефективним типом об'єкта,
  • кваліфікована версія типу, сумісна з ефективним типом об'єкта,
  • тип, який є типовим або непідписаним типом, що відповідає ефективному типу об'єкта,
  • тип, який є типовим або неподписаним типом, що відповідає кваліфікованій версії ефективного типу об'єкта,
  • сукупний або союзний тип, який включає один із вищезгаданих типів серед своїх членів (включаючи, рекурсивно, член субагрегату або об'єднання, що міститься), або
  • тип символів

73) або 88) Метою цього переліку є конкретизація тих обставин, за яких об'єкт може бути, а може і не бути відчуженим.


7
Бен, як сюди часто звертаються люди, я дозволив собі також додати посилання на стандарт С, заради повноти.
Кос

1
Подивіться на роз'яснення C89 cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf в розділі 3.3, де йдеться про це.
phorgan1

2
Якщо ви маєте значення значення типу структури, приймає адресу члена і передає його функції, яка використовує її як вказівник на тип члена, чи буде це розцінено як доступ до об'єкта типу члена (законний), або об’єкт типу структури (заборонено)? Багато коду передбачає , що це законно структурам доступу таким чином, і я думаю, що багато людей буде пронизливий крик на правило , яке розуміється як забороняє такі дії, але неясно , які саме правила. Далі, профспілки та структури трактуються однаково, але розумні правила для кожного повинні бути різними.
supercat

2
@supercat: Як формулюється правило для структур, фактичний доступ завжди до примітивного типу. Тоді доступ через посилання на примітивний тип є законним, оскільки типи відповідають, а доступ через посилання на тип структури, що містить тип, є законним, оскільки це спеціально дозволено.
Бен Войгт

2
@BenVoigt: Я не думаю, що загальна початкова послідовність працює, якщо доступ не здійснюється через об'єднання. Дивіться goo.gl/HGOyoK, щоб побачити, що робить gcc. Якщо доступ до значення значення Union через lvalue типу члена (не використовуючи оператора доступу union-member) був легальним, тоді wow(&u->s1,&u->s2)він повинен бути легальним навіть тоді, коли вказівник використовується для зміни u, і це заперечує більшість оптимізацій, що Правило псевдоніму було розроблено для полегшення.
supercat

80

Примітка

Це уривок з мого "Що таке правило суворого відчуження і чому нас хвилює?" докладно описувати.

Що таке суворий псевдонім?

У псевдонімі C і C ++ пов'язане з типом виразів, яким ми можемо отримувати доступ до збережених значень. І в C, і в C ++ стандарт вказує, які типи виразів дозволені для псевдонімів, які типи. Компілятор і оптимізатор дозволяють припустити, що ми суворо дотримуємось правил псевдонімування, звідси і термін суворе правило псевдоніму . Якщо ми намагаємося отримати доступ до значення за допомогою типу не дозволеного, це класифікується як невизначена поведінка ( UB ). Після того, як ми не визначимо поведінку, всі ставки будуть вимкнені, результати нашої програми вже не є надійними.

На жаль, із суворими порушеннями дозволу ми часто отримуємо очікувані результати, залишаючи можливість майбутній варіант компілятора з новою оптимізацією зламати код, який ми вважали дійсним. Це небажано, і важливою є мета зрозуміти суворі правила дозволу та як уникнути їх порушення.

Щоб дізнатися більше про те, чому нас хвилює, ми обговоримо проблеми, які виникають при порушенні суворих правил згладжування, введіть покарання, оскільки загальні методи, що застосовуються при типовому покаранні, часто порушують суворі правила псевдоніму та як правильно вводити каламбур.

Попередні приклади

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

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

У нас є int *, що вказує на пам'ять, яку займає int, і це дійсний псевдонім. Оптимізатор повинен припустити, що призначення через ip можуть оновити значення, яке займає x .

Наступний приклад показує згладжування, яке призводить до невизначеної поведінки ( живий приклад ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

У функції foo ми беремо int * і float * , у цьому прикладі ми викликаємо foo і встановлюємо обидва параметри, щоб вказувати на те саме місце пам'яті, яке в цьому прикладі містить int . Зауважте, reinterpret_cast вказує компілятору трактувати вираз так, як ніби він має тип, визначений параметром шаблону. У цьому випадку ми кажемо йому, що він має відношення до виразу & x так, як якщо б він мав тип float * . Ми можемо наївно очікувати, що результат другої кути буде 0, але з увімкненою оптимізацією за допомогою -O2 і gcc, і кланг дають такий результат:

0
1

Що може бути не очікуваним, але цілком справедливо, оскільки ми посилалися на невизначену поведінку. Поплавок не може законним чином псевдонім з Int об'єкта. Тому оптимізатор може припустити, що константа 1, що зберігається при перенаправлення i, буде значенням повернення, оскільки зберігання через f не могло би вплинути на об'єкт int . Підключення коду до Провідника компілятора показує, що це саме те, що відбувається ( живий приклад ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Оптимізатор, що використовує типовий аналіз псевдонімів (TBAA), передбачає, що 1 буде повернуто і безпосередньо переміщує постійне значення в регістр eax, який несе повернене значення. TBAA використовує мовні правила щодо того, які типи дозволені для псевдоніму, щоб оптимізувати навантаження та магазини. У цьому випадку TBAA знає, що поплавок не може псевдонімувати і int, і оптимізує подальше навантаження i .

Тепер до Правила

Що саме в стандарті йдеться про те, що нам дозволено і не дозволяють робити це? Стандартна мова не є простою, тому для кожного елемента я спробую навести приклади коду, що демонструє значення.

Що говорить стандарт C11?

Стандарт C11 в розділі 6.5, параграф 7, виражає наступне :

Об'єкт має збережене значення, доступ до якого має лише вираз lvalue, який має один із таких типів: 88) - тип, сумісний з ефективним типом об'єкта,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- кваліфікована версія типу, сумісна з ефективним типом об'єкта,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- тип, який є типовим або неподписаним типом, що відповідає ефективному типу об'єкта,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc / clang має розширення, а також дозволяє призначати неподписаний int * до int *, хоча вони не сумісні типи.

- тип, який є типовим або непідписаним типом, що відповідає кваліфікованій версії ефективного типу об'єкта,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- сукупний або союзний тип, який включає один із вищезгаданих типів серед своїх членів (включаючи, рекурсивно, член субагрегату або об'єднання, що міститься), або

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- тип символів.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Що говориться у проекті стандарту C ++ 17

Проект стандарту C ++ 17 у параграфі 11 розділу [basic.lval] говорить:

Якщо програма намагається отримати доступ до збереженого значення об'єкта через glvalue іншого, ніж одного з наступних типів, поведінка не визначена: 63 (11.1) - динамічний тип об'єкта,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - версія для динамічного типу об'єкта, кваліфікована cv,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - тип, подібний (визначений у 7.5) динамічному типу об'єкта,

(11.4) - тип, який є підписаним або непідписаним типом, що відповідає динамічному типу об'єкта,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - тип, який є типовим або непідписаним типом, що відповідає версії cv-кваліфікації динамічного типу об'єкта,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - сукупний або об'єднаний тип, який включає один із вищезазначених типів серед його елементів або нестатичних членів даних (включаючи, рекурсивно, елемент або нестатичний член даних субагрегата або міститься в об'єднанні),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - тип, який є типовим базовим класом динамічного типу об'єкта (можливо, що відповідає кваліфікації cv),

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - тип char, непідписаний char або std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Варто зазначити, що підписані знаки не включені до списку вище, це помітна відмінність від C, яка говорить про тип символу .

Що таке тип набивання

Ми дійшли до цього моменту і нам може бути цікаво, чому ми хотіли б псевдонім? Відповідь, як правило, вводить каламбур , часто використовувані методи порушують суворі правила дозволу.

Іноді ми хочемо обійти систему типів і інтерпретувати об'єкт як інший тип. Це називається типом покарання , щоб повторно інтерпретувати сегмент пам'яті як інший тип. Введення покарань корисно для завдань, які хочуть отримати доступ до базового зображення об’єкта для перегляду, транспортування чи маніпулювання. Типові області, за якими ми вважаємо, що застосовується тип покарання, - це компілятори, серіалізація, мережевий код тощо ...

Традиційно це досягається шляхом взяття адреси об’єкта, накидання його на покажчик типу, який ми хочемо переосмислити як, а потім отримати доступ до значення, або іншими словами шляхом псевдоніму. Наприклад:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Як ми бачили раніше, це неправдивий псевдонім, тому ми посилаємось на невизначену поведінку. Але традиційно компілятори не скористалися суворими правилами дозволу, і цей тип коду, як правило, працював, розробники, на жаль, звикли робити такі дії. Поширений альтернативний метод для покарання типів - це об'єднання, що є дійсним в C, але невизначена поведінка в C ++ ( див. Прямий приклад ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

Це не є дійсним для C ++, і деякі вважають, що мета профспілок полягає лише у впровадженні варіантів типів, і вважають, що використання об'єднань для покарання типу є зловживанням.

Як правильно вводити Pun?

Стандартний метод для типового покарання як для C, так і для C ++ - це memcpy . Це може здатися дещо важким, але оптимізатор повинен розпізнати використання memcpy для набору типів та оптимізувати його та створити регістр для реєстрації переміщення. Наприклад, якщо ми знаємо, що int64_t має той самий розмір, що і подвійний :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

ми можемо використовувати memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

При достатньому рівні оптимізації будь-який гідний сучасний компілятор генерує ідентичний код попередньо згаданому методу reinterpret_cast або об'єднанню для типового нанесення . Розглядаючи згенерований код, ми бачимо, що він використовує просто зареєструвати mov ( живий приклад компілятора Explorer ).

C ++ 20 та біт_каст

У C ++ 20 ми можемо отримати bit_cast ( реалізація доступна за посиланням із пропозиції ), що дає простий та безпечний спосіб набрати шрифт, а також бути корисним у контексті constexpr.

Нижче наводиться приклад того, як використовувати bit_cast для введення пункту неподписаного int для плавання , ( див. Це в прямому ефірі ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

У випадку, коли типи « До» та « Від» не мають однакового розміру, це вимагає від нас використання проміжної структури15. Ми будемо використовувати на структуру , яка містить SizeOf (беззнаковое ціле) масив символів ( передбачає 4 байта без знака Int ) , щоб бути від типу і без знака Int , що і щоб ввести.:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Прикро, що нам потрібен цей проміжний тип, але це обмеження в потоці bit_cast .

Викриття суворих порушень

У нас не так багато хороших інструментів для лову суворого псевдоніму в C ++, інструменти, які ми маємо, виявлять деякі випадки суворих порушень збитку та деякі випадки нерівних навантажень і магазинів.

gcc, використовуючи прапор -fstrict-aliasing та -Wstrict-aliasing, може зафіксувати деякі випадки, хоча не без помилкових позитивів / негативів. Наприклад, наступні випадки генерують попередження в gcc ( дивіться його в прямому ефірі ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

хоча цей додатковий випадок не буде ( див. це наживо ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Хоча кланг дозволяє ці прапори, він, мабуть, насправді не реалізує попередження.

Ще один доступний нам інструмент - ASan, який може ловити нерівні вантажі та магазини. Незважаючи на те, що вони не є прямими чіткими порушеннями спостереження, вони є загальним результатом суворих порушень. Наприклад, наступні випадки створюватимуть помилки виконання під час побудови з клангом за допомогою -fsanitize = address

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Останній інструмент, який я порекомендую, - це специфічний для C ++ і не суворо інструмент, а практика кодування, не допускати кастингу в стилі C. І gcc, і clang будуть виробляти діагностику для анкет у стилі C, використовуючи -Wold-style-cast . Це змусить будь-які невизначені типи каламбурів використовувати reinterpret_cast, загалом reinterpret_cast повинен бути прапором для більш детального перегляду коду. Так само простіше здійснити пошук у вашій кодовій базі для reinterpret_cast для проведення аудиту.

Для C у нас усі інструменти вже охоплені, а також у нас є tis-interpreter, статичний аналізатор, який вичерпно аналізує програму для великого підмножини мови С. З огляду на версії C попереднього прикладу, коли використання -fstrict-aliasing пропускає один випадок ( див. Його в прямому ефірі )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-інтерпетер здатний спіймати всіх трьох, наступний приклад запускає tis-kernal як tis-interpreter (вихід редагується для стислості):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Нарешті, є TySan, який наразі розробляється. Цей дезінфікуючий додає інформацію про перевірку типів у сегменті тіньової пам'яті та перевіряє доступ, щоб перевірити, чи порушують вони правила дозволу. Інструмент потенційно повинен бути в змозі виявити всі спокійні порушення, але може мати великі накладні витрати.


Коментарі не для розширеного обговорення; ця розмова була переміщена до чату .
Bhargav Rao

3
Якби я міг, +10, добре написаний і пояснений, також з обох сторін, автори-компілятори та програмісти ... єдина критика: Було б добре мати зустрічні приклади вище, щоб побачити, що заборонено стандартом, його не очевидно різновид :-)
Габріель

2
Дуже гарна відповідь. Я тільки шкодую, що початкові приклади наведені в C ++, що ускладнює наслідування таким людям, як я, які знають або дбають про C і не мають уявлення, що reinterpret_castможе робити або що coutможе означати. (Добре згадати C ++, але оригінальне запитання стосувалося C та IIUC, ці приклади можна було б так само достовірно записати в C.)
Гро-Цен

Що стосується типового покарання: тож якщо я записую масив якогось типу X у файл, то читаю з цього файлу цей масив у пам’яті, вказує на void *, тоді я кидаю цей покажчик на реальний тип даних, щоб використовувати його - ось невизначена поведінка?
Михайло IV

44

Строгий псевдонім не стосується лише покажчиків, він також впливає на посилання, я написав документ про нього для вікі-розробника, і він був настільки добре прийнятий, що перетворив його на сторінку свого консалтингового веб-сайту. Це повністю пояснює, що це таке, чому це так бентежить людей і що з цим робити. Сувора чиста книга . Зокрема, це пояснює, чому спілки є ризикованою поведінкою для C ++, і чому використання memcpy - це єдиний переносний виправлення як на C, так і на C ++. Сподіваюся, це корисно.


3
" Суворий псевдонім не стосується лише покажчиків, він також впливає на посилання " Насправді він посилається на значення . " використання memcpy є єдиним портативним виправленням " Слухай!
curiousguy

5
Хороший папір. Я вважаю: (1) ця псевдонім-проблема є надмірною реакцією на погане програмування - намагається захистити поганого програміста від його / її шкідливих звичок. Якщо у програміста є звичні звички, то цей псевдонім - лише неприємність, і перевірки можна сміливо вимкнути. (2) Оптимізацію на компіляторі слід проводити лише у відомих випадках, і якщо сумніви суворо дотримуються вихідного коду; змушуючи програміста писати код для задоволення ідіосинкразії компілятора, просто кажучи, неправильно. Ще гірше зробити його частиною стандарту.
slashmais

4
@slashmais (1) " - це надмірна реакція на погане програмування " Дурниці. Це відмова від шкідливих звичок. Ти робиш це? Ви платите ціну: жодної гарантії для вас! (2) Добре відомі випадки? Котрий? Правило суворого псевдоніму повинно бути "добре відомим"!
цікавогут

5
@curiousguy: Очистивши кілька точок плутанини, зрозуміло, що мова С із правилами псевдонімування робить неможливим для програм реалізацію типово-агностичних пулів пам'яті. Деякі види програм можуть виконуватись з malloc / free, але для інших потрібна логіка управління пам’яттю, що краще підходить до завдань, що стоять перед вами. Мені цікаво, чому обґрунтування C89 використовує такий кричущий приклад причини згладжування правила, оскільки з їх прикладу здається, що правило не складе великих труднощів у виконанні будь-яких розумних завдань.
supercat

5
@curiousguy, більшість наборів компіляторів є, зокрема, -fstrict-aliasing як за замовчуванням на -O3, і цей прихований контракт змушений використовувати користувачів, які ніколи не чули про TBAA і писали код, як, наприклад, як системний програміст. Я не хочу звучати непристойно для системних програмістів, але такий тип оптимізації повинен залишатися поза опцією за замовчуванням -O3 і повинен бути оптимізацією для тих, хто знає, що таке TBAA. Не весело дивитися на "помилку" компілятора, яка виявляється, що код користувача порушує TBAA, особливо відстежуючи порушення рівня джерела в коді користувача.
kchoi

34

Як додаток до того, що Дуг Т. вже писав, ось простий тестовий випадок, який, ймовірно, викликає це з gcc:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Компілювати з gcc -O2 -o check check.c. Зазвичай (у більшості версій, які я пробував), це виводить "сувору проблему згладжування", оскільки компілятор припускає, що "h" не може бути такою ж адресою, як "k" у функції "check". Через це компілятор оптимізує if (*h == 5)подальше і завжди викликає printf.

Для тих, кого тут цікавить, є код асемблера x64, створений gcc 4.6.3, який працює на ubuntu 12.04.2 для x64:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Тож умова if повністю відійшов від асемблерського коду.


якщо ви додаєте другий короткий * j, щоб перевірити () і використовувати його (* j = 7), то оптимізація не проходить, оскільки ggc не відбувається, якщо h і j не є фактично вказують на одне значення. так, оптимізація справді розумна.
Філіпп Ларді

2
Щоб зробити речі більш веселими, використовуйте вказівники на типи, які не сумісні, але мають однаковий розмір і представлення (у деяких системах, що стосується, наприклад, * long long*та int64_t*). Можна очікувати, що розумний компілятор повинен визнати, що a long long*і int64_t*може отримати доступ до одного і того ж сховища, якщо вони зберігаються однаково, але таке лікування вже не модне.
supercat

Grr ... x64 - це конвенція Microsoft. Замість цього використовуйте amd64 або x86_64.
СС Енн

Grr ... x64 - це конвенція Microsoft. Замість цього використовуйте amd64 або x86_64.
СС Енн

17

Введення покарань за допомогою покажчиків покажчиків (на відміну від використання об'єднання) є головним прикладом порушення строгого згладжування.


1
Дивіться тут мою відповідь щодо відповідних цитат, особливо виносок, але типове покарання через союзи завжди було дозволено в С, хоча спочатку це було погано сформульовано. Ви хочете уточнити свою відповідь.
Шафік Ягмур

@ShafikYaghmour: C89 чітко дозволив виконавцям вибирати випадки, в яких вони чи не бажали б розпізнати покарання типу через союзи. Реалізація може, наприклад, вказати, що для запису в один тип, за яким слід читати інший, слід визнати типом покарання, якщо програміст зробив будь-яке з наступних дій між записом і читанням : (1) оцінити значення, що містить тип союзу [прийняття адреси члена буде кваліфікованим, якщо це буде зроблено в потрібній точці послідовності]; (2) перетворити вказівник на один тип у вказівник на інший та отримати доступ через цей ptr.
supercat

@ShafikYaghmour: Реалізація також може вказати, наприклад, що тип покарання між цілими і значеннями з плаваючою комою буде надійно працювати лише тоді, коли код виконає fpsync()директиву між записом як fp та читанням як int або навпаки [щодо реалізацій з окремими цілими і FPU конвеєрами та кешами , така директива може бути дорогою, але не такою дорогою, як змушення компілятора виконувати таку синхронізацію при кожному доступі до об'єднання]. Або впровадження могло б вказати, що отримане значення ніколи не буде корисним, за винятком обставин, що використовують загальні початкові послідовності.
supercat

@ShafikYaghmour: В умовах C89 реалізація може забороняти більшість форм покарання типу, в тому числі через союзи, але еквівалентність покажчиків до об'єднань і покажчиків для їх членів передбачає, що покарання типу дозволяється в реалізаціях, які прямо не забороняли це.
supercat

17

Відповідно до обґрунтування C89, автори Стандарту не хотіли вимагати, щоб компілятори надавали код типу:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

Потрібно вимагати, щоб перезавантажити значення xміж оператором присвоєння та поверненням таким чином, щоб забезпечити можливість, на яку pможе вказуватись x, і присвоєння *pможе, відповідно, змінити значення x. Думка про те, що компілятор повинен мати право припускати, що в таких ситуаціях, як зазначено вище, не буде згладжуватися, було суперечливим.

На жаль, автори C89 написали своє правило таким чином, що, якщо читати буквально, змусили б навіть таку функцію викликати Невизначене поведінку:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

тому що він використовує значення "lvalue" intдля доступу до об'єктів типу struct S, і intне входить до типів, які можуть використовуватися для доступу до struct S. Оскільки було б абсурдно ставитись до будь-якого використання членів структур та об'єднань, що не є типовими символами, як Невизначена поведінка, майже кожен визнає, що існують принаймні деякі обставини, коли для доступу до об'єктів іншого типу може бути використане значення двох типів. . На жаль, Комітет зі стандартів С не зміг визначити, що це за обставини.

Значна частина проблем - це результат звіту про дефекти № 028, який запитав про поведінку програми на зразок:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

У звіті про дефекти №28 зазначено, що програма посилається на не визначену поведінку, оскільки дія написання члена профспілки типу "подвійний" та читання одного з типів "int" викликає поведінку, визначену реалізацією. Таке міркування є безглуздим, але є основою для правил Ефективного типу, які непотрібно ускладнюють мову, не роблячи нічого для вирішення початкової проблеми.

Найкращий спосіб вирішити первісну проблему, мабуть, буде поводити виноску про мету правила як би нормотворну та робить це правило неприйнятним, за винятком випадків, що насправді пов'язані з конфліктуючим доступом із використанням псевдонімів. Дано щось на кшталт:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

Немає конфлікту всередині, inc_intоскільки всі доступ до пам’яті, до якого здійснюється доступ *p, робляться з типом lvalue int, і немає конфлікту в testтому, що pце видимо походить від a struct S, і при наступному sвикористанні, всі доступ до цього сховища, який коли-небудь буде зроблений наскрізь pвже відбулося.

Якщо код було трохи змінено ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Тут між конфліктом pі доступом до s.xцього позначеного рядка виникає, тому що в цьому місці виконання існує ще одне посилання, яке буде використовуватися для доступу до тієї ж пам’яті .

Якби звіт про дефекти 028 заявив, що оригінальний приклад посилався на UB через перекриття між створенням та використанням двох покажчиків, це зробило б речі більш зрозумілими без необхідності додавати "ефективні типи" чи іншу таку складність.


Добре кажучи, було б цікаво прочитати таку пропозицію, яка була б більш-менш "тим, що міг би зробити комітет з питань стандартизації", який досяг їхніх цілей, не вводячи стільки складних умов.
jrh

1
@jrh: Я думаю, це було б досить просто. Визнайте, що 1. Для випробовування, яке відбудеться під час певного виконання функції або циклу, під час цього виконання необхідно використовувати два різних вказівника або значення , щоб звертатися до одного і того ж сховища на конфліктуючому фашоні; 2. Визнайте, що у контекстах, коли один вказівник або значення є свіжо похідним від іншого, доступ до другого є доступом до першого; 3. Визнайте, що правило не має на меті застосовуватися у випадках, які насправді не мають псевдоніму.
supercat

1
Точні обставини, коли компілятор розпізнає щойно отримане значення, може бути проблемою якості виконання, але будь-який віддалено пристойний компілятор повинен бути в змозі розпізнати форми, які gcc і clang навмисно ігнорують.
supercat

11

Прочитавши багато відповідей, я відчуваю потребу щось додати:

Суворий псевдонім (який я трохи опишу) важливий, оскільки :

  1. Доступ до пам’яті може бути дорогим (функціональним), тому дані маніпулюються в регістрах процесора, перш ніж вони записуються у фізичну пам'ять.

  2. Якщо дані в двох різних процесорах регістрів будуть записуватися в один і той же простір пам'яті, ми не можемо передбачити, які дані "виживуть", коли ми будемо кодувати в C.

    У зборі, де ми кодуємо завантаження та вивантаження регістрів процесорів вручну, ми будемо знати, які дані залишаються недоторканими. Але С (на щастя) абстрагує цю деталь далеко.

Оскільки два покажчики можуть вказувати на одне місце в пам'яті, це може призвести до складного коду, який обробляє можливі зіткнення .

Цей додатковий код повільний і шкодить продуктивності, оскільки він виконує додаткові операції з читання / запису пам'яті, які є повільнішими та (можливо) непотрібними.

Суворе правило згладжування дозволяє уникнути надлишкового коду машини в тих випадках , в яких він повинен бути з упевненістю припустити , що два покажчика не вказують на той же блок пам'яті (дивись також restrictключове слово).

У строгому згладжуванні можна стверджувати, що вказівники на різні типи вказують на різні місця в пам'яті.

Якщо компілятор помітить, що два вказівники вказують на різні типи (наприклад, a int *і a float *), він припустить, що адреса пам'яті відрізняється, і вона не захистить від зіткнень адреси пам'яті, що призведе до більш швидкого машинного коду.

Наприклад :

Дозволяє виконувати наступну функцію:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Для того, щоб обробляти випадок, коли a == b(обидва вказівки вказують на одну і ту ж пам’ять), нам потрібно замовити і протестувати спосіб завантаження даних з пам'яті в регістри процесора, щоб код міг закінчитися таким чином:

  1. завантаження aі bз пам'яті.

  2. додати aдо b.

  3. зберегти b та перезавантажити a .

    (зберегти з реєстру процесора в пам'ять і завантажити з пам'яті в регістр процесора).

  4. додати bдо a.

  5. зберегти a(з реєстру процесора) в пам'ять.

Крок 3 дуже повільний, оскільки йому потрібно отримати доступ до фізичної пам'яті. Однак потрібно захищати від випадків, коли aі bвказують на ту саму адресу пам'яті.

Суворе згладжування дозволило б нам не допустити цього, сказавши компілятору, що ці адреси пам'яті відрізняються між собою (що в цьому випадку дозволить ще більше оптимізувати, що неможливо виконати, якщо покажчики поділяють адресу пам'яті).

  1. Це можна сказати компілятору двома способами, використовуючи різні типи для вказівки. тобто:

    void merge_two_numbers(int *a, long *b) {...}
  2. Використання restrictключового слова. тобто:

    void merge_two_ints(int * restrict a, int * restrict b) {...}

Тепер, виконуючи правило суворого згладжування, кроку 3 можна уникнути, і код запуститься значно швидше.

Насправді, додавши restrictключове слово, всю функцію можна оптимізувати до:

  1. завантаження aі bз пам'яті.

  2. додати aдо b.

  3. зберегти результат і до, aі до b.

Цю оптимізацію раніше не можна було робити через можливе зіткнення (де aі bвтричі замість подвоєння).


з обмежувальним ключовим словом, на кроці 3, чи не слід зберегти результат лише на "b"? Здається, що результат підсумовування також буде збережений у "a". Чи потрібно це "b" знову перезавантажувати?
NeilB

1
@NeilB - Так, ти маєш рацію. Ми зберігаємо лише b(не перезавантажуючи їх) та перезавантажуємо a. Я сподіваюся, що зараз зрозуміліше.
Міст

Типовий псевдонім, можливо, пропонував певні переваги до цього restrict, але я думаю, що останній у більшості обставин буде більш ефективним, а послаблення деяких обмежень registerдозволить йому заповнити деякі випадки, коли restrictце не допоможе. Я не впевнений, що коли-небудь "важливо" ставитися до Стандарту як до повного опису всіх випадків, коли програмісти повинні очікувати, що компілятори визнають докази спотворення, а не просто описують місця, де компілятори повинні припускати псевдонім навіть тоді, коли конкретних доказів цього не існує .
supercat

Зауважте, що хоча завантаження з основної оперативної пам’яті відбувається дуже повільно (і може довго затримувати ядро ​​процесора, якщо від результату залежать наступні операції), завантаження з кешу L1 відбувається досить швидко, і так відбувається запис у рядок кешу, який нещодавно писав до того ж ядра. Таким чином, всі, крім першого читання чи запису на адресу, зазвичай будуть досить швидкими: різниця між доступом до reg / mem addr менша, ніж різниця між керованим / некешованим mem addr.
curiousguy

@curiousguy - хоча ви маєте рацію, "швидка" в цьому випадку відносна. Кеш L1, ймовірно, все-таки на порядок повільніше, ніж регістри процесора (я думаю, що в 10 разів повільніше). До того ж, restrictключове слово мінімізує не тільки швидкість операцій, але і їх кількість, що може мати значення ... Я маю на увазі, зрештою, найшвидша операція - це взагалі не операція :)
Myst

6

Строгий псевдонім не дозволяє різним типам вказівників на одні і ті ж дані.

Ця стаття повинна допомогти вам зрозуміти проблему повністю.


4
Ви можете мати псевдонім між посиланнями і між посиланням і вказівником. Дивіться мій підручник dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

4
Дозволено мати різні типи вказівників на одні і ті ж дані. Там, де відбувається чітке псевдонім, коли одне і те ж місце пам'яті записується через один тип вказівника і читається через інший. Також дозволені деякі різні типи (наприклад, intструктура, яка містить an int).
ММ

-3

Технічно в C ++ суворе правило суворого псевдонімування, мабуть, ніколи не застосовується.

Зверніть увагу на визначення непрямості ( * оператор ):

Оператор Unary * виконує непряме: вираз, до якого він застосовується, має бути вказівником на тип об'єкта або вказівником на тип функції, а результат - значенням, що посилається на об'єкт або функцію, на яку вказує вираз .

Також з визначення glvalue

Glvalue - це вираз, оцінка якого визначає особистість об'єкта, (... snip)

Отже, у будь-якому чітко визначеному програмному сліді glvalue посилається на об'єкт. Тож так зване правило суворого псевдоніму не застосовується ніколи. Це може бути не те, чого хотіли дизайнери.


4
Стандарт C використовує термін "об'єкт" для позначення кількох різних понять. Серед них послідовність байтів, що виділяються виключно з якоюсь метою, необов'язково виключне посилання на послідовність байтів, до яких може бути записане чи прочитане значення певного типу , або така посилання, яка насправді має були або будуть доступні в якомусь контексті. Я не думаю, що існує якийсь розумний спосіб визначити термін "Об'єкт", який би відповідав усім способам, яким він користується.
supercat

@supercat Неправильно. Незважаючи на вашу уяву, це насправді досить послідовно. У ISO C вона визначається як "область зберігання даних у середовищі виконання, вміст якої може представляти значення". У ISO C ++ є подібне визначення. Ваш коментар навіть не має значення, ніж відповідь, тому що все, що ви згадали, - це способи подання для позначення вмісту об'єктів , тоді як відповідь ілюструє концепцію C ++ (glvalue) свого роду виразів, що тісно стосується ідентичності об'єктів. І всі правила дозволу в основному мають відношення до ідентичності, але не до їх змісту.
FrankHB

1
@FrankHB: Якщо хтось заявляє int foo;, що має доступ до виразу lvalue *(char*)&foo? Це об’єкт типу char? Чи існує цей об’єкт одночасно foo? Чи писали б fooзмінити збережене значення цього згаданого об'єкта типу char? Якщо так, чи є якесь правило, яке дозволило б отримати доступ до збереженого значення об'єкта типу charза допомогою значення lvalue int?
supercat

@FrankHB: За відсутності 6.5p7, можна просто сказати, що кожна область зберігання одночасно містить усі об'єкти кожного типу, які могли б вміститися в цій області зберігання, і що доступ до цієї області зберігання одночасно отримує доступ до всіх них. Однак інтерпретувати таким чином використання терміна "об'єкт" у 6.5p7 забороняло б робити багато чого з значеннями, що не належать до символів, що, очевидно, буде абсурдним результатом і повністю переможе мету правила. Далі, поняття "об'єкт", що використовується скрізь, крім 6.5p6, має статичний тип часу компіляції, але ...
supercat

1
sizeof (int) - 4, чи int i;створює декларація чотири об'єкти кожного типу символів in addition to one of type int ? I see no way to apply a consistent definition of "object" which would allow for operations on both * (char *) & i` та i. Нарешті, у Стандарті немає нічого, що дозволяє навіть volatileкваліфікованому вказівнику отримувати доступ до апаратних регістрів, які не відповідають визначенню "об'єкт".
supercat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.