mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-18 21:24:42 +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++ код, в работоспособности которого вы хотите быть хоть немного уверенными, вам стоит знать о существовании различных подводных камней, ловко расставленных мин в стандарте языка и всячески их избегать. Иначе ваши программы будут работать правильно только на конкретной машине и только по воле случая.
|
Если вы собираетесь писать на C++ код, в работоспособности которого хотите быть хоть немного уверенными, вам стоит знать о существовании различных подводных камней и ловко расставленных мин в стандарте языка, и всячески их избегать. Иначе ваши программы будут работать правильно только на конкретной машине и только по воле случая.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -27,5 +18,8 @@
|
|||||||
|
|
||||||
# Содержание
|
# Содержание
|
||||||
0. [Как искать UB?](how_to_find_ub.md)
|
0. [Как искать UB?](how_to_find_ub.md)
|
||||||
1. Целые и вещественные числа
|
1. [Сужающие преобразования](numeric/narrowing.md)
|
||||||
1. [Переполнение знаковых целых чисел](integers/overflow.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++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
|
1. https://wiki.sei.cmu.edu/confluence/display/c/INT32-C.+Ensure+that+operations+on+signed+integers+do+not+result+in+overflow
|
||||||
2.
|
2.
|
||||||
Reference in New Issue
Block a user