mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 05:14:34 +03:00
dangling
This commit is contained in:
20
README.md
20
README.md
@@ -6,19 +6,10 @@
|
||||
|
||||
--------------
|
||||
|
||||
Все начинается просто и незатейливо: обычный десятиклассник увлекается программированием, знакомится с алгоритмическими задачками, решения которых должны быть быстрыми. Узнает о языке C++, учит минимальный синтаксис, основные конструкции, контейнеры, решает задачи с предопределенным и всегда корректным форматом ввода и вывода, и горя не знает...
|
||||
|
||||
В это же время, где-то в большом мире, матерые разработчики каждый день ругают то одни языки программирования, то другие. По самым разным причинам:
|
||||
не удобно, нет какой-то возможности, много лишних букв писать, ошибки в стандартной библиотеке... Но есть язык, который ругают за все и особенно за такую непонятную и таинственную вещь как неопределенное поведение (undefined behavior, UB).
|
||||
|
||||
Спустя лет пять или шесть наш простой десятиклассник, горя не видавший в море оторванных от реальности программ, внезапно узнает, что тем самым горячо нелюбимым языком всегда был, остается и будет его C++.
|
||||
|
||||
А потом еще в течение нескольких лет он наткнется на самые кошмарные и невероятные ужасы, поджидающие программистов на C++ почти на каждом шагу. Так и появится эта серия заметок, собирающая наиболее отвратительные примеры, на которые очень легко наткнуться при решении повседневных задач.
|
||||
|
||||
"Преждевременная оптимизация -- корень всех зол" (Д. Кнут или Э. Хоар -- в зависимости от того, какой источник смотрите). Язык С++, пожалуй, наиболее яркая тому демонстрация: огромное количество ошибок в C++ программах свзязаны
|
||||
с неопределенным поведением, заложенным в фундаменте языка просто для того, чтобы дать простор оптимизациям.
|
||||
с неопределенным поведением, заложенным в фундаменте языка просто для того, чтобы дать простор оптимизациям на этапе компиляции.
|
||||
|
||||
Если вы собираетесь писать на C++ код, в работоспособности которого вы хотите быть хоть немного уверенными, вам стоит знать о существовании различных подводных камней, ловко расставленных мин в стандарте языка и всячески их избегать. Иначе ваши программы будут работать правильно только на конкретной машине и только по воле случая.
|
||||
Если вы собираетесь писать на C++ код, в работоспособности которого хотите быть хоть немного уверенными, вам стоит знать о существовании различных подводных камней и ловко расставленных мин в стандарте языка, и всячески их избегать. Иначе ваши программы будут работать правильно только на конкретной машине и только по воле случая.
|
||||
|
||||
|
||||
|
||||
@@ -27,5 +18,8 @@
|
||||
|
||||
# Содержание
|
||||
0. [Как искать UB?](how_to_find_ub.md)
|
||||
1. Целые и вещественные числа
|
||||
1. [Переполнение знаковых целых чисел](integers/overflow.md)
|
||||
1. [Сужающие преобразования](numeric/narrowing.md)
|
||||
2. Целые и вещественные числа
|
||||
1. [Переполнение знаковых целых чисел](numeric/overflow.md)
|
||||
2. [Числа с плавающей точкой](numeric/floats.md)
|
||||
3. Нарушения lifetime объектов
|
||||
270
lifetime/use_after_free.md
Normal file
270
lifetime/use_after_free.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# Висячие ссылки, указатели и use-after-free
|
||||
|
||||
80% случаев неопределенного поведения в C++ связаны с ними.
|
||||
|
||||
Объект жил на стеке и умер. Или объект жил в куче и умер. Разница по сути не очень большая: обобщенный сценарий воспроизведения ошибки один и тот же -- где-то остались указатель или ссылка на уже мертвый объект. А потом этой ссылкой (или указателем) воспользовались, чтобы обратиться к мертвому объекту. Такой спиритический сеанс заканчивается неопределенным поведением. Если повезет -- будет ошибка сегментации с возможностью узнать, кто именно обратился.
|
||||
|
||||
Но все же между мервым объектом со стека или мервым объектом из кучи есть разница в возможности обнаружения методами динамического анализа:
|
||||
Для инструментации стека санитайзерами вообще говоря нужно перекомпилировать программу. Для инструментации кучи -- можно подменить библиотеку с аллокатором.
|
||||
|
||||
------
|
||||
Конечно, в жизни почти никто и никогда явно не пишет некорректный код вида
|
||||
```C++
|
||||
|
||||
int main() {
|
||||
int* p = nullptr;
|
||||
{
|
||||
int x = 5;
|
||||
p = &x;
|
||||
}
|
||||
return *p;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Но проблема в том, что подобный код в языке С++ может быть ловко замаскирован под слоем
|
||||
абстракций из классов и функций.
|
||||
|
||||
Простой пример:
|
||||
|
||||
```C++
|
||||
int main() {
|
||||
int x = 11;
|
||||
auto&& min_x_10 = std::min(x, 10);
|
||||
return min_x_10;
|
||||
}
|
||||
```
|
||||
|
||||
В [этом](https://godbolt.org/z/zWh1j7) коде неопределенное поведение из-за висячей ссылки.
|
||||
|
||||
Проблема в том, что `std::min` объявлен как
|
||||
```C++
|
||||
template<class T> const T& min(const T& a, const T& b);
|
||||
```
|
||||
|
||||
Число `10` является временным объектом (_prvalue_), который умирает сразу же по выходе из функции `std::min`.
|
||||
|
||||
В C++ разрешено присваивать временные объекты константным ссылкам. В таком случае константная ссылка продлевает временному объекту жизнь (объект "материализуется") и живет до выхода ссылки из области видимости. Дальнейшие присваивания константным ссылкам эффекта продлевания времени жизни не имеют.
|
||||
|
||||
Любой код, возвращающий из функции или метода ссылку или сырой указатель, является потенциальным источником проблем где угодно.
|
||||
Код, который только принимает аргументы по ссылке и никуда эти ссылки не сохраняет, также может быть источником проблем, но в куда более неочевидных ситуациях.
|
||||
|
||||
```C++
|
||||
template <class T>
|
||||
void append_n_copies(std::vector<T>* elements, const T& x, int N) {
|
||||
for (int i = 0; i < N; ++i) {
|
||||
elements->push_back(x);
|
||||
}
|
||||
}
|
||||
|
||||
void foo() {
|
||||
std::vector<int> v; v.push_back(10);
|
||||
...
|
||||
append_n_copies(&v, v.front(), 5); // будет UB при реаллокации вектора!
|
||||
}
|
||||
```
|
||||
У такого кода есть все шансы появиться в реальном проекте и доставить множество неприятностей.
|
||||
|
||||
В некритичных к производительности учатсках кода лучше использовать передачу по значению и перемещение вместо передачи по ссылке. Это, увы, также не снимает всех проблем, но ошибка в программе будет явно локализована в точке вызова функции, а не размазана по ее телу.
|
||||
|
||||
```C++
|
||||
template <class T>
|
||||
std::vector<T> append_n_copies(std::vector<T> elements, T x, int N) {
|
||||
for (int i = 0; i < N; ++i) {
|
||||
elements.push_back(x);
|
||||
}
|
||||
return elements; // implicit move
|
||||
}
|
||||
|
||||
void foo() {
|
||||
std::vector<int> v; v.push_back(10);
|
||||
...
|
||||
// v = append_n_copies(std::move(v), v.front(), 5);
|
||||
// UB, use-after-move, порядок вычисления аргументов неопределен:
|
||||
// v.front() может быть вызван на пустом векторе
|
||||
|
||||
auto el = v.front();
|
||||
v = append_n_copies(std::move(v), std::move(el), 5);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Если нужно работать со ссылками, стоит озаботиться их безопасностью.
|
||||
|
||||
|
||||
Например, можно использовать `std::reference_wrapper`, которому нельзя присваивать временные объекты.
|
||||
|
||||
```C++
|
||||
#include <utility>
|
||||
|
||||
template <class T>
|
||||
std::reference_wrapper<const T> safe_min(std::reference_wrapper<const T> a,
|
||||
std::reference_wrapper<const T> b){
|
||||
return std::min(a, b);
|
||||
}
|
||||
|
||||
int main() {
|
||||
const int x = 11;
|
||||
auto&& y = safe_min<int>(x, 11); // compilation error
|
||||
}
|
||||
```
|
||||
|
||||
Или, с помощью _forwarding references_ проанализировать категорию (rvalue/lvlaue) переданного аргумента и решить, что с ним делать. На С++20 это выглядит так:
|
||||
|
||||
```C++
|
||||
#include <type_traits>
|
||||
|
||||
template <class T1, class T2>
|
||||
requires std::is_same_v<std::decay_t<T1>,
|
||||
std::decay_t<T2>> // std::min требует одинаковых типов
|
||||
decltype(auto) // выводим тип без отбрасывания ссылок
|
||||
safe_min(T1&& a, T2&& b) { // forwarding reference на каждый аргумент.
|
||||
if constexpr (std::is_lvalue_reference_v<decltype(a)> &&
|
||||
std::is_lvalue_reference_v<decltype(b)>) {
|
||||
// оба аргумента были lvalue -- можно безопасно вернуть ссылку
|
||||
return std::min(a, b);
|
||||
} else {
|
||||
// один из аргументов -- временный объект.
|
||||
// возвращаем по значению.
|
||||
// для этого делаем копию
|
||||
auto temp = std::min(a,b); // auto&& нельзя!
|
||||
// иначе return выведет ссылку
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Конкретно для функций `std::min` и `std::max` в стандартной библиотеке есть безопасные версии, принимающие аргументы по значению и также возвращающие результат по значению.
|
||||
Более того, они "поддерживают" более двух аргументов.
|
||||
|
||||
```C++
|
||||
const int x = 11;
|
||||
const int y = 20;
|
||||
auto&& y = std::min({x, 10, 15, y}); // OK
|
||||
```
|
||||
|
||||
Может показаться, что проблема возврата ссылок касается только `const` ссылок. С неконстантными ссылками никаких паразитных продлений жизни нет, и все должно быть хорошо. Однако, это не совсем так.
|
||||
|
||||
----
|
||||
|
||||
Все вышеописанное рассматривало только свободные функции и, что то же самое, статическим методы классов.
|
||||
|
||||
Но с методами классов возврат ссылок -- обычное дело. И проблемы с ними те же, но менее явные. Неявность связана с передачей указателя `this` на текущий объект.
|
||||
|
||||
Так, например, безопасная реализация условного _Builder_ с поддержкой вызовов методов по цепочке оказывается весьма нетривиальной.
|
||||
|
||||
```C++
|
||||
class VectorBuilder {
|
||||
std::vector<int> v;
|
||||
|
||||
public:
|
||||
VectorBuilder& Append(int x) {
|
||||
v.push_back(x);
|
||||
return *this;
|
||||
}
|
||||
|
||||
const std::vector<int>& GetVector() { return v; }
|
||||
};
|
||||
|
||||
int main() {
|
||||
auto&& v = VectorBuilder{}
|
||||
.Append(1)
|
||||
.Append(2)
|
||||
.Append(3)
|
||||
.GetVector(); // dangling reference
|
||||
}
|
||||
```
|
||||
|
||||
Проблема опять в умирающем объекте, вернувшем ссылку на свое содержимое.
|
||||
|
||||
Если мы перегрузим лишь `GetVector`, чтобы различать lvalue и rvalue, проблема не исчезнет:
|
||||
|
||||
```C++
|
||||
class VectorBuilder {
|
||||
...
|
||||
const std::vector<int>& GetVector() & {
|
||||
std::cout << "As Lvalue\n";
|
||||
return v;
|
||||
}
|
||||
|
||||
std::vector<int> GetVector() && {
|
||||
std::cout << "As Rvalue\n";
|
||||
return std::move(v);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Мы [получим](https://godbolt.org/z/3aGP53) сообщение "As Lvalue". Цепочка `Append`
|
||||
неявно превратила безымянный временный объект в не совсем временный.
|
||||
|
||||
`Append` также нужно перегрузить для разбора случаев rvalue и lvalue:
|
||||
|
||||
```C++
|
||||
class VectorBuilder {
|
||||
...
|
||||
VectorBuilder& Append(int x) & {
|
||||
v.push_back(x);
|
||||
return *this;
|
||||
}
|
||||
|
||||
VectorBuilder&& Append(int x) && {
|
||||
v.push_back(x);
|
||||
return std::move(*this);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Мы справились с висячей ссылкой на содержимое вектора.
|
||||
|
||||
Однако, если мы захотим написать так
|
||||
```C++
|
||||
auto&& builder = VectorBuilder{}.Append(1).Append(2).Append(3);
|
||||
```
|
||||
|
||||
Опять получим висячую ссылку, но уже на сам объект `VectorBuilder`. Причем выкидывание перегрузки `Append` тут не при чем -- неявный `this` успевал прибиваться к временному объекту, и единоразово продлевать ему жизнь.
|
||||
|
||||
Чтобы этого избежать, нам нужно:
|
||||
|
||||
Либо настраивать линтер, запрещающий использовать `auto&&` и `const auto&` c этим классом в правой части.
|
||||
|
||||
Либо жертвовать производительностью, и в rvalue версии `Append` возвращать по значению (+ move) -- при большом количестве примитивных, всегда копируемых, объектов внутри просадка будет заметной.
|
||||
|
||||
Либо в принципе запретить использовать `VectorBuilder` в rvalue контексте:
|
||||
```C++
|
||||
class VectorBuilder {
|
||||
...
|
||||
auto Append() && = delete;
|
||||
}
|
||||
```
|
||||
Но тогда строить цепочки, начиная с безымянного временного объекта, будет нельзя.
|
||||
|
||||
|
||||
Также, не стоит никогда [играть](https://godbolt.org/z/6xbcrM) с цепочками операций `op=` (`+=`, `-=`, `/=`) над временными объектами. Для них редко когда обрабатывают rvalue случай:
|
||||
|
||||
```C++
|
||||
struct Min {
|
||||
int x;
|
||||
|
||||
Min& operator += (const Min& other) {
|
||||
x = std::min(x, other.x);
|
||||
return *this;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
int main() {
|
||||
auto&& m = (Min{5} += Min {10});
|
||||
return m.x; // dangling reference
|
||||
}
|
||||
```
|
||||
|
||||
Или с [использованием](https://godbolt.org/z/Yc9MWj) типов стандартной бибилиотеки:
|
||||
```C++
|
||||
int main() {
|
||||
using namespace std::literals;
|
||||
auto&& m = "hello"s += "world";
|
||||
std::cout << m; // dangling reference
|
||||
}
|
||||
```
|
||||
---------------
|
||||
33
numeric/floats.md
Normal file
33
numeric/floats.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Числа с плавающей точкой
|
||||
|
||||
С `float` и `double` в принципе всегда все сложно. Особенно в C++.
|
||||
|
||||
Стандарт C++ не требует следования стандарту IEEE 754, потому деление на ноль в вещественных числах также считается неопределенным поведением, несмотря на то, что
|
||||
по IEEE 754 выражение `x/0.0` определяется как `-INF`, `NaN`, или `INF` в зависимости от знака числа `x` (`NaN` для нуля).
|
||||
|
||||
Сравнение вещественных чисел -- излюбленная головная боль.
|
||||
Вещественные числа до C++20 нельзя было использовать в качестве параметров-значений в шаблонах. Теперь же можно. Правда, ожидать, что вы насчитаете в run-time и в compile-time одно и то же -- [не стоит](https://godbolt.org/z/q55891).
|
||||
|
||||
Выражение `x == y` фактически является кривым побитовым сравнением для чисел с плавающей точкой, по особенному работающее со случаями `-0.0` и `+0.0`, и `NaN`.
|
||||
О существовании этого и `!=` операторов для вещественных чисел стоит забыть и никогда не вспонимать.
|
||||
|
||||
Для побитового сравнения нужно использовать `memcmp`.
|
||||
Для сравнения чисел -- приближенные варианты вида `std::abs(x - y) < EPS`, где `EPS` -- какое-то абсолютное или вычисляемое на основе `x` и `y` значение. А также различные манипуляции с [`ULP`](https://en.wikipedia.org/wiki/Unit_in_the_last_place) сравниваемых чисел.
|
||||
|
||||
Так как стандарт C++ не форсирует IEEE 754,
|
||||
проверки на `x == NaN` через его свойство `(x != x) == true` могут быть убраны компилятором, как заведомо ложные. Проверять нужно с помощью предназначенных для этого
|
||||
функций `std::isnan`.
|
||||
|
||||
Поддерживается или нет IEEE 754 можно проверить с помощью предопределенной константы
|
||||
`std::numeric_limits<FloatType>::is_iec559`
|
||||
|
||||
Сужающие преобразования из `float` в знаковые или беззнаковые целые могут повлечь неопределенное поведение, если значение непредставимо в целочисленном типе. Никаких обрезок по модулю `2^N` не предполагается.
|
||||
|
||||
```C++
|
||||
constexpr uint16_t x = 1234567.0; // CE, undefined behavior
|
||||
```
|
||||
|
||||
|
||||
### Полезные ссылки
|
||||
1. https://en.cppreference.com/w/cpp/numeric/math/isnan
|
||||
2. https://bitbashing.io/comparing-floats.html
|
||||
@@ -168,6 +168,14 @@ ErrorOrInteger<I> mod(I a, std::type_identity_t<I> b) {
|
||||
Также проблемы неопределенного поведения при переполнении касаются битовых сдвигов влево для отрицательных чисел (или при сдвиге положительного числа с залезанием в знаковый бит). Начиная с C++20, стандарт требует фиксированной единой реализации отрицательных чисел -- через дополнительный код ([_two's complement_](https://en.wikipedia.org/wiki/Two%27s_complement)), и многие проблемы сдвигов сняты. Тем не менее все равно стоит следовать общей рекомендации: любые битовые операции выполнять только в `unsigned` типах.
|
||||
|
||||
|
||||
Стоит заметить, что сужающее преобразование из целочисленного типа в другой целочисленный тип к неопределенному поведению не приводит, и выполнять побитовое `и` с маской перед присваиванием переменной меньшего типа не обязательно. Но желательно, чтобы избежать предупреждений компилятора
|
||||
```C++
|
||||
constexpr int x = 12345678;
|
||||
constexpr uint8_t first_byte = x; // Implicit cast. Warning
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Полезные ссылки
|
||||
1. https://wiki.sei.cmu.edu/confluence/display/c/INT32-C.+Ensure+that+operations+on+signed+integers+do+not+result+in+overflow
|
||||
2.
|
||||
Reference in New Issue
Block a user