Безопасность — важный аспект C++26.
Контракты — пожалуй, самая важная функция для обеспечения безопасности.
Но C++26 может предложить гораздо больше.

Сегодня я хотел бы рассказать о трёх небольших, но важных улучшениях в C++26, которые решают типичные проблемы с безопасностью в C++
Запретить привязку возвращаемой ссылки к временной
Обнаружить проблему с безопасностью может быть непросто.
Этот фрагмент кода взят из предложения
P2748R5, на которое я ссылаюсь на протяжении всего этого раздела.
Я превратил его в минимальную исполняемую программу.
const
<string_view> #include
<string> #include
<iostream> #include
// bindReferenceToTemporary.cpp std::string_view& getString() {
static std::string s = "Hallo Welt!";
return s;
}
int main() {
std::cout << getString() << '\n';
}
Компилятор GCC уже выдает понятное сообщение об ошибке:

Нужен еще один пример?
В следующем фрагменте кода есть ошибка.
X struct {
const std::map<std::string, int> d_map;
const std::pair<std::string, int>& d_first;
X(const std::map<std::string, int>& map)
: d_map(map), d_first(*d_map.begin()) {}
};
К сожалению, программист упустил из виду, что первый элемент пары, называемый ключом, является константой. Это приводит к созданию временной переменной. В результате d_first становится недействительным.
Это подводит нас к следующему усовершенствованию системы безопасности.
Ошибочное поведение при неинициализированном чтении
Теперь я перейду к предложению p2795r5.
Прежде всего, к каким объектам это относится? Это все объекты с автоматической продолжительностью хранения и временные объекты. Особенность в том, что эти объекты инициализируются произвольным значением. Это означает, что программа имеет неопределённое поведение.
Разумеется, остается один вопрос, на который нужно ответить. Что означает автоматическая продолжительность хранения данных? Автоматическая продолжительность хранения данных предусмотрена для следующих переменных:
- Переменные, имеющие область видимости блока и не объявленные явно как
static,thread_local, илиextern. Эти переменные хранятся до тех пор, пока не завершится выполнение блока. - Переменные, относящиеся к области видимости параметров, например к функции. Они автоматически уничтожаются при закрытии области видимости параметров.
Два примера из предложения должны прояснить ситуацию:
f void extern(int);
int main() {
int x; // инициализация по умолчанию, значение x неопределенное
f(x); // преобразование glvalue в prvalue имеет неопределенное поведение
}
f void(T&);
void g(bool);
void h() {
T* p; // неинициализированный, имеет ошибочное значение (например, null)
bool b; // неинициализированный, ошибочное значение может быть недопустимым bool
f(*p); // разыменование имеет неопределённое поведение
g(b); // неопределённое поведение, если b недопустим
}
Если коротко, то предложение превращает неинициализированные операции чтения, которые в C++23 приводили к неопределённому поведению, в ошибочные программы в C++26.
Конечно, полная инициализация автоматических переменных может быть довольно затратной операцией.
Поэтому предусмотрен механизм отказа от инициализации.
[[indeterminate]] Атрибут
Атрибут [[indeterminate]] представляет собой механизм отказа от использования. Эта функция предназначена только для экспертов. Атрибут позволяет считывать значения переменных с автоматическим хранением, которые не были инициализированы, без риска возникновения ошибки в программе. Ниже приведен упрощенный пример из предложения:
int x [[indeterminate]];
std::cin >> x;
[[indeterminate]] int a, b[10], c[10][10];
compute_values(&a, b, c, 10);
// Этот класс выигрывает от отсутствия защиты от неопределенной инициализации.
struct SelfStorage {
std::byte data_[512];
void f(); // использует data_ в качестве временного хранилища
};
SelfStorage s [[indeterminate]]; // в документации предлагалось так
void g([[indeterminate]] SelfStorage s = SelfStorage()); // то же самое; необычно, но возможно
Последняя функция безопасности касается неполных типов.
Удаление указателя на неполный тип должно быть некорректным
Неполный тип — это тип данных, для которого существует только объявление, но нет определения. Указатель или ссылка на неполный тип — это нормально. Однако операции, для которых требуется знать размер, структуру или функции-члены этого типа данных, являются ошибочными.
Новая функция немного более специфична: удаление указателя на неполный тип класса является некорректным, если только этот тип класса не имеет тривиального деструктора и не содержит специфичной для класса функции освобождения памяти. Это означает, что компилятор создал деструктор для класса, operator delete который не был перегружен в классе.
В предложении приводится наглядный пример, демонстрирующий тонкую разницу между тривиальным и нетривиальным деструктором:
- Тривиальный деструктор
xyz
namespace // trivialDestructor.cpp {
struct Widget; // предварительное объявление
Widget *new_widget();
} // пространство имен xyz
int main() {
xyz::Widget *p = xyz::new_widget();
delete p;
}
namespace xyz {
struct Widget {
const char *d_name;
int d_data;
// (неявный) тривиальный деструктор
// Это единственное отличие.
};
Widget *new_widget() {
return new Widget();
}
} // namespace xyz
Нетривиальный деструктор
xyz
namespace // nontrivialDestructor.cpp {
struct Widget; // предварительное объявление
Widget *new_widget();
} // пространство имен xyz
int main() {
xyz::Widget *p = xyz::new_widget();
delete p;
}
namespace xyz {
struct Widget {
const char *d_name;
int d_data;
~Widget() {} // нетривиальный dtor
// Это единственное отличие.
};
Widget *new_widget() {
возвращает новый виджет();
}
} // пространство имен xyz
Во втором примереnontrivialDestructor.cpp, используется нетривиальный деструктор.
В соответствии с предложением, компиляция прерывается с сообщением об ошибке:
