Помилка компіляції C ++?


74

У мене є такий код:

#include <iostream>
#include <complex>
using namespace std;

int main() {
    complex<int> delta;
    complex<int> mc[4] = {0};

    for(int di = 0; di < 4; di++, delta = mc[di]) {
        cout << di << endl;
    }

    return 0;
}

Я очікую, що він виведе "0, 1, 2, 3" і зупиниться, але виведе нескінченний ряд "0, 1, 2, 3, 4, 5, ....."

Схоже, порівняння di<4не працює добре і завжди повертає істину.

Якщо я просто коментую ,delta=mc[di], я отримую "0, 1, 2, 3", як зазвичай. У чому проблема невинного призначення?

Я використовую Ideone.com g ++ C ++ 14 з опцією -O2.


5
Я спробував це з g ++, і без оптимізацій він працює нормально. За допомогою -O3 це дає поведінку, згадану OP. З -O1 це нормально.
Chris Card

11
Це звучить як агресивна оптимізація циклу через невизначену поведінку, як у цьому випадку
Шафік Ягмур

Незважаючи на те, що код викликає невизначену поведінку, оптимізація є досить агресивною, і, як я помічаю, це не допомагає тому, що gcc не може послідовно подавати попередження для цієї оптимізації.
Шафік Ягмур

2
@Shafik Yaghmour Причиною того, що GCC не надає попередження з, cout << diє, мабуть, те, що оператор вставки потоку для комплексу передає адресу diякомусь "непрозорому" коду (або що оператор вставки потоку для комплексу сам по собі є непрозорим - що мене здивує хоча). І залежно від того, що робить цей "непрозорий" код, поведінка програми все ще може бути чітко визначена. Я не кажу, що в цьому випадку неможливо буде надати попередження без занадто великої кількості помилкових спрацьовувань (або навіть будь-яких помилкових спрацьовувань). Тільки що це було б досить важко.
Paul Groke,

@ShafikYaghmour Я використовую Ideone.com g ++ C ++ 14 з опцією -O2. (Дякую, я додав це у своєму запитанні.)
eivour

Відповіді:


109

Це пов’язано з невизначеною поведінкою, ви отримуєте доступ до масиву mcпоза межами останньої ітерації вашого циклу. Деякі компілятори можуть виконувати агресивну оптимізацію циклу навколо припущень про відсутність невизначеної поведінки. Логіка буде схожа на таку:

  • Доступ mcпоза межами - невизначена поведінка
  • Не приймайте жодної невизначеної поведінки
  • Отже, di < 4це завжди правда, оскільки в іншому випадку mc[di]викличе невизначену поведінку

gcc з увімкненою оптимізацією та використанням -fno-aggressive-loop-optimizationsпрапора змушує поведінку нескінченного циклу зникати ( дивіться це в прямому ефірі ). Хоча живий приклад з оптимізацією, але без -fno-агресивної-циклічної оптимізації демонструє нескінченну поведінку циклу, яку ви спостерігаєте.

Godbolt живий приклад коду показує di < 4чек видаляється і замінюється і безумовна JMP:

jmp .L6

Це майже ідентично випадку, викладеному в Порівняльних стандартах GEC до 4.8 . Коментарі до цієї статті чудові і їх варто прочитати. Він зазначає, що Clang зафіксував цей випадок у статті, використовуючи -fsanitize=undefinedяку я не можу відтворити для цієї справи, але gcc використовуючи -fsanitize=undefinedробить ( дивіться в прямому ефірі ). Ймовірно, найбільш ганебною помилкою навколо оптимізатора, який робить висновок щодо невизначеної поведінки, є видалення перевірки нульового вказівника ядра Linux .

Хоча це агресивна оптимізація, важливо зазначити, що як зазначає стандарт C ++, невизначена поведінка:

поведінка, щодо якої цей міжнародний стандарт не вимагає жодних вимог

Що по суті означає, що все можливо, і це зазначає ( наголос на моєму ):

[...] Допустима невизначена поведінка варіюється від ігнорування ситуації повністю з непередбачуваними результатами , до поведінки під час перекладу або виконання програми документованим чином, характерним для середовища (з видачею діагностичного повідомлення або без нього), до припинення перекладу або виконання (з видачею діагностичного повідомлення). [...]

Для того, щоб отримати попередження від gcc, нам потрібно перемістити coutзовнішню частину циклу, після чого ми побачимо таке попередження ( перегляньте його в прямому ефірі ):

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
     for(di=0; di<4;di++,delta=mc[di]){ }
     ^

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

Примітка, -fno-aggressive-loop-optimizationsце задокументовано у примітках до випуску gcc 4.8 .


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

@MatthieuM. справді, ми постійно повертаємось до цієї теми, деякі цитати в моїй відповіді тут також є актуальними.
Шафік Ягмор

Який дурень з мене, я не помітив, що отримую доступ до mc [4];) Я використовував Ideone.com, тому я все одно не отримав попередження. Наступного разу я скористаюся редактором, який дає мені попередження, навіть коли компіляція вдається :)
eivour

@eivour може бути корисним надати посилання на ваш приклад в Інтернеті, в цьому випадку я думаю, що кожного разу, коли ви запускаєте приклад в ideone, він надає URL-адресу цього прикладу. Я особисто вважаю за краще використовувати Coliru або Wandbox, які обидва надають кнопку спільного доступу.
Шафік Ягмур

gcc 4.9.3 все ще не видає жодного попередження: g++ -Waggressive-loop-optimizations -Wall -Wextra -O3 test.cpp- відповідно до цієї сторінки компілятор повинен відобразити попередження: gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
Пітер ВАРГА

38

Оскільки ви зростаєте, diперш ніж використовувати його для індексації mc, четвертий раз через цикл ви будете посилатися на mc [4], що минув кінець вашого масиву, що, в свою чергу, може призвести до проблемної поведінки.


Проігноруйте мій останній коментар, можливо, це була проблема з тим, що ideone.com не перезапустив мій код належним чином після редагування. Спрацював ще
interjay

2
Використання одного di++,delta=mc[di-1]або delta=mc[di],di++також вирішує проблему. Схоже, Логікрат правильний.
KompjoeFriek

2
Можливо, тут є поодинока помилка, і eivour мав на увазі, delta=mc[di++]а неdelta=mc[++di] використовувати всі mcзначення?
Toby Speight

5

У вас є це:

for(int di=0; di<4; di++, delta=mc[di]) {
  cout<<di<<endl;
}

Спробуйте замість цього:

for(int di=0; di<4; delta=mc[di++]) {
   cout<<di<<endl;
}

РЕДАГУВАТИ:

Щоб пояснити, що відбувається Давайте розіб’ємо ітерацію Your For Loop:

1-а ітерація: Спочатку для di встановлено значення 0. Перевірка порівняння: Ді менше 4? Так добре продовжуйте. Збільште di на 1. Тепер di = 1. Візьміть "n-й" елемент mc [] і встановіть його як дельту. Цього разу ми беремо 2-й елемент, оскільки це індексоване значення дорівнює 1, а не 0. Нарешті, виконайте блок / коди коду всередині циклу for.

2-а ітерація: Тепер для di встановлено значення 1. Перевірка порівняння: Чи менше di 4? Так і продовжуйте. Збільште di на 1. Тепер di = 2. Візьміть "n-й" елемент mc [] і встановіть його як дельту. Цього разу ми беремо 3-й елемент, оскільки це індексоване значення дорівнює 2. Нарешті, виконайте блок / коди коду всередині циклу for.

3-а ітерація: Тепер для di встановлено значення 2. Перевірка порівняння: Чи менше di 4? Так і продовжуйте. Збільште di на 1. Тепер di = 3. Візьміть "n-й" елемент mc [] і встановіть його як дельту. Цього разу ми беремо 4-й елемент, оскільки це індексоване значення дорівнює 3. Нарешті, виконайте блок / коди коду всередині циклу for.

4-а ітерація: Тепер для di встановлено значення 3. Перевірка порівняння: Чи менше di за 4? Так і продовжуйте. Збільште di на 1. Тепер di = 4. (Ви бачите, куди це йде?) Візьміть "n-й" елемент mc [] і встановіть його як дельту. Цього разу ми беремо 5-й елемент, оскільки це індексоване значення дорівнює 4. О, у нас проблема; наш розмір масиву становить лише 4. Дельта тепер має сміття, і це невизначена поведінка або пошкодження. Нарешті, виконайте блок / коди коду всередині циклу for, використовуючи "дельту сміття".

5-а ітерація. Тепер di встановлено на 4. Перевірка порівняння: Чи менше di 4? Ні, вирватися з циклу.

Пошкодження через перевищення меж суміжної пам'яті (масиву).


5

Це тому, що di ++ виконується під час останнього циклу циклу.

Наприклад;

int di = 0;
for(; di < 4; di++);
// after the loop di == 4
// (inside the loop we see 0,1,2,3)
// (inside the for statement, after di++, we see 1,2,3,4)

Ви отримуєте доступ до mc [], коли di == 4, тож це проблема поза межами, потенційно знищуючи частину стека та пошкоджуючи змінну di.

рішенням буде:

for(int di = 0; di < 4; di++) {
    cout << di << endl;
    delta = mc[di];
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.