C++ всегда был языком, предоставляющим разработчикам большие возможности и гибкость, но вместе с тем требующим ответственности. Одной из самых коварных проблем даже для опытных программистов остаются висячие ссылки (dangling references) - ссылки на объекты, которые уже прекратили существование. Эта проблема особенно коварна, потому что код с висячими ссылками может прекрасно компилироваться, проходить все тесты в одном окружении, а затем внезапно давать сбой в другом. Висячие ссылки возникают, когда код продолжает использовать ссылку на объект, который уже был уничтожен. Это может произойти при работе с временными объектами, при неправильном управлении ресурсами или из-за неочевидных жизненных циклов объектов. В отличие от некоторых других языков программирования, C++ не имеет автоматического сборщика мусора, который мог бы защитить от таких ошибок.
Вот типичная ситуация: функция возвращает ссылку на локальную переменную, которая уничтожается при выходе из функции. Код, использующий эту ссылку после возврата из функции, взаимодействует с уже недействительной областью памяти, что приводит к неопределенному поведению.
C++ | 1
2
3
4
| const std::string& getDefaultName() {
std::string name = "DefaultUser";
return name; // Возвращает ссылку на переменную, которая будет уничтожена!
} |
|
В C++11 и C++14 компиляторы могли выдавать предупреждения для очевидных случаев, но многие сценарии оставались необнаруженными. С приходом C++17 и C++20 ситуация улучшилась, но проблема висячих ссылок всё еще оставалась серьезной головной болью для разработчиков. C++26 делает существенный шаг вперед в решении этой проблемы. Новые механизмы, включенные в стандарт, превращают многие предупреждения компилятора (которые можно легко игнорировать) в полноценные ошибки, что заставляет разработчиков исправлять потенциально опасный код еще на этапе компиляции. Также стандарт вводит улучшения в работу со стеком, что позволяет избежать переполнения стека при работе с большими объектами в инициализаторах. Это может показаться мелочью, но на практике такие изменения защищают от ряда сложно диагностируемых проблем.
Проблема висячих ссылок от C++11 до C++26
История борьбы с висячими ссылками в C++ напоминает эволюцию систем безопасности: каждая версия стандарта добавляла новые слои защиты, пока не появился комплексный подход в C++26. Когда в 2011 году вышел стандарт C++11, он привнес в язык множество важных новшеств: лямбда-выражения, семантику перемещения, умные указатели и автоматический вывод типов. Но при этом проблема висячих ссылок получила новое измерение. Появление таких возможностей, как инициализаторы списков и возможность возврата по ссылке из функций, открыло программистам новые способы случайно создать висячие ссылки. В то время компиляторы были довольно снисходительны к таким ошибкам. GCC и Clang могли выдавать предупреждения в самых очевидных случаях, но большинство потенциально опасных конструкций оставались незамеченными:
C++ | 1
2
3
4
| vector<int>& createVector() {
vector<int> result = {1, 2, 3};
return result; // C++11 компиляторы могли, но не были обязаны предупреждать
} |
|
Стандарт C++14 не внес существенных изменений в решение этой проблемы, но начали появляться более продвинутые статические анализаторы, способные обнаруживать сложные случаи висячих ссылок. Тем не менее, использование таких инструментов не было обязательным, и многие проекты продолжали страдать от этой проблемы. Значительный прогресс произошел с выходом C++17. В этой версии стандарта появилась библиотека std::string_view , которая, будучи очень полезной, стала и источником новых проблем с висячими ссылками. string_view представляет собой невладеющую ссылку на последовательность символов, и неправильное использование могло легко привести к ссылкам на уже уничтоженные строки:
C++ | 1
2
3
4
| std::string_view getName() {
std::string name = "Alice";
return name; // Опасно! Возвращаем view на временную строку
} |
|
C++17 также ужесточил требования к компиляторам по выдаче предупреждений о таких ситуациях, но все еще не сделал их ошибками. С приходом C++20 появились концепты (concepts) и ограничения (constraints), которые позволили более точно описывать требования к типам и функциям. Хотя напрямую проблему висячих ссылок это не решало, улучшенная система типов позволяла создавать более безопасные интерфейсы и лучше контролировать передачу объектов между функциями. Также C++20 ввел std::span – аналог std::string_view для последовательностей произвольных элементов, что снова подняло вопрос о безопасном обращении с невладеющими ссылками.
Период между C++20 и C++26 характеризовался активными обсуждениями в сообществе о необходимости системного решения проблемы висячих ссылок. Разработчики компиляторов экспериментировали с новыми статическими анализаторами и предупреждениями. Интересно, что параллельно развивался язык Rust, который с самого начала ставил безопасность работы с памятью во главу угла. Его система владения (ownership) и заимствования (borrowing) гарантирует отсутствие висячих ссылок на уровне компиляции. Это оказало влияние на сообщество C++, подтолкнув его к поиску более строгих решений.
К моменту разработки C++26, стало ясно, что язык нуждается в радикальном подходе к безопасности – простых предупреждений уже недостаточно. Необходимы встроенные механизмы, которые сделают невозможным компиляцию кода с очевидно опасными паттернами. Так родилась концепция строгой диагностики и менеджеров ресурсов, которые мы рассмотрим в следующих разделах.
Ссылки на файлы ресурсов в скомпилированном .exe (подходы к менеджменту ресурсов) В общем, задался тут вопросом:
Вот у нас есть проект: там папки, здесь файлы, там же папка /Resources, в которую мы кладем картинки, тексты и... Подскажите как привязать изменение атрибута ссылки к изменению кода в параметре href у произвольной ссылки? Подскажите, как мне в соответствии с атрибутом у картинки изменить href ссылки. Т.е. есть у нас слайдер, скажем, состоящий из 5 кадров:
<ul... Безопасность кода Только начал изучать php, вот пишу калькулятор))
Подскажите, пожалуйста правильно ли я делаю?
<?php
if ($_SERVER == 'POST'){
$num1 =... Безопасность кода. Могут ли данные такие как пароль указанные в коде попасть в чужие руки?
$html =...
Основные проблемы безопасности
Ключевая сложность заключается в неочевидном времени жизни объектов. C++ предоставляет разработчику полный контроль над памятью, но взамен требует глубокого понимания жизненного цикла всех создаваемых объектов. Временные объекты – особенно коварный источник проблем, так как их время жизни может быть неинтуитивным.
C++ | 1
2
3
4
5
6
7
8
| const std::vector<int>& getVector() {
return std::vector<int>{1, 2, 3, 4, 5}; // Возвращаем ссылку на временный объект
}
void processData() {
const auto& vec = getVector(); // vec теперь содержит висячую ссылку
std::cout << vec[0]; // Неопределенное поведение!
} |
|
Этот код компилируется во многих современных компиляторах с минимальными настройками, но приводит к непредсказуемому поведению при выполнении. Временный объект std::vector уничтожается сразу после возврата из функции, но ссылка на него продолжает использоваться. Особенно опасны ситуации, когда такие ошибки не проявляются сразу. Код может работать корректно в отладочной сборке или на одной платформе, но давать сбой в релизной версии или на другой платформе. Причина в том, что неопределенное поведение может выражаться по-разному в зависимости от множества факторов, включая оптимизации компилятора и особенности целевой архитектуры.
Еще одна распространенная проблема – возврат ссылок на локальные переменные:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| class ConfigManager {
private:
std::map<std::string, std::vector<uint8_t>> configs;
public:
const std::vector<uint8_t>& findConfig(const std::string& name) {
auto it = configs.find(name);
if (it != configs.end())
return it->second;
std::vector<uint8_t> defaultConfig = {1, 2, 3}; // Локальная переменная
return defaultConfig; // Ошибка! Возвращаем ссылку на локальную переменную
}
}; |
|
В этом примере функция findConfig при отсутствии запрашиваемой конфигурации возвращает ссылку на локальную переменную defaultConfig , которая уничтожается при выходе из функции. Это приводит к висячей ссылке и потенциальным проблемам при дальнейшем использовании возвращаемого значения. Помимо явных возвратов по ссылке, есть и более тонкие сценарии. Например, при использовании лямбда-выражений, захватывающих ссылки на переменные с ограниченным временем жизни:
C++ | 1
2
3
4
5
6
7
8
9
| std::function<int()> createCallback() {
int multiplier = 10;
return [&multiplier]() { return multiplier * 2; }; // Захват по ссылке
}
void executeCallback() {
auto callback = createCallback(); // callback захватывает ссылку на multiplier
int result = callback(); // Неопределенное поведение, multiplier уже не существует
} |
|
Здесь лямбда-функция захватывает локальную переменную multiplier по ссылке, но когда createCallback завершает выполнение, multiplier уничтожается, оставляя в лямбде висячую ссылку.
Проблемы с висячими ссылками часто встречаются при работе с контейнерами STL. Итераторы и ссылки на элементы контейнера могут стать недействительными после модификации контейнера:
C++ | 1
2
3
4
| std::vector<int> numbers = {1, 2, 3, 4, 5};
auto& firstElement = numbers[0];
numbers.push_back(6); // Может вызвать перевыделение памяти для вектора
std::cout << firstElement; // Потенциально висячая ссылка, если произошло перевыделение |
|
Особо стоит отметить распространенную ошибку при работе с "плоскими" контейнерами вроде std::unordered_map или std::vector . Многие программисты забывают, что вставка новых элементов может привести к инвалидации всех имеющихся итераторов и ссылок.
Последствия висячих ссылок варьируются от очевидных крахов программы до трудноуловимых ошибок, которые проявляются только при определенных обстоятельствах. В критических системах такие ошибки могут привести к серьезным сбоям, утечке конфиденциальных данных или даже созданию уязвимостей безопасности, которые могут быть использованы злоумышленниками. Интересно отметить, что некоторые разработчики прибегают к использованию специализированных инструментов статического анализа, таких как Clang Static Analyzer, PVS-Studio или Coverity, чтобы обнаруживать потенциальные проблемы с висячими ссылками. Тем не менее, эти инструменты не являются панацеей и не могут обнаружить все возможные проблемы, особенно в сложных кодовых базах.
До появления строгих проверок в C++26 разработчики часто полагались на собственные конвенции и паттерны проектирования, чтобы минимизировать риск возникновения висячих ссылок. Например, широко применялся принцип "никогда не возвращай ссылки на локальные объекты" или "используй умные указатели вместо сырых ссылок". Однако, такие практики требуют дисциплины и могут быть нарушены под давлением сроков или при работе новых членов команды, не знакомых с принятыми конвенциями.
Проблемы при работе с контейнерами и итераторами
Контейнеры и итераторы из стандартной библиотеки – мощный инструментарий C++, но именно при работе с ними часто возникают наиболее коварные случаи висячих ссылок. Стандартная библиотека предлагает множество контейнеров, каждый со своими гарантиями и особенностями поведения, которые легко упустить из виду. Наиболее распространённая ловушка – это инвалидация итераторов. Многие операции с контейнерами могут сделать существующие итераторы недействительными, но не все разработчики учитывают эти тонкости. Например, вектор часто становится источником проблем:
C++ | 1
2
3
4
5
| std::vector<int> values = {1, 2, 3};
auto it = values.begin(); // Получаем итератор на первый элемент
values.push_back(4); // Может вызвать перевыделение памяти
*it = 10; // Потенциально неопределённое поведение! |
|
Здесь проблема в том, что std::vector гарантирует непрерывное размещение элементов в памяти. Когда вектор заполняется и требуется добавление нового элемента, происходит перевыделение большего блока памяти, копирование всех элементов и освобождение старой памяти. После этого все итераторы и ссылки на элементы становятся недействительными. Ситуация усложняется, когда речь идёт о вложенных контейнерах или сложных структурах данных:
C++ | 1
2
3
4
5
6
7
| std::vector<std::vector<int>> matrix;
matrix.push_back({1, 2, 3});
auto& row = matrix[0]; // Ссылка на вектор внутри вектора
matrix.push_back({4, 5, 6}); // Может инвалидировать все ссылки
row.push_back(7); // Неопределённое поведение |
|
Ситуации с невладеющими типами вроде std::string_view и std::span , введёнными в C++17 и C++20 соответственно, ещё сложнее. Эти типы спроектированы для эффективного представления диапазона элементов без создания копий, но при неправильном использовании они легко приводят к висячим ссылкам.
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
| std::string_view extractPrefix(const std::string& input) {
size_t pos = input.find(':');
if (pos != std::string::npos) {
return std::string_view(input.data(), pos);
}
return input; // Безопасно, пока input существует
}
std::string_view getPrefix() {
std::string temp = "user:password";
return extractPrefix(temp); // Ошибка! Возвращаем view на временный объект
} |
|
В этом примере getPrefix() возвращает string_view , указывающий на содержимое строки temp , которая уничтожается при выходе из функции. Любая попытка доступа к содержимому такого string_view приведёт к неопределённому поведению.
Ассоциативные контейнеры, такие как std::map и std::unordered_map , имеют свои особенности инвалидации. В отличие от векторов, вставка новых элементов в std::map не инвалидирует существующие итераторы (за исключением операции удаления конкретного итератора). Однако это не всегда очевидно, и программисты, переключающиеся между разными контейнерами, могут забыть об этих различиях. Одна из наименее очевидных проблем связана с контейнерами, хранящими объекты по значению. При получении ссылки на элемент контейнера важно учитывать, как долго эта ссылка будет использоваться:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| class Database {
private:
std::map<std::string, Record> records;
public:
const Record& find(const std::string& id) {
auto it = records.find(id);
if (it != records.end()) {
return it->second;
}
return Record{}; // Катастрофическая ошибка! Возврат ссылки на временный объект
}
}; |
|
Эта ошибка была бы обнаружена в C++26 на этапе компиляции, но до этого могла незаметно проскользнуть в производственный код.
Особенно остро проблема висячих ссылок проявляется в многопоточных приложениях. Представьте сценарий, где один поток извлекает ссылку на элемент в контейнере, а другой в это время модифицирует сам контейнер. Даже если каждый отдельный поток защищён блокировкой при доступе к контейнеру, ссылка может стать недействительной после выхода из критической секции:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| std::mutex mtx;
std::vector<BigObject> objects;
void thread1() {
std::lock_guard<std::mutex> lock(mtx);
auto& obj = objects[0]; // Получаем ссылку под защитой мьютекса
// Выходим из критической секции, освобождая мьютекс
// Другие потоки могут модифицировать вектор...
obj.process(); // Возможно, уже висячая ссылка!
} |
|
Для решения этих проблем разработчики часто используют дополнительные уровни абстракции, например, работают с индексами вместо итераторов или ссылок, либо применяют механизмы копирования при выходе из критической секции. Но эти подходы требуют дополнительной дисциплины и могут снижать производительность. Введение в C++26 строгих проверок на уровне компилятора существенно снижает риск появления таких ошибок. Однако полное понимание правил инвалидации для различных контейнеров STL остаётся важным навыком для любого C++ разработчика.
Технические решения в C++26
C++26 вводит ряд технических решений, направленных на борьбу с висячими ссылками и повышение безопасности кода. Эти нововведения делают язык более строгим и предпочитают раннее обнаружение ошибок на этапе компиляции, а не во время выполнения. Ключевым компонентом этих улучшений является предложение P2748R5 — "Запрет привязки возвращаемой ссылки к временному объекту". Это предложение превращает то, что раньше было просто предупреждением компилятора, в полноценную ошибку. Рассмотрим его на примере:
C++ | 1
2
3
| const std::vector<int>& getDefaultVector() {
return {1, 2, 3}; // Теперь это ошибка компиляции, а не предупреждение
} |
|
В C++23 и ранее компилятор мог выдать предупреждение, но код всё равно компилировался. В C++26 такой код не пройдёт компиляцию, что заставляет разработчика немедленно исправить потенциально опасный код.
Другое важное предложение — P2752R3, "Статическое хранение для инициализаторов в фигурных скобках". Оно решает проблему, которая может привести к переполнению стека при работе с большими объектами в инициализаторах списков.
До C++26, когда вы использовали инициализатор списка для создания контейнера, компилятор создавал временный массив на стеке:
C++ | 1
2
3
4
5
6
| static std::vector<uint8_t> getDefaultConfig() {
static std::vector<uint8_t> def {
1, 2, 3, 4, 5, /* ... представим тысячи элементов ... */
};
return def;
} |
|
Внутренне этот код трансформировался примерно так:
C++ | 1
2
3
4
5
6
7
| static std::vector<uint8_t> getDefaultConfig() {
// Создаётся временный массив на стеке
const uint8_t tempArray[] = {1, 2, 3, 4, 5, ...};
static std::vector<uint8_t> def(tempArray, tempArray + sizeof(tempArray)/sizeof(uint8_t));
return def;
} |
|
Если инициализатор содержит много элементов (например, 1-2 МБ данных), это могло привести к исчерпанию стека, который обычно ограничен несколькими мегабайтами. В C++26 такие временные массивы теперь размещаются в статической памяти, аналогично строковым литералам, что устраняет риск переполнения стека.
Помимо этих конкретных предложений, C++26 улучшает и расширяет концепцию "менеджеров ресурсов". Менеджер ресурсов — это объект, ответственный за управление жизненным циклом других объектов или ресурсов. В контексте C++ это может быть класс, который хранит и предоставляет доступ к ресурсам, гарантируя их корректное освобождение. Рассмотрим пример менеджера конфигураций:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| class ConfigManager {
private:
std::map<std::string, std::vector<uint8_t>> configs;
// Статический метод, возвращающий ссылку на статический объект
static const std::vector<uint8_t>& getDefaultConfig() {
static std::vector<uint8_t> def {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
return def;
}
public:
ConfigManager() {
configs["database"] = {
'h','o','s','t','=','l','o','c','a','l','h','o','s','t',';',
'p','o','r','t','=','5','4','3','2'
};
}
// Безопасный вариант метода findConfig
const std::vector<uint8_t>& findConfig(const std::string& name) const {
auto it = configs.find(name);
if (it != configs.end())
return it->second;
// Возвращаем ссылку на статический объект с долгим временем жизни
return getDefaultConfig();
}
}; |
|
В этом примере getDefaultConfig() возвращает ссылку на статический объект, что гарантирует, что ссылка будет действительна на протяжении всего времени жизни программы. Это безопасный паттерн, и в C++26 он остаётся валидным.
C++26 также расширяет возможности компилятора по отслеживанию времени жизни объектов. Новые алгоритмы статического анализа позволяют обнаруживать более сложные случаи висячих ссылок, включая те, что возникают при передаче временных объектов в функции или при сохранении ссылок на элементы временных контейнеров. Важной частью технических улучшений является более строгий контроль за std::string_view и std::span . Эти типы не владеют данными, на которые ссылаются, что делает их потенциально опасными при неправильном использовании. C++26 вводит дополнительные проверки для выявления случаев, когда эти типы ссылаются на временные объекты.
C++ | 1
2
3
4
5
6
7
8
| std::string_view extractDomain(const std::string& email) {
size_t atPos = email.find('@');
if (atPos != std::string::npos) {
std::string domain = email.substr(atPos + 1); // Временная строка
return domain; // В C++26 это ошибка компиляции
}
return {};
} |
|
Также стандарт вводит улучшенные средства диагностики для работы с лямбда-функциями, которые захватывают внешние переменные по ссылке. Теперь компилятор может выявлять случаи, когда лямбда-функция переживает переменные, которые она захватывает:
C++ | 1
2
3
4
5
6
7
| std::function<void()> createCallback() {
int counter = 0;
return [&counter]() { // Захват по ссылке
counter++;
std::cout << counter << std::endl;
}; // В C++26 это вызовет ошибку компиляции
} |
|
Ещё одним техническим решением является введение концепции "защищённых" контейнеров или адаптеров, которые обеспечивают дополнительный уровень проверок. Например, можно представить вариант std::vector , который отслеживает все выданные ссылки и итераторы и инвалидирует их при модификации контейнера. Хотя эти усовершенствования могут показаться просто техническими деталями, они имеют огромное практическое значение. Один из основных принципов C++ всегда заключался в том, что "вы не платите за то, что не используете". C++26 следует этому принципу, внедряя проверки безопасности, которые обнаруживают ошибки на этапе компиляции, не добавляя накладных расходов во время выполнения.
Менее очевидное, но важное изменение касается правил определения времени жизни объектов в сложных выражениях. Раньше в некоторых контекстах временные объекты могли неявно продлевать своё время жизни, что приводило к запутанным правилам и потенциальным ошибкам. C++26 упрощает и уточняет эти правила, делая их более предсказуемыми. Рассмотрим подробнее, как работает механизм проверки возврата ссылок на временные объекты:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| class UserManager {
private:
std::map<int, User> users;
public:
// Проблемный метод
const User& getDefaultUser() const {
return User{0, "Guest"}; // Ошибка в C++26: возврат ссылки на временный объект
}
// Правильные альтернативы:
// 1. Вернуть копию объекта
User getDefaultUserCopy() const {
return User{0, "Guest"};
}
// 2. Поддерживать статический объект
const User& getDefaultUserStatic() const {
static const User defaultUser{0, "Guest"};
return defaultUser;
}
// 3. Хранить объект в классе
UserManager() {
users[-1] = User{0, "Guest"};
}
const User& getDefaultUserFromMember() const {
return users.at(-1);
}
}; |
|
Компилятор в C++26 применяет сложный анализ потока данных для определения, может ли возвращаемая ссылка указывать на временный объект. Эта проверка работает даже в случаях с несколькими уровнями вызовов функций.
Еще одно важное техническое улучшение касается определения времени жизни объекта в контексте условных выражений (?: ) и разветвленных путей выполнения. Например:
C++ | 1
2
3
| const std::string& selectPrefix(bool isAdmin) {
return isAdmin ? "admin_" : std::string("user_"); // Ошибка только если isAdmin == false
} |
|
В C++26 компилятор выдаст ошибку на подобный код, указывая на проблемный путь выполнения, даже если не все пути содержат ошибку.
Кроме того, C++26 вводит более строгие проверки для случаев, когда объект возвращает ссылку на свои внутренние данные, которые могут оказаться невалидными после уничтожения объекта:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class StringHolder {
private:
std::string data;
public:
StringHolder(const std::string& s) : data(s) {}
const char* c_str() const {
return data.c_str();
}
};
const char* getConstCString() {
StringHolder temp("Hello");
return temp.c_str(); // В C++26 может быть обнаружено как ошибка
} |
|
Улучшенные компиляторы могут отслеживать такие случаи каскадного возврата данных, где владение ресурсом переходит через несколько уровней вызовов.
Отдельного внимания заслуживает новый подход к проверке инициализаторов. Проблема переполнения стека при использовании больших инициализаторов списков была неочевидной, но потенциально опасной. В C++26 инициализаторы в фигурных скобках теперь компилируются иначе:
C++ | 1
2
3
4
5
6
7
8
9
10
11
| // До C++26 (схематично)
std::vector<int> createLargeVector() {
const int tempArray[] = {1, 2, 3, /* ... тысячи элементов ... */}; // На стеке!
return std::vector<int>(std::begin(tempArray), std::end(tempArray));
}
// C++26 (схематично)
std::vector<int> createLargeVector() {
static const int tempArray[] = {1, 2, 3, /* ... тысячи элементов ... */}; // В статической памяти!
return std::vector<int>(std::begin(tempArray), std::end(tempArray));
} |
|
Это изменение особенно важно для встраиваемых систем и системного программирования, где размер стека может быть жестко ограничен.
Важно отметить, что новые проверки и гарантии C++26 помогают не только предотвратить ошибки, но и улучшают читаемость и понятность кода. Когда компилятор запрещает потенциально опасные конструкции, это направляет разработчиков к использованию более безопасных и понятных паттернов программирования. Код, который проходит строгие проверки C++26, с большей вероятностью будет корректным и поддерживаемым.
Комитет по стандартизации C++ также рассматривал более радикальные предложения, такие как введение полноценного анализа времени жизни объектов в стиле Rust, но эти идеи не вошли в C++26. Вместо этого выбран подход постепенного усиления проверок без фундаментального изменения языка, что позволяет сохранить обратную совместимость и привычные практики программирования.
Применение
Начнём с примера менеджера конфигураций, который уже упоминали. В реальных проектах такие менеджеры часто служат единой точкой доступа к настройкам программы. Вот пример проблемного кода и его исправленной версии:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Проблемный код (до C++26)
class ConfigManager {
private:
std::map<std::string, std::vector<uint8_t>> configs;
public:
ConfigManager() {
configs["database"] = {
'h','o','s','t','=','l','o','c','a','l','h','o','s','t',';',
'p','o','r','t','=','5','4','3','2'
};
}
const std::vector<uint8_t>& findConfig(const std::string& name) const {
auto it = configs.find(name);
if (it != configs.end())
return it->second;
return {42}; // Возврат ссылки на временный объект!
}
}; |
|
При компиляции с C++26, этот код вызовет ошибку из-за возврата ссылки на временный объект. Исправленная версия могла бы выглядеть так:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Исправленный код (C++26)
class ConfigManager {
private:
std::map<std::string, std::vector<uint8_t>> configs;
static const std::vector<uint8_t>& getDefaultConfig() {
static std::vector<uint8_t> def {42};
return def;
}
public:
ConfigManager() {
configs["database"] = {
'h','o','s','t','=','l','o','c','a','l','h','o','s','t',';',
'p','o','r','t','=','5','4','3','2'
};
}
const std::vector<uint8_t>& findConfig(const std::string& name) const {
auto it = configs.find(name);
if (it != configs.end())
return it->second;
return getDefaultConfig();
}
}; |
|
Это решение гарантирует, что ссылка всегда указывает на действительный объект. Статическая переменная def существует в течение всего времени работы программы, так что возвращаемая ссылка никогда не станет висячей. Альтернативный подход – возвращать значение вместо ссылки:
C++ | 1
2
3
4
5
6
7
| std::vector<uint8_t> findConfig(const std::string& name) const {
auto it = configs.find(name);
if (it != configs.end())
return it->second;
return {42};
} |
|
Или использовать std::optional для явного указания на возможное отсутствие значения:
C++ | 1
2
3
4
5
6
7
8
| std::optional<std::reference_wrapper<const std::vector<uint8_t>>>
findConfig(const std::string& name) const {
auto it = configs.find(name);
if (it != configs.end())
return std::reference_wrapper<const std::vector<uint8_t>>(it->second);
return std::nullopt;
} |
|
Выбор подхода зависит от конкретных требований: первый вариант предлагает простой и безопасный интерфейс с незначительными издержками на статический объект, второй требует копирования данных, а третий усложняет сигнатуру функции, но делает отсутствие конфигурации явным.
Рассмотрим другую типичную проблему – работу с большими объектами в инициализаторах. C++26 автоматически размещает такие инициализаторы в статической памяти:
C++ | 1
2
3
4
5
6
7
8
9
| void generateDefaultCertificate() {
// Представим, что здесь 2MB бинарных данных для сертификата
static std::vector<uint8_t> certificate {
0x30, 0x82, 0x05, 0xA1, 0x30, 0x82, 0x03, 0x89,
// ... тысячи байтов сертификата ...
};
processCertificate(certificate);
} |
|
До C++26 такой код мог привести к переполнению стека из-за создания временного массива. Теперь же компилятор автоматически использует статическую память, что делает код безопасным без каких-либо изменений с вашей стороны.
Когда дело касается невладеющих ссылочных типов, таких как std::string_view или std::span , хорошей практикой является явное документирование требований к времени жизни объектов:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // До C++26 - компилируется, но опасно
std::string_view extractUsername(const std::string& email) {
std::string username = email.substr(0, email.find('@'));
return username; // Висячая ссылка на уничтоженную строку!
}
// C++26 - ошибка компиляции
// Правильная версия:
std::string_view extractUsername(const std::string& email) {
size_t atPos = email.find('@');
if (atPos != std::string::npos) {
return std::string_view(email.data(), atPos);
}
return email; // Безопасно, пока email существует
} |
|
В многопоточном программировании C++26 также предлагает более безопасные подходы. Например, при работе с общими ресурсами в нескольких потоках:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class ThreadSafeCache {
private:
std::mutex mtx;
std::map<std::string, std::shared_ptr<Resource>> resources;
public:
// Вместо возврата ссылки, которая могла бы стать недействительной,
// возвращаем shared_ptr, обеспечивающий управление временем жизни
std::shared_ptr<Resource> getResource(const std::string& key) {
std::lock_guard<std::mutex> lock(mtx);
auto it = resources.find(key);
if (it != resources.end()) {
return it->second;
}
auto resource = std::make_shared<Resource>(key);
resources[key] = resource;
return resource;
}
}; |
|
Такой подход гарантирует, что даже если кэш будет модифицирован или очищен другим потоком, возвращённый указатель останется действительным, пока с ним работают. При работе с обратными вызовами и функторами также следует быть осторожным с захватом ссылок:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Потенциально опасно при захвате по ссылке
template<typename Callback>
void registerCallback(Callback&& callback) {
callbacks.push_back(std::forward<Callback>(callback));
}
// Более безопасный вариант, требующий захвата по значению
template<typename Callback>
void registerSafeCallback(Callback callback) {
static_assert(std::is_copy_constructible_v<Callback>,
"Callback must be copyable (captures by value)");
callbacks.push_back(std::move(callback));
} |
|
C++26 помогает выявлять многие случаи ошибочного захвата по ссылке, но всегда полезно явно указывать ваши намерения в API.
Хорошей практикой также является использование статических анализаторов кода совместно с компилятором. Даже с улучшениями C++26, некоторые сложные случаи висячих ссылок могут быть пропущены стандартными проверками. Инструменты вроде Clang Static Analyzer или PVS-Studio помогут выявить дополнительные проблемы. В крупных проектах также стоит рассмотреть внедрение собственных умных контейнеров или оберток, которые обеспечивают дополнительный уровень безопасности:
C++ | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| template<typename T>
class SafeVector {
private:
std::vector<T> data;
mutable std::atomic<size_t> accessVersion{0};
public:
class SafeReference {
private:
const SafeVector* container;
size_t version;
size_t index;
public:
SafeReference(const SafeVector* c, size_t v, size_t i)
: container(c), version(v), index(i) {}
const T& get() const {
if (container->accessVersion != version) {
throw std::runtime_error("Container was modified!");
}
return container->data[index];
}
};
SafeReference at(size_t index) const {
if (index >= data.size()) {
throw std::out_of_range("Index out of bounds");
}
return SafeReference(this, accessVersion.load(), index);
}
void push_back(const T& value) {
data.push_back(value);
accessVersion++;
}
// Другие методы...
}; |
|
Такие обертки добавляют накладные расходы, но могут быть полезны в критических секциях кода, где безопасность важнее производительности.
Оценка влияния нововведений
Одним из первых положительных эффектов можно считать сокращение времени отладки. По оценкам ряда компаний, использующих C++ в промышленной разработке, до 30% времени разработчиков уходит на поиск и исправление ошибок, связанных с управлением ресурсами. Ошибки с висячими ссылками особенно сложны для отладки, поскольку часто приводят к непредсказуемому поведению, которое трудно воспроизвести. Превращение таких ошибок из проблем времени выполнения в ошибки компиляции существенно сокращает этот цикл.
Интересно отметить, что многие крупные проекты уже сейчас готовятся к переходу на C++26 именно из-за улучшений безопасности. Базы данных, финансовые системы, телекоммуникационное оборудование — области, где надёжность критична — будут первыми, кто получит выгоду от этих изменений.
Сравнение подходов к безопасности в C++ и Rust
Говоря о безопасности управления памятью, невозможно не сравнить подходы C++ и Rust – языка, который изначально проектировался с упором на безопасность работы с памятью. Это сравнение особенно актуально в контексте нововведений C++26. Rust реализует концепцию владения (ownership) и заимствования (borrowing) на уровне системы типов. Каждое значение в Rust имеет переменную-владельца, и в каждый момент времени может существовать только один владелец. Когда владелец выходит из области видимости, значение автоматически уничтожается:
Rust | 1
2
3
4
5
6
| fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 больше не действителен, произошло перемещение владения
println!("{}", s1); // Ошибка компиляции: s1 был перемещен
} |
|
C++, напротив, традиционно полагался на дисциплину программиста и соглашения, дополненные инструментами вроде RAII, умных указателей и правила трех/пяти. C++26 делает шаг в сторону подхода Rust, но остается более гибким:
C++ | 1
2
3
4
5
6
7
| void example() {
std::string s1 = "hello";
auto& s2 = s1; // Заимствование по ссылке
std::string s3 = s1; // Копирование, не перемещение
std::cout << s1; // В C++ допустимо, в отличие от Rust
} |
|
В Rust система заимствований (borrowing) обеспечивает, что ссылки никогда не переживают свои целевые объекты. Компилятор строго следит, чтобы объект не был уничтожен, пока существуют ссылки на него:
Rust | 1
2
3
4
| fn get_ref() -> &String {
let s = String::from("hello");
&s // Ошибка компиляции: возврат ссылки на локальную переменную
} |
|
C++26 внедряет аналогичные проверки, но они ограничены определенными сценариями и не являются настолько всеобъемлющими:
C++ | 1
2
3
4
| std::string& get_ref() {
std::string s = "hello";
return s; // В C++26 это ошибка компиляции
} |
|
Rust также запрещает мутабельные заимствования при наличии других ссылок на тот же объект, предотвращая гонки данных:
Rust | 1
2
3
| let mut s = String::from("hello");
let r1 = &s; // Неизменяемое заимствование
let r2 = &mut s; // Ошибка: нельзя получить изменяемую ссылку при наличии других ссылок |
|
Ключевое различие: Rust изначально проектировался вокруг безопасности без компромиссов по производительности, тогда как C++ постепенно добавляет механизмы безопасности, сохраняя обратную совместимость и гибкость. Примечательно, что некоторые компании, например Chromium и части Mozilla, используют оба языка – C++ для существующих крупных кодовых баз и Rust для новых компонентов, особенно в критичных для безопасности частях вроде парсинга контента из недоверенных источников.
Каждый язык силен в своей нише: Rust лучше предотвращает ошибки управления памятью на этапе компиляции, а C++ обеспечивает максимальную гибкость и контроль над всеми аспектами программы. C++26 делает заимствования из модели безопасности Rust, сохраняя при этом собственную идентичность и эволюционный путь развития.
Безопасность кода Дела вот в чём. В коде у меня содержится стринговое поле, в котором байтовое представление dll файла в зашифрованном виде. При выполнении оно... Оцените безопасность кода Имеется следующий код:
Thread * volatile threadQueue;
int threadMaxRunningPriority = 0;
Mutex threadQueueMutex;
Thread * volatile... Безопасность исходного кода Доброе время суток. Думаю, что подобные темы в разделе поднимаются не редко, однако информация иногда бывает противоречивой. И надеюсь, что... безопасность php-кода Добрый день! Интересует такой вопрос: вот я размещаю свой php-код. Как мне его обезопасить от использования другими лицами? То есть чтобы сотрудники... Как влияют ссылки с иностранных ресурсов. Как влияют ссылки на выдачу и на ТИЦ ссылки с иностранных ресурсов.
Если такое влияние есть, имеет ли смысл покупать ссылки с иностранных ресурсов.... Ссылки со сторонних ресурсов открываются в новых окнах Здравствуйте!
у меня браузет Мозила ФайерФокс, в настройках выделена опция "Вместо новых окон открывать новые вкладки", однако со сторонних... Ссылки со сторонних ресурсов открываются в новых окнах Здравствуйте!
у меня браузет Мозила ФайерФокс /57.0.2 (32-бит)/, в настройках выделена опция "Вместо новых окон открывать новые вкладки",... Безопасность управляемого кода .NET Прочитал для меня шокирующие заявление в статье журнала argc & argv
'...При этом, вам нужно четко понимать некоторые моменты. Во-первых: .NET... Нужна критика кода (Интересует безопасность передачи данных) Здравствуйте, нужна критика кода (Интересует безопасность передачи данных).
Всё ли сделано нормально в плане безопасности?
Если нет подскажите... Изменение элементов ресурсов из кода Добрый день. Прошу помощи в таком вопросе:
Есть к примеру ресурс
<Window.Resources>
<Popup x:Key="pup"... Редактирование словаря ресурсов из кода Добрый день. Досталось приложение, которое необходимо локализировать. Делал по этому мануалу https://habrahabr.ru/post/256193/. Но возникла ситуация,... Висячие соединения Добрый день! Есть tcp сервер и клиент. Если клиент закрывает соединение не нормальным способом, то на сервере остается висячие соединение. Как можно...
|