Files
ubbook/pointer_provenance/invalid_pointer.md
2024-09-15 18:01:28 +01:00

111 lines
9.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Указатель Шредингера: валиден и невалиден одновременно.
Что вообще такое указатель?
Когда их пытаются объяснить новичкам в C++, часто говорят, что это число, адрес, указывающий на номер ячейки в памяти, где что-то лежит.
Это в каком-то смысле справедливо на очень низком уровне — в ассемблере, в машинных кодах. Но В C/С++ указатель это не просто адрес. И тем более не число, которое как-то просто по-особому используется.
Более того, в C++ (не в C), есть указатели, которые вообще не являются адресами в памяти — указатели на поля и методы классов. Но о них мы говорить сейчас не будем.
Указатель — это ссылочный тип данных. Нечто, с помощью чего, можно получить доступ к другим объектам. И, в отличие от C++-ссылок, объекты-указатели являются настоящими объектами, а не странными псевдонимами для существующих значений. С числами и адресами в памяти указатели связаны только деталями реализации.
Для указателей в стандарте C++ подробно расписано, откуда они могут появляться.
Если коротко, то:
1. Как результат применения операции взятия адреса (`&x` или `std::addressof(x)`)
2. Как результат вызова оператора `new` (возможно, _placement new_)
3. Как результат неявного преобразования имени массива или имени функции в указатель.
4. Как результат некоторой опустимой_ операции над другим указателем.
5. Копирование существующего указателя. В частности — копирование `nullptr`.
Все остальные источники указателей — implementation defined или вообще undefined.
Главная операция, выполняемая над указателями, — разыменование, то есть получение доступа
к объекту, на который этот указатель ссылается. И вместе с этой операцией приходит главная проблема — ее не ко всем указателям применять можно. Есть и другие операции, которые также применимы не к любому указателю.
Но, конечно, есть единственная операция, допустимая (почти) всегда — сравнение на равенство (не равенство).
В идеальном светлом мире, от типа объекта зависит множество допустимых над ним операций. Но в случае указателей, и это очень печально, применимость или неприменимость зависит не только от значения указателя, но еще и от того, откуда этот указатель взялся. А также откуда взялись другие указатели!
```C++
int x = 5;
auto x_ptr = &x; // валидный указатель, его МОЖНО разыменовывать
auto x_end_ptr = (&x) + 1; // валидный указатель, но его НЕЛЬЗЯ разыменовывать
auto x_invalid_ptr = (&x) + 2; // невалидный указатель,
// само его существование недопустимо.
```
Сравнение указателей на больше или меньше определено только для указателей на элементы одного и того же массива.
Для произвольных указателей — unspecified.
Арифметика указателей допустима только в пределах одного и того же массива (от указателя на первый элемент до указателя на элемент за последним) Иначе — undefined behavior.
Особый только случай `(&x) + 1` — любой объект считается массивом из одного элемента.
Пример кода, который валится с UB именно на арифметике указателей, найти сложно, зато можно привести пример с итераторами (которые разворачиваются в указатели).
```C++
std::string str = "hell";
str.erase(str.begin() + 4 + 1 - 3);
```
Этот код [упадет](https://rextester.com/GPVRKM58250) в отладочной сборке под msvc. `str.begin() + 4` — указатель на элемент за последним. И еще `+1`
выводит за пределы строки. Это UB. И не важно, что дальше вычитание возвращает внутренний указатель обратно в границы строки.
Не стоит выполнять сложные вычисления с указателями. Прибавлять к ним или вычитать лучше всегда конечный числовой результат. В данном конкретном примере рассчет отступа (4 + 1 - 3) нужно выполнить отдельно — расставить скобки или (еще лучше) безопаснее и понятнее, вынести в отдельную переменную.
-----------
Помимо выхода за границы объектов, невалидные указатели могут появляться после отрабатывания некоторых функций.
Наиболее яркий пример такого UB представил Nick Lewycky для [Undefined Behavior Consequences Contest](https://blog.regehr.org/archives/767). Немного переделанная и под C++ (чтобы в ней было только одно UB, а не два) версия выглядит так:
```C++
int* p = (int*)malloc(sizeof(int));
int* q = (int*)realloc(p, sizeof(int));
if (p == q) {
new(p) int (1);
new(q) int (2);
std::cout << *p << *q << "\n"; // print 12
}
```
Этот код, собранный clang, [выводит](https://godbolt.org/z/31av9f) результат, противоречащий здравому смыслу (если вы не знаете, что в коде UB!). И этот же пример демонстрирует, что указатели это не просто число-адрес.
Указателем, переданным в сишную функцию `realloc`, при успешной реаллокации, пользоваться более нельзя. Его можно только перезаписать (а потом уже использовать).
Данный пример, конечно, искусственный, но в него можно легко влететь, если, например, по какой-то причине писать свою версию вектора, используя `realloc`, и захотеть немного «соптимизировать».
```C++
template <class T>
struct Vector {
static_assert(std::is_trivially_copyable_v<T>);
size_t size() const {
return end_ - data_;
}
private:
T* data_;
T* end_;
size_t capacity_;
void reallocate(size_t new_cap){
auto ndata = realloc(data_, new_cap * sizeof(T));
if (!ndata) {
throw std::bad_alloc();
}
capacity_ = new_cap;
if (ndata != data_) {
const auto old_size = size(); // !access to invalidated data_!
data_ = ndata;
end_ = data_ + old_size;
} // else — "ok", noop
}
}
```
Этот код с неопределенным поведением. Скорее всего, оно никак не проявится сейчас, но не значит, что так будет и в будущем.
Возможно, вызов `reallocate` заинлайнится в неподходящем месте и все пойдет вверх дном.
Однако, начиная любые попытки реализовать свой собственный вектор (стандартный вполне может кого-то не устраивать тем, что он по умолчанию инициализирует память), надо иметь в виду следующий печальный факт: это [невозможно](https://stackoverflow.com/questions/60481204/dynamic-arrays-in-c-without-undefined-behavior) сделать без неопределенного поведения (формального или реального). Основная причина — арифметика указателей внутри сырой памяти. В сырой памяти формально нет C++ массивов, только внутри которых арифметика и определена.