Які загальні невизначені форми поведінки, про які повинен знати програміст на C ++? [зачинено]


201

Які загальні невизначені форми поведінки, про які повинен знати програміст на C ++?

Скажіть, як:

a[i] = i++;


3
Ти впевнений. Це виглядає чітко визначеним.
Мартін Йорк

17
6.2.2 Порядок оцінювання [expr.evaluation] мовою програмування C ++ кажуть так. Я не маю жодної іншої посилання
yesraaj

4
Він правий .. щойно переглянув 6.2.2 мовою програмування на C ++, і там сказано, що v [i] = i ++ не визначено
dancavallaro

4
Я б міг уявити, тому що комлер змушує виконувати i ++ до або після обчислення місця в пам'яті v [i]. Звичайно, я завжди буду призначений туди. але він може писати або v [i], або v [i + 1] залежно від порядку операцій ..
Еван Теран

2
Все, на що говорить мова програмування C ++, - це "Порядок операцій підвиразів у виразі не визначений. Зокрема, ви не можете припустити, що вираз оцінюється зліва направо".
dancavallaro

Відповіді:


233

Покажчик

  • Перенаправлення NULLпокажчика
  • Перенаправлення покажчика, що повертається "новим" виділенням розміру нуля
  • Використання покажчиків на об’єкти, термін служби яких закінчився (наприклад, стек виділених об'єктів або видалених об'єктів)
  • Перенаправлення покажчика, який ще не був точно визначений
  • Виконання арифметики вказівника, яка дає результат поза межами (або вище, або внизу) масиву.
  • Перенаправлення вказівника на місце, розташоване поза кінцем масиву.
  • Перетворення покажчиків на об’єкти несумісних типів
  • Використовується memcpyдля копіювання буферів, що перекриваються .

Буфер переповнює

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

Цілий перелив

  • Переповнене ціле число переповнення
  • Оцінка виразу, який математично не визначений
  • Значення зсуву вліво на негативну суму (правильне зміщення на негативні суми визначено реалізацією)
  • Зміщення значень на величину, що перевищує або дорівнює кількості бітів у кількості (наприклад, int64_t i = 1; i <<= 72не визначено)

Типи, ролі та загравання

  • Закидання числового значення у значення, яке не може бути представлено цільовим типом (безпосередньо чи через static_cast)
  • Використання автоматичної змінної до її визначеного призначення (наприклад, int i; i++; cout << i;)
  • Використання значення будь-якого об'єкта типу, окрім volatileабо sig_atomic_tпри отриманні сигналу
  • Спроба змінити літеральний рядок або будь-який інший об'єкт const протягом його життя
  • Об’єднання вузького з широким літеральним рядком під час попередньої обробки

Функція та шаблон

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

ООП

  • Каскадні руйнування об'єктів зі статичною тривалістю зберігання
  • Результат віднесення об'єктів, що частково перекриваються
  • Рекурсивно повторне введення функції під час ініціалізації її статичних об'єктів
  • Здійснення віртуальних функцій викликає чисті віртуальні функції об'єкта від його конструктора чи деструктора
  • Посилаючись на нестатичні члени об'єктів, які не були побудовані або вже зруйновані

Вихідний файл та попередня обробка

  • Непорожній вихідний файл, який не закінчується новим рядком або закінчується зворотною косою рисою (до C ++ 11)
  • Зворотний косий рядок з наступним символом, який не є частиною вказаних кодів евакуації в символьній або рядковій константі (це визначено реалізацією в C ++ 11).
  • Перевищення меж реалізації (кількість вкладених блоків, кількість функцій у програмі, наявний простір стека ...)
  • Числові значення препроцесора, які не можуть бути представлені символом a long int
  • Директива щодо попередньої обробки ліворуч від визначення функції, що нагадує функцію
  • Динамічно генеруючи визначений маркер у #ifвиразі

Для класифікації

  • Виклик виходу під час знищення програми зі статичною тривалістю зберігання

Hm ... NaN (x / 0) та нескінченність (0/0) були охоплені IEE 754, якщо C ++ був розроблений пізніше, чому він записує x / 0 як невизначений?
new123456

Re: "Похила косої риси з наступним символом, який не є частиною вказаних кодів евакуації в символьній або рядковій константі." Це UB в C89 (§3.1.3.4) та C ++ 03 (який включає C89), але не в C99. C99 каже, що "результат не є лексемою, і потрібна діагностика" (§6.4.4.4). Імовірно, C ++ 0x (який включає C89) буде однаковим.
Адам Розенфілд

1
Стандарт C99 містить перелік невизначених форм поведінки у додатку J.2. Щоб адаптувати цей список до C ++, знадобиться певна робота. Вам доведеться змінити посилання на правильні пропозиції C ++, а не на C99, видалити що-небудь нерелевантне, а також перевірити, чи всі ці речі дійсно не визначені в C ++, а також C. Але це дає початок.
Стів Джессоп

1
@ new123456 - не всі одиниці з плаваючою точкою сумісні з IEE754. Якщо C ++ вимагає відповідності IEE754, компіляторам необхідно перевірити та обробити випадок, коли RHS дорівнює нулю за допомогою явної перевірки. Зробивши поведінку не визначеною, компілятор може уникнути цього накладного висловлювання, "якщо ви не використовуєте IEE754 FPU, ви не отримаєте IEEE754 FPU".
SecurityMatt

1
"Оцінка виразу, результат якого не знаходиться в діапазоні відповідних типів" .... переливання цілих чисел добре визначено для невідомих цілісних типів, просто не підписаних.
nacitar sevaht

31

Порядок оцінювання параметрів функції - це не визначена поведінка . (Це не призведе до збою, вибуху або замовлення піци ... На відміну від невизначеної поведінки .)

Єдина вимога - всі параметри повинні бути повністю оцінені до виклику функції.


Це:

// The simple obvious one.
callFunc(getA(),getB());

Це може бути рівнозначно цьому:

int a = getA();
int b = getB();
callFunc(a,b);

Або це:

int b = getB();
int a = getA();
callFunc(a,b);

Це може бути будь-який; це залежить від компілятора. Результат може мати значення, залежно від побічних ефектів.


23
Порядок не визначений, не визначений.
Роб Кеннеді

1
Я ненавиджу цього :) Я втратив день роботи, коли відстежував один із цих випадків ... все одно засвоїв свій урок і знову не впав на щастя
Роберт Гулд

2
@Rob: Я б сперечався з вами про зміну значення тут, але я знаю, що комітет зі стандартів дуже прискіпливий до точного визначення цих двох слів. Тож я просто його зміню :-)
Мартін Йорк

2
Мені пощастило на цьому. Мене це покусало, коли я навчався в коледжі і мав професора, який один раз подивився на це і розповів мені про свою проблему приблизно за 5 секунд. Не кажучи, скільки часу я би витратив на налагодження в іншому випадку.
Білл Ящірка

27

Компілятор вільний повторно упорядкувати частини вираження (якщо вважати, що значення не змінюється).

З початкового питання:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Подвійне перевірене блокування. І одна проста помилка зробити.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.

що означає точка послідовності?
yesraaj


1
Ой ... це жахливо, тим більше, що я бачив, що саме така структура рекомендується в Яві
Том

Зауважте, що деякі компілятори визначають поведінку в цій ситуації. Наприклад, у VC ++ 2005+, якщо a є непостійним, необхідні бар'єри пам’яті встановлюються для запобігання переупорядкуванню інструкцій, щоб двічі перевірити блокування.
Затемнення

Мартін Йорк: <i> // (c) гарантовано відбудеться після (a) та (b) </i> Це? В цьому конкретному прикладі, правда, єдиним сценарієм, коли це могло б мати значення, якби "i" була мінливою змінною, відображеною в апаратному реєстрі, а [i] (старе значення "i") було йому псевдонімом, але чи є гарантуєте, що приріст відбудеться перед точкою послідовності?
supercat

5

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


Робили це раніше, але я не бачу, як це визначено. Цілком очевидно, що ти робиш нескінченну рекурсію у задумливості.
Роберт Гулд

Проблема полягає в тому, що компілятор не може вивчити ваш код і точно вирішити, чи буде він страждати від нескінченної рекурсії чи ні. Це екземпляр проблеми зупинки. Дивіться: stackoverflow.com/questions/235984/…
Daniel Earwicker

Так, це, безумовно, проблема, що зупиняється
Роберт Гулд

це призвело до краху моєї системи через заміну, спричинену занадто мало пам'яті.
Йоханнес Шауб - ліб

2
Константи попереднього процесу, які не вписуються в int, також час компіляції.
Джошуа

