Pull to refresh

Comments 24

Жалко, что не спросили как правильно объяснять студентам Undefined Behaviour в С++.
UFO just landed and posted this here
И если это была бы всегда ошибка на этапе компиляции, то писать на C++ было бы значительно легче.
Это замечательно. Есть ли такое для всех компиляторов С++?
Достаточно писать unit тесты, автотесты и собирать с GCC/CLang
UFO just landed and posted this here
А если бы компилятор умел читать мысли и делал бы не то, что вы написали, а что вы задумали — было б вообще великолепно, да?

В том-то и дело, что почти любая строка в C++ потенциально может стать источником UB — а равно и какой-нибудь другой проблемой.

И, как верно заметили, у студентов UB не вызывает никаких особенных проблем.

Проблемы с UB — это всегда «горе от ума»: написано, что разименовывать nullptr нельзя… но я-то знаю, что произойдёт GPF!

Нет, не знаете. Сегодня произойдёт, завтра — нет. Написано UB — значит так делать нельзя. Точка. Конец истории.

Прекратите считать, что вы умнее разработчиков стандартов и компиляторов и вдруг, внезапно UB перестанет быть проблемой и станет помощником.
Вы пишите, что «почти любая строка в C++ потенциально может стать источником UB» — сомневаюсь, что это правда. Если же это так, то это ошибка дизайна языка.

Проблема С++ это то, что компилятор считает, что программист очень умный и его программа без UB и поэтому ее можно агрессивно оптимизировать. Поэтому перед изучением С++ стоит реально оценить, достаточно ли ты умный для этого языка и в случае сомнений выбрать другой язык.
Вы пишите, что «почти любая строка в C++ потенциально может стать источником UB» — сомневаюсь, что это правда.
Почти любая арифметика может привести к переполнению. Переполнение — это UB.

Если же это так, то это ошибка дизайна языка.
Нет, это специфика предметной области. Просто то, что, скажем, в Java приводит к выбросу какого-нибудь ArrayIndexOutOfBoundsException в C/C++ может привести к более серьёзным последствиям. Но также как почти любая строка в Java может выкинуть какой-нибудь NullPointerException, ArrayIndexOutOfBoundsException или ещё чего похуже, так любая строчка в C/C++ при «неподходящих» аргументах может привести к UB.

Проблема С++ это то, что компилятор считает, что программист очень умный и его программа без UB и поэтому ее можно агрессивно оптимизировать.
Нет. Компилятор в Java так не считает, но бинарный поиск всё равно частенько не работает. «Тупой» и «предсказуемый» компилятор — вовсе не гарантия того, что ваша программа будет работать без ошибок…

Проблема C/C++ в том, что «тупые» компиляторы стали «умными», а некоторые разработчики, считающие себя умнее компилятора (и бывшие, в прошлом, реально «умнее» какого-нибудь Turbo C), «нарываются на неприятности» — после чего поднимают «вселенский вой» на форумах.

Если же UB избегать и на пытаться «домыслить» за компилятор, то программировать на C/C++ не сильно сложнее, чем на Java или C#…

Для студента это как раз несложно: сказали обязательно выделять память перд использованием (обращение к неинициализированному указателю — первый UB, на который «нарываются» новички) — выделяем, сказали не допускать переполнения целых чисел — не допускаем. Правил много, но ничего особенно страшного в них нет. А вот для программиста с опытом, который «точно знает», что переполнение ничем, кроме получения отрицательного числа там, где оно должно быть положительным, «не грозит» — поведение компилятора может действительно оказаться неожиданным…
Все таки UB — специфика дизайна языка. В универсальном ассемблере нельзя сказать, как будет обрабатываться переполнение на конкретном процессоре. А при программировании на ассемблере для конкретного процессора — можно.

Про Java пока не будем, а давайте обсудим пример отсюда:
blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
Код:
void process_something(int size) {
  // Catch integer overflow.
  if (size > size+1)
    abort();
  ...
  // Error checking from this code elided.
  char *string = malloc(size+1);
  read(fd, string, size);
  string[size] = 0;
  do_something(string);
  free(string);
}

становиться после оптимизации компилятора
void process_something(int *data, int size) {
  char *string = malloc(size+1);
  read(fd, string, size);
  string[size] = 0;
  do_something(string);
  free(string);
}

Мне кажется, это немного печальная оптимизация.
И такой быстрый код совсем не нужен.
В универсальном ассемблере нельзя сказать, как будет обрабатываться переполнение на конкретном процессоре.
И именно поэтому переполнение — это UB.

А при программировании на ассемблере для конкретного процессора — можно.
А это — уже неважно. С и C++ — это инструменты для написания переносимого кода. Если вы пытаетесь при программировании на них использовать ваше знание конкретного процессора — вас ждут сюрпризы.

а давайте обсудим пример отсюда:
blog.llvm.org/2011/05/what-every-c-programmer-should-know_14.html
Давайте.

// Catch integer overflow.
if (size > size+1)
  abort();
Типичный код написанный мистером компилятор-писали-дураки-я знаю-как-лучше. Кто заставлял вместо простого и понятного:
// Catch integer overflow.
if (size = INT_MAX)
  abort();
писать вот то, что там написали? Желание выпендриться? И вообще: почему там int, а не size_t? Чтобы было веселее?

Если даже предположить, что у нас «тупой», не оптимизирующий компилятор, то вряд ли первоначальный вариант будет быстрее (вам нужно будет куда-то скопировать «size», потом увеличить его на единицу, только после этого сравнить… вместо того, чтобы использовать одну инструкцию cmp).

Код изначально было ужасен, так стоит ли удивляться, что после вмешательства компилятора он перестал работать? Ровно то, о чём я говорил:
Некоторые разработчики, считающие себя умнее компилятора (и бывшие, в прошлом, реально «умнее» какого-нибудь Turbo C), «нарываются на неприятности» — после чего поднимают «вселенский вой» на форумах.


А теперь давайте вернёмся всё-таки к Java. Заметьте, что код, о котором там говорится вызывает на C/C++ как раз UB — но ведь и в Java он тоже не работает, пусть и по другому!

И вот это — типичная ситуация. UB делает программирование на C/C++ сложнее только тогда, когда вы начинаете аппелировать вот к этим вот при программировании на ассемблере для конкретного процессора — можно. C/C++ — не ассемблер — это, по большому счёту, всё, что нужно знать про то, чтобы при его использовании не возникало проблем с UB.

Ещё раз: где-то 99% всех бед, которые я наблюдал от «оптимизаций, базирующихся на UB» происходят из-за того, что кто-то пытается быть «слишком умным». Так вот: «шибко умным» нужно было быть с каким-нибудь Borland C++ 2.0 или каким-нибудь Zortech C++ 3.1. В те времена реально можно было отиграть заметный процент перейдя от индексов к указателям в цикле, например. В XXI веке это не нужно, а в последнее время — становится вредным.

Всё, что вам нужно помнить об UB это одну фразу: «не выпендривайтесь» — и этого достаточно в 99% случаев. Я про это уже писал.

Именно поэтому вопрос UB — это достаточно болезненная тема в C/C++ сообществе, но… не при обучении программированию! Студенты, в большинстве своём, просто недостаточно продвинуты, чтобы UB их «укусил»: чтобы написать код, который на неоптимизирующем компиляторе будет работать, а на оптимизирующем «упрётся в UB» нужно весьма немало знать!
Это не C++, это Си.
Переполнение — это UB.


Только для знаковых типов.
Переполняться, в соответствии со стандартом, могут только числа со знаком: A computation involving unsigned operands can never overflow, because a result that cannot be represented by the resulting unsigned integer type is reduced modulo the number that is one greater than the largest value that can be represented by the resulting type.
Это зависит от того, как определить переполнение. Т.е. с точки зрения здорового человека, для uint8_t: 250+10=4 — переполнение, но в C оно четко определено и нормально (хоть и называется по другому).
К сожалению в природе нет здоровых людей, есть только недообследованные. Потому лучше по возможности использовать определения из стандарта — иначе можно потратить на споры о терминологии тысячи человеко-часов и до сути так и не добраться.
Учитывая, что в большинстве своём программисты используют именно знаковые типы, утверждение

> Почти любая арифметика может привести к переполнению

является верным.
В соседней теме объяснили, почему это невозможно. Неопределённое поведение — крайне полезная штука, позволяющая писать быстрый код. Например, то же целочисленное переполнение или доступ за границы массива — это тоже примеры неопределённого поведения. Да даже reinterpret_cast<> — чистый UB. Хотите отказаться от них?

А если хотите язык без UB, то не стоит смотреть в сторону C++.
Не понимаю, почему все так выделяют UB. Такая же ошибка, как и многие другие, которые не ловятся компилятором. Например, открываем сокет и не освобождаем. Через пару дней работы программа перестает правильно работать, потому что сокеты кончились. И это не будет ошибкой на этапе компиляции. Поэтому правильный ответ — «Не пишите так. Никогда. Даже если вам кажется, что работает. И то, что ваши тесты сейчас проходят, не значит ничего.». Плюс многие вещи могут отловиться статическими анализаторами, так что не игнорируйте их предупреждения без веской причины.
Не так просто — посмотрите habrahabr.ru/post/216189.
А утечку ресурсов можно обнаружить при тестировании.
А ещё про различие Undefined и Unspecified behavior.
Кстати, как дела обстоят со стандартным пакетным менеджером? Есть ли какие-то новости?
Sign up to leave a comment.

Articles