Язык программирования C++

Ядро языка

C++ включает большую часть языка C. Поскольку используется подмножество C, применимы рекомендации из Безопасное программирование на C.

Выделение массива с помощью operator new[]

Для очень больших значений n выражение типа new T[n] может вернуть указатель на область кучи, которая слишком мала. Другими словами, не все элементы массива фактически размещены в зарезервированной для массива памяти кучи. Текущие версии GCC генерируют код, выполняющий вычисление вида sizeof(T) * size_t(n) + cookie_size, где cookie_size в настоящее время не превышает 8. Это вычисление может переполниться, и версии GCC до 4.8 генерировали код, который не обнаруживал этого. (Fedora 18 была первым релизом, в котором это было исправлено в GCC.)

Вместо явного выделения массива можно использовать шаблон std::vector. (Реализация GCC внутренне обнаруживает переполнение.)

Если нет альтернативы operator new[] и исходный код будет компилироваться старыми версиями GCC, код, выделяющий массивы переменной длины, должен вручную проверять переполнение. Для примера new T[n] проверка размера может выглядеть как n || (n > 0 && n > (size_t(-1) - 8) / sizeof(T)). (См. Рекомендации по целочисленной арифметике) Если есть дополнительные размерности (которые должны быть константами согласно стандарту C++), они должны быть включены как множители в делитель.

Эти меры предотвращают запись за пределами выделенной области и потенциальное выполнение кода. Очень большие выделения памяти всё ещё могут привести к отказу в обслуживании. Рекомендации для декодеров, написанных вручную содержит предложения по смягчению этой проблемы при обработке недоверенных данных.

См. Выделение массивов для получения советов по выделению массивов в стиле C.

Перегрузка

Не перегружайте функции версиями, которые имеют разные характеристики безопасности. Например, не реализуйте функцию strcat, которая работает с аргументами std::string. Аналогично, не называйте методы в честь таких функций.

Совместимость ABI и подготовка к обновлениям безопасности

Для обновлений безопасности гораздо предпочтительнее стабильный двоичный интерфейс (ABI). Без стабильного ABI все обратные зависимости требуют перекомпиляции, что может быть большим объёмом работы и даже невозможным в некоторых случаях. В идеале обновление безопасности должно обновлять только один динамический разделяемый объект и автоматически применяться после перезапуска затронутых процессов.

За пределами критически важного для производительности кода следует обеспечить возможность широкого спектра изменений без нарушения ABI. Некоторые основные рекомендации:

  • Избегайте встраиваемых функций.

  • Используйте идиому «указатель на реализацию».

  • Старайтесь избегать шаблонов. Используйте их, если повышенная безопасность типов даёт преимущество программисту.

  • Выносите критически важный для безопасности код из шаблонного кода, чтобы при необходимости его можно было исправить в централизованном месте.

Проект KDE публикует документ с более подробными рекомендациями по изменению кода C++ с сохранением ABI, Policies/Binary Compatibility Issues With C++ (d-pointer относится к идиоме «указатель на реализацию»).

Поддержка C++0X и C++11

GCC предлагает различные режимы совместимости языка:

  • -std=c++98 для исходного стандарта C++ 1998 года

  • -std=c++03 для стандарта 1998 года с изменениями из технического отчёта TR1

  • -std=c++11 для стандарта C++ 2011 года. Эту опцию не следует использовать.

  • -std=c++0x для нескольких различных версий поддержки C++11 в разработке, в зависимости от версии GCC. Эту опцию не следует использовать.

Для каждого из этих флагов существуют варианты, которые также включают расширения GNU (в основном языковые возможности, также присутствующие в C99 или C11):

  • -std=gnu++98

  • -std=gnu++03

  • -std=gnu++11

Опять же, -std=gnu++11 не следует использовать.

Если вы включите поддержку C++11, ABI стандартной библиотеки C++ libstdc++ изменится тонким образом. В настоящее время ни одна библиотека C++ не компилируется в режиме C++11, поэтому если вы скомпилируете свой код в режиме C++11, он будет несовместим с остальной системой. К сожалению, это также относится к случаю, если вы не используете никаких возможностей C++11. В настоящее время нет безопасного способа включить режим C++11 (кроме автономных приложений).

Значение режима C++0X менялось от выпуска к выпуску GCC. Ранние версии всё ещё были ABI-совместимы с режимом C++98, но в новейших версиях переход в режим C++0X активирует поддержку C++11 со всеми её проблемами совместимости.

Некоторые возможности C++11 (или их приближения) доступны с поддержкой TR1, то есть с -std=c03` или [option]`-std=gnu03 и в заголовочных файлах <tr1/*>. Это включает std::tr1::shared_ptr (из <tr1/memory>) и std::tr1::function (из <tr1/functional>). Для других возможностей C++11 библиотека Boost C++ содержит замены.

Стандартная библиотека C++

Стандартная библиотека C++ включает по ссылке большую часть своей аналогичной библиотеки C, см. Безопасное программирование на C.

Функции, которые сложно использовать

В этом разделе собраны функции и шаблоны функций, которые являются частью стандартной библиотеки и сложны в использовании.

Непарные итераторы

Функции, использующие выходные операторы или итераторы, которые не представлены парами (обозначающими диапазоны), не могут выполнять проверку диапазона итераторов. (См. Итераторы) Шаблоны функций, использующие выходные итераторы, особенно опасны:

  • std::copy

  • std::copy_backward

  • std::copy_if

  • std::move (трёхаргументный вариант)

  • std::move_backward

  • std::partition_copy_if

  • std::remove_copy

  • std::remove_copy_if

  • std::replace_copy

  • std::replace_copy_if

  • std::swap_ranges

  • std::transform

Кроме того, std::copy_n, std::fill_n и std::generate_n также не выполняют проверку итераторов, но существует явное количество, которое должен предоставить вызывающий, в отличие от неявного индикатора длины в виде пары прямых итераторов.

Эти функции, ожидающие выходной итератор, следует использовать только с выходными итераторами неограниченного диапазона, такими как итераторы, полученные с помощью функции std::back_inserter.

Другие функции используют один входной или прямой итератор, который может читать за пределы входного диапазона, если вызывающий неосторожен:

  • std::equal

  • std::is_permutation

  • std::mismatch

Обработка строк с помощью std::string

Класс std::string предоставляет удобный способ работы со строками. В отличие от строк C, объекты std::string имеют явную длину (и могут содержать встроенные символы NUL), а память для их символов управляется автоматически. В этом разделе рассматривается std::string, но эти наблюдения также применимы к другим экземплярам шаблона std::basic_string.

Указатель, возвращаемый функцией-членом data(), не обязательно указывает на строку, завершённую нулевым символом. Чтобы получить указатель на строку, совместимый с C, используйте c_str(), которая добавляет нулевой терминатор.

Указатели, возвращаемые функциями data() и c_str(), а также итераторы действительны только до наступления определённых событий. Требуется, чтобы конкретный объект std::string всё ещё существовал (даже если он был изначально создан как копия другого строкового объекта). Указатели и итераторы также становятся недействительными при вызове неконстантных функций-членов или функций с параметром неконстантной ссылки. Поведение реализации GCC отклоняется от требуемого стандартом C++ при наличии нескольких потоков. В общем случае только первый вызов неконстантной функции-члена после структурного изменения строки (например, добавления символа) делает их недействительными, но это также относится к функциям-членам, таким как неконстантная версия begin(), что нарушает стандарт C++.

Особая осторожность требуется при вызове функции-члена c_str() для временного объекта. Это удобно для вызова функций C, но указатель станет недействительным, как только временный объект будет уничтожен, что обычно происходит, когда самое внешнее выражение, содержащее выражение, для которого вызывается c_str(), завершает вычисление. Однако передача результата c_str() в функцию, которая не сохраняет и иным образом не передаёт этот указатель, безопасна.

Как и в случае с std::vector и std::array, индексация с помощью operator[] не выполняет проверку границ. Используйте вместо этого функцию-член at(size_type). См. Контейнеры и operator[]. Кроме того, доступ к завершающему нулевому символу с помощью operator[] невозможен. (В некоторых реализациях функция-член c_str() записывает нулевой символ по требованию.)

Никогда не записывайте данные по указателям, возвращаемым data() или c_str(), после удаления const. Если вам нужна изменяемая строка в стиле C, используйте объект std::vector<char> и его функцию-член data(). В этом случае вы должны явно добавить завершающий нулевой символ.

Текущая реализация std::string в GCC основана на подсчёте ссылок. Ожидается, что в будущей версии подсчёт ссылок будет удалён из-за проблем с производительностью и соответствием стандарту. В результате код, который неявно предполагает совместное использование, удерживая указатели или итераторы слишком долго, сломается, что приведёт к сбоям во время выполнения или хуже. С другой стороны, функции, возвращающие неконстантные итераторы, больше не будут давать другим потокам возможность сделать существующие итераторы и указатели недействительными, поскольку инвалидация итераторов больше не зависит от совместного использования внутреннего массива символов.

Контейнеры и operator[]

Многие последовательные контейнеры, подобные std::vector, предоставляют как operator[](size_type), так и функцию-член at(size_type). Это относится к самому std::vector, std::array, std::string и другим экземплярам std::basic_string.

operator[](size_type) не требуется стандартом для выполнения проверки границ (и реализация в GCC не выполняет). В отличие от этого, at(size_type) должна выполнять такую проверку. Поэтому в коде, который не является критичным для производительности, следует предпочитать at(size_type) вместо operator[](size_type), даже если это немного более многословно.

Функции-члены front() и back() не определены, если вектор пуст. Вы можете использовать vec.at(0) и vec.at(vec.size() - 1) в качестве проверяемых замен. Для пустого вектора data() определена; она возвращает произвольный указатель, но не обязательно нулевой.

Итераторы

Итераторы не выполняют никакой проверки границ. Поэтому все функции, работающие с итераторами, должны принимать их парами, обозначающими диапазон, и гарантировать, что итераторы не выходят за пределы этого диапазона. Для прямых и двунаправленных итераторов необходимо проверять равенство перед перемещением первого или последнего итератора в диапазоне. Для итераторов произвольного доступа необходимо вычислить разность перед добавлением или вычитанием смещения. Невозможно выполнить операцию, а затем проверить наличие недопустимого оператора.

Выходные итераторы нельзя сравнивать на равенство. Поэтому невозможно написать код, который определяет, что ему предоставлена слишком маленькая выходная область, и их использование следует избегать.

Эти проблемы делают некоторые функции стандартной библиотеки сложными для правильного использования, см. Непарные итераторы.