5

Призначення константи після зачистки constness, використовуючи const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined

5

Окрім невизначеної поведінки , існує також однаково неприємна поведінка, визначена реалізацією .

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

Поведінка, визначена реалізацією, - це дія програми, результат якої не визначений стандартом, але яку необхідно документувати. Приклад - "Багатобайтові літерали символів", із запитання про переповнення стека. Чи є компілятор C, який цього не зможе скомпілювати? .

Поведінка, визначена реалізацією, кусає вас лише коли ви починаєте перенесення (але оновлення до нової версії компілятора також переносить!)


4

Змінні можуть бути оновлені лише один раз у виразі (технічно один раз між точками послідовності).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.

Дійте принаймні один раз між двома точками послідовності.
Prasoon Saurav

2
@Prasoon: Я думаю, ви мали на увазі: максимум один раз між двома пунктами послідовності. :-)
Наваз

3

Основне розуміння різних екологічних меж. Повний список знаходиться в розділі 5.2.4.1 специфікації C. Ось кілька;

  • 127 параметрів в одному визначенні функції
  • 127 аргументів в одному виклику функції
  • 127 параметрів в одному макрозначенні
  • 127 аргументів в одному макро-виклику
  • 4095 символів у логічному рядку джерела
  • 4095 символів у рядку символів буквально або в широкому рядку (після конкатенації)
  • 65535 байт в об'єкті (лише в розміщеному середовищі)
  • 15 рівнів швидкості для # включено менше
  • 1023 мітки регістру для оператора перемикача (виключаючи ті, що стосуються будь-яких операторів переключення)

Я насправді трохи здивувався межі 1023 міток справи для оператора перемикання, я можу передбачити перевищення цього для генерованого коду / lex / парсери досить легко.

Якщо ці межі перевищені, у вас є невизначена поведінка (збої, вади безпеки тощо).

Правильно, я знаю, це з специфікації C, але C ++ ділиться цими основними підтримками.


9
Якщо ви досягли цих меж, у вас більше проблем, ніж невизначена поведінка.
new123456

Ви можете ЛЕГКО перевищувати 65535 байт в такому об'єкті, як STD :: вектор
Demi

2

Використовується memcpyдля копіювання між областями пам'яті, що перекриваються. Наприклад:

char a[256] = {};
memcpy(a, a, sizeof(a));

Поведінка не визначена відповідно до Стандарту C, який поширюється на стандарт C ++ 03.

7.21.2.1 Функція memcpy

Конспект

1 / #include void * memcpy (void * обмежити s1, const void * обмежити s2, size_t n);

Опис

2 / Функція memcpy копіює n символів з об'єкта, на який вказує s2, в об'єкт, на який вказує s1. Якщо копіювання відбувається між об'єктами, які перекриваються, поведінка не визначена. Повертає 3 Функція memcpy повертає значення s1.

7.21.2.2 Функція пам'яті

Конспект

1 #include void * memmove (void * s1, const void * s2, size_t n);

Опис

2 Функція пам'яті копіює n символів з об'єкта, на який вказує s2, в об'єкт, на який вказує s1. Копіювання відбувається так, ніби n символів з об’єкта, на які вказує s2, спочатку копіюються у тимчасовий масив з n символів, який не перекриває об'єкти, на які вказують s1 та s2, а потім n символів із тимчасового масиву копіюються в об’єкт, на який вказує s1. Повертається

3 Функція пам'яті повертає значення s1.


2

Єдиним типом, для якого С ++ гарантує розмір, є char. А розмір - 1. Розмір усіх інших типів залежить від платформи.


Хіба це не для чого <cstdint>? Він визначає такі типи, як uint16_6 et cetera.
Джаспер Беккерс

Так, але розмір більшості типів, скажімо, довгий, недостатньо визначений.
JaredPar

Також cstdint ще не є частиною поточного стандарту c ++. див. boost / stdint.hpp для портативного рішення.
Еван Теран

Це не визначена поведінка. Стандарт говорить, що платформа, що відповідає, визначає розміри, а не стандарт, що їх визначає.
Даніель Ервікер

1
@JaredPar: Це складний пост із великою кількістю ниток розмов, тому я підсумував це все тут . Суть полягає в наступному: "5. Щоб представити -2147483647 та +2147483647 у двійковій формі, вам потрібно 32 біти."
Джон Дайблінг

2

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.