Коли виклик функції-члена на нульовому екземплярі призводить до невизначеної поведінки?


120

Розглянемо наступний код:

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

Ми очікуємо (b)краху, оскільки xдля нульового вказівника немає відповідного члена . На практиці (a)не виходить з ладу, оскільки thisвказівник ніколи не використовується.

Оскільки (b)dereferences thisвказівник ( (*this).x = 5;), і thisє null, програма вводить невизначене поведінку, оскільки невизначення nul завжди кажуть як невизначене поведінку.

Чи (a)призводить до невизначеної поведінки? Що робити, якщо обидві функції (і x) є статичними?


Якщо обидві функції статичні , то як х можна віднести всередину baz ? (x - нестатична змінна члена)
legends2k

4
@ legends2k: Претендент xтеж був статичним. :)
GManNickG

Безумовно, але для випадку (a) він працює однаково у всіх випадках, тобто функція викликається. Однак, замінюючи значення вказівника від 0 до 1 (скажімо, через reinterpret_cast), воно майже незмінно завжди виходить з ладу. Чи розподіл значення 0 і, таким чином, NULL, як у випадку a, представляє щось особливе для компілятора? Чому він завжди збігається з будь-яким іншим значенням, яке йому надається?
Сіддхарт Шанкаран

5
Цікаво: прийде наступна версія C ++, більше не буде перенаправлення покажчиків. Тепер ми будемо виконувати непряму за допомогою покажчиків. Щоб дізнатися більше, будь ласка, виконайте непряме посилання за цим посиланням: N3362
James McNellis

3
Викликати функцію члена на нульовий покажчик - це завжди невизначена поведінка. Тільки дивлячись на ваш код, я вже відчуваю, як невизначена поведінка повільно повзає за шию!
fredoverflow

Відповіді:


113

І те (a)й (b)інше призводить до невизначеної поведінки. Завжди невизначеною поведінкою викликати функцію члена через нульовий покажчик. Якщо функція статична, вона також технічно не визначена, але є певна суперечка.


Перше, що потрібно зрозуміти, чому це невизначена поведінка, щоб знецінити нульовий покажчик. У C ++ 03 тут насправді є трохи неоднозначності.

Хоча "перенаправлення нульового вказівника призводить до невизначеної поведінки" згадується в примітках як у §1.9 / 4, так і в § 8.3.2 / 4, це ніколи прямо не зазначено. (Примітки ненормативні.)

Однак можна спробувати вивести це з § 3.10 / 2:

Lvalue означає предмет або функцію.

При перенаправлення результату є значення. Нульовий вказівник не посилається на об'єкт, тому при використанні lvalue ми маємо невизначену поведінку. Проблема полягає в тому, що попереднє речення ніколи не висловлюється, тож що означає "використовувати" lvalue? Просто навіть генерувати його взагалі або використовувати його в більш формальному сенсі для здійснення конверсії lvalue-to-rvalue?

Незважаючи на це, його напевно не можна перетворити на рецензію (§4.1 / 1):

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

Тут напевно не визначена поведінка.

Неоднозначність походить від того, чи це не визначена поведінка на затримку, але не використовувати значення з недійсного вказівника (тобто, отримати lvalue, але не перетворити його в rvalue). Якщо ні, то int *i = 0; *i; &(*i);це чітко визначено. Це активне питання .

Таким чином, у нас є суворий перегляд "dereference null pointer, view undefined поведінка" і слабкий "use a dereferenced null pointer, get undefined поведінка" view.

Тепер ми розглянемо питання.


Так, це (a)призводить до невизначеної поведінки. Насправді, якщо thisнуль, то незалежно від змісту функції, результат не визначений.

Це випливає з § 5.2.2 / 3:

Якщо E1має тип "покажчик на клас X", то вираз E1->E2перетворюється в еквівалентну форму(*(E1)).E2;

*(E1)призведе до невизначеної поведінки із суворою інтерпретацією та .E2перетворить її на реальне значення, зробивши її невизначеною поведінкою для слабкої інтерпретації.

Звідси випливає, що це невизначена поведінка безпосередньо з (§ 9.3.1 / 1):

Якщо нестатична функція члена класу X викликається для об'єкта, який не типу X, або типу, похідного від X, поведінка не визначена.


При статичних функціях сувора та слабка інтерпретація має значення. Строго кажучи, це не визначено:

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

Тобто, вона оцінюється так, ніби вона нестатична, і ми знову перенаправляємо нульовий покажчик (*(E1)).E2.

Однак, оскільки E1не використовується у статичному виклику функції члена, якщо ми використовуємо слабку інтерпретацію, виклик є чітко визначеним. *(E1)приводить до значення, значення статичної функції вирішується, *(E1)відкидається, а функція викликається. Немає конверсії lvalue to rvalue, тому немає визначеної поведінки.

У C ++ 0x станом на n3126 неоднозначність залишається. Поки що будьте безпечні: використовуйте суворе тлумачення.


5
+1. Продовжуючи педантизм, за "слабким визначенням" нестатична функція члена не була викликана "для об'єкта, який не відноситься до типу X". Його називали рівнем, який зовсім не є об'єктом. Таким чином, запропоноване рішення додає текст "або якщо lvalue порожнє значення" до пункту, який ви цитуєте.
Стів Джессоп

Не могли б ви трохи уточнити? Зокрема, що стосуються ваших посилань із закритим випуском та "активним випуском", які номери випусків? Крім того, якщо це закрите питання, що саме відповідає так / ні для статичних функцій? Я відчуваю, що пропускаю останній крок у спробі зрозуміти вашу відповідь.
Брукс Мойсей

4
Я не думаю, що дефект 315 CWG не такий «закритий», як це випливає з його присутності на сторінці «закритих питань». Обґрунтування говорить про те, що це слід дозволити, оскільки " *pне є помилкою, коли pє нульовою, якщо lvalue не перетворена в rvalue". Однак це спирається на концепцію "порожнього значення", що є частиною запропонованої резолюції щодо дефекту 232 CWG , але вона не була прийнята. Отже, з мовою як у C ++ 03, так і в C ++ 0x, перенаправлення нульового вказівника все ще не визначене, навіть якщо не існує перетворення значення-в-значення.
Джеймс Мак-Нілліс

1
@JamesMcNellis: Наскільки я розумію, якби pапаратна адреса, яка спричинила б певну дію при прочитанні, але не була оголошена volatile, заява *p;не вимагала б, але дозволила б насправді прочитати цю адресу; &(*p);проте заява забороняється робити це. Якби *pбуло volatile, читання було б потрібно. У будь-якому випадку, якщо покажчик недійсний, я не бачу, як перше твердження не було б Невизначеним поведінкою, але я також не можу зрозуміти, чому буде другий вислів.
supercat

1
".E2 перетворює його в рецензію", - Ага, ні, це не так
ММ

30

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

Ви можете подумати, що виклик функції по вказівнику об'єкта знеструмить вказівник і спричинить UB. На практиці, якщо функція не є віртуальною, компілятор перетворив її в звичайний виклик функції, передаючи вказівник як перший параметр цього , минаючи дереференцію і створюючи бомбу часу для викликаної функції-члена. Якщо функція-член не посилається на будь-які змінні або віртуальні функції-учасниці, вона може бути успішною без помилок. Пам'ятайте, що успіх потрапляє у Всесвіт "невизначеного"!

Функція Microsoft MFC GetSafeHwnd фактично покладається на таку поведінку. Я не знаю, що вони курили.

Якщо ви викликаєте віртуальну функцію, вказівник повинен бути скасований, щоб дістатися до vtable, і напевно ви збираєтесь отримати UB (можливо, збій, але пам’ятайте, що гарантій немає).


1
GetSafeHwnd спочатку робить це! Перевіряє, і якщо це правда, повертає NULL. Потім він починає кадр SEH і відкидає вказівник. якщо є порушення доступу до пам’яті (0xc0000005), це вловлюється і NULL повертається абоненту :) Інакше HWND повертається.
Петър Петров

@ ПетърПетров минуло досить багато років, як я переглянув код GetSafeHwnd, можливо, вони з цього часу вдосконалили його. І не забувайте, що вони мають інсайдерські знання про роботу компілятора!
Марк Рансом

Я констатую зразок можливої ​​реалізації, яка має той самий ефект, що насправді це зробити, це зробити реверсивну інженерію за допомогою налагоджувача :)
Петър Петров

1
"вони мають інсайдерські знання про роботу компілятора!" - причина вічної неприємності для таких проектів, як MinGW, які намагаються дозволити g ++ зібрати код, який викликає API Windows
MM

@MM Я думаю, що ми всі погодимось, що це несправедливо. І тому я також вважаю, що існує закон про сумісність, який робить його крихітним незаконним.
v.oddou
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.