Используйте стандартную библиотеку типобезопасным способом
Причина
Потому что, очевидно, нарушение этого правила может привести к неопределённому поведению, повреждению памяти и всевозможным другим серьёзным ошибкам.
Примечание
Это полуфилософское мета-правило, которому требуется множество конкретных вспомогательных правил. Оно необходимо как зонтичное правило для более частных правил.
Сводка более конкретных правил:
SL.con: Контейнеры
???
Сводка правил для контейнеров:
- SL.con.1: Предпочитайте STL
arrayилиvectorвместо массивов в стиле C - SL.con.2: Предпочитайте STL
vectorпо умолчанию, если нет причины использовать другой контейнер - SL.con.3: Избегайте выхода за пределы диапазона
- SL.con.4: Не используйте
memsetилиmemcpyдля аргументов, не являющихся тривиально копируемыми
SL.con.1: Предпочитайте STL array или vector вместо массивов в стиле C
Причина
Массивы в стиле C менее безопасны и не имеют никаких преимуществ перед array и vector. Для массива фиксированной длины используйте std::array — он не деградирует до указателя при передаче в функцию и знает свой размер. Кроме того, как и встроенный массив, std::array, выделенный на стеке, хранит свои элементы на стеке. Для массива переменной длины используйте std::vector, который дополнительно может изменять свой размер и управляет памятью.
Пример
int v[SIZE]; // ПЛОХО
std::array<int, SIZE> w; // ok
Пример
int* v = new int[initial_size]; // ПЛОХО, владеющий сырой указатель
delete[] v; // ПЛОХО, ручное удаление
std::vector<int> w(initial_size); // ok
Примечание
Используйте gsl::span для невладеющих ссылок на контейнер.
Примечание
Сравнение производительности массива фиксированного размера, выделенного на стеке, с vector, чьи элементы находятся в куче, некорректно. С таким же успехом можно сравнивать std::array на стеке с результатом malloc(), доступным через указатель. Для большинства кода даже разница между выделением на стеке и в куче не имеет значения, тогда как удобство и безопасность vector — имеют. Те, кто пишет код, для которого эта разница важна, вполне способны самостоятельно выбирать между array и vector.
Применение
- Отмечайте объявление массива в стиле C внутри функции или класса, в которых также объявлен STL-контейнер (чтобы не создавать избыточных предупреждений для устаревшего кода без STL). Исправление: как минимум замените массив в стиле C на
std::array.
SL.con.2: Предпочитайте STL vector по умолчанию, если нет причины использовать другой контейнер
Причина
vector и array — единственные стандартные контейнеры, обладающие следующими преимуществами:
- наибыстрейший обобщённый доступ (произвольный доступ, включая дружественность к векторизации);
- наибыстрейший паттерн доступа по умолчанию (обход от начала к концу или от конца к началу удобен для аппаратного предвыборки);
- минимальные накладные расходы по памяти (смежное расположение в памяти означает нулевые расходы на каждый элемент, что хорошо для кэша).
Как правило, вам нужно добавлять и удалять элементы из контейнера, поэтому по умолчанию используйте vector; если изменять размер контейнера не нужно, используйте array.
Даже когда другие контейнеры кажутся более подходящими — например, map для O(log N) поиска или list для эффективной вставки в середину — vector обычно всё равно работает быстрее для контейнеров объёмом до нескольких килобайт.
Примечание
string не следует использовать как контейнер отдельных символов. string — это текстовая строка; если нужен контейнер символов, используйте вместо него vector</*char_type*/> или array</*char_type*/>.
Исключение
Если есть веская причина использовать другой контейнер — используйте его. Например:
- Если
vectorподходит, но вам не нужен контейнер переменного размера, используйтеarray.
- Если вам нужен контейнер с поиском в словарном стиле, гарантирующий O(K) или O(log N) поиск, контейнер будет большим (более нескольких килобайт) и вы часто вставляете элементы так, что расходы на поддержание отсортированного
vectorнеприемлемы — используйтеunordered_mapилиmap.
Примечание
Для инициализации вектора заданным количеством элементов используйте инициализацию через (). Для инициализации вектора списком элементов используйте инициализацию через {}.
vector<int> v1(20); // v1 содержит 20 элементов со значением 0 (vector<int>{})
vector<int> v2 {20}; // v2 содержит 1 элемент со значением 20
Предпочитайте синтаксис инициализации через {}.
Применение
- Отмечайте
vector, размер которого никогда не меняется после конструирования (например, потому что онconstили потому что на нём не вызываются не-constфункции). Исправление: используйте вместо негоarray.
SL.con.3: Избегайте выхода за пределы диапазона
Причина
Чтение или запись за пределы выделенного диапазона элементов, как правило, приводит к серьёзным ошибкам, неверным результатам, сбоям и нарушениям безопасности.
Примечание
Все функции стандартной библиотеки, работающие с диапазонами элементов, имеют (или могут иметь) безопасные по диапазону перегрузки, принимающие span. Стандартные типы, например vector, могут быть изменены для выполнения проверки диапазона в рамках профиля bounds (совместимым образом, например, через добавление контрактов) или использоваться с at().
В идеале гарантия нахождения в пределах диапазона должна обеспечиваться статически. Например:
- цикл range-
forне может выйти за пределы диапазона контейнера, к которому он применяется; v.begin(),v.end()легко определить как безопасный по диапазону.
Такие циклы столь же быстры, как и любые непроверяемые/небезопасные аналоги.
Зачастую простая предварительная проверка позволяет избежать проверки отдельных индексов. Например:
- для
v.begin(),v.begin()+iзначениеiлегко проверить относительноv.size().
Такие циклы могут быть значительно быстрее, чем доступ к элементам с индивидуальной проверкой.
Пример, плохой
void f()
{
array<int, 10> a, b;
memset(a.data(), 0, 10); // ПЛОХО, и содержит ошибку в длине (length = 10 * sizeof(int))
memcmp(a.data(), b.data(), 10); // ПЛОХО, и содержит ошибку в длине (length = 10 * sizeof(int))
}
Кроме того, std::array<>::fill(), std::fill() или даже пустой инициализатор являются лучшими кандидатами, чем memset().
Пример, хороший
void f()
{
array<int, 10> a, b, c{}; // c инициализируется нулями
a.fill(0);
fill(b.begin(), b.end(), 0); // std::fill()
fill(b, 0); // std::ranges::fill()
if ( a == b ) {
// ...
}
}
Пример
Если код использует немодифицированную стандартную библиотеку, существуют обходные пути, позволяющие использовать std::array и std::vector в безопасном по диапазону режиме. Можно вызывать метод .at() для каждого класса — это приведёт к выбрасыванию исключения std::out_of_range. Альтернативно, можно вызывать свободную функцию at(), что при нарушении диапазона приведёт к немедленному завершению (или к настраиваемому действию).
void f(std::vector<int>& v, std::array<int, 12> a, int i)
{
v[0] = a[0]; // ПЛОХО
v.at(0) = a[0]; // OK (альтернатива 1)
at(v, 0) = a[0]; // OK (альтернатива 2)
v.at(0) = a[i]; // ПЛОХО
v.at(0) = a.at(i); // OK (альтернатива 1)
v.at(0) = at(a, i); // OK (альтернатива 2)
}
Применение
??? вставьте ссылку на список запрещённых функций
- Выдавайте диагностику при любом вызове функции стандартной библиотеки, не выполняющей проверку диапазона.
Это правило является частью профиля bounds.
SL.con.4: Не используйте memset или memcpy для аргументов, не являющихся тривиально копируемыми
Причина
Это нарушает семантику объектов (например, перезаписывает vptr).
Примечание
Аналогично для (w)memset, (w)memcpy, (w)memmove и (w)memcmp.
Пример
struct base {
virtual void update() = 0;
};
struct derived : public base {
void update() override {}
};
void f(derived& a, derived& b) // прощайте, таблицы виртуальных функций
{
memset(&a, 0, sizeof(derived));
memcpy(&a, &b, sizeof(derived));
memcmp(&a, &b, sizeof(derived));
}
Вместо этого определите корректные функции инициализации по умолчанию, копирования и сравнения
void g(derived& a, derived& b)
{
a = {}; // инициализация по умолчанию
b = a; // копирование
if (a == b) do_something(a, b);
}
Применение
- Отмечайте использование этих функций для типов, не являющихся тривиально копируемыми.
Примечания TODO:
- Влияние на стандартную библиотеку потребует тесной координации с WG21, хотя бы для обеспечения совместимости, даже если это никогда не будет стандартизировано.
- Рассматривается возможность определения безопасных по диапазону перегрузок для функций stdlib (особенно C stdlib), таких как
memcmp, и их включения в GSL. - Для существующих функций и типов stdlib, например
vector, которые не имеют полной проверки диапазона, цель состоит в том, чтобы эти функции выполняли проверку диапазона при вызове из кода с включённым профилем bounds и не выполняли её при вызове из унаследованного кода — возможно, с использованием контрактов (которые в настоящее время предлагаются несколькими членами WG21).
SL.str: Строки
Обработка текста — огромная тема. std::string охватывает её не полностью. Этот раздел в первую очередь призван прояснить отношение std::string к char*, zstring, string_view и gsl::span<char>. Важный вопрос о наборах символов не из ASCII и кодировках (например, wchar_t, Unicode и UTF-8) будет рассмотрен в другом месте.
См. также: регулярные выражения
Здесь мы используем «последовательность символов» или «строка» для обозначения последовательности символов, предназначенной для чтения как текст (каким-либо образом, в конечном счёте). Мы не рассматриваем ???
Сводка правил для строк:
- SL.str.1: Используйте
std::stringдля владения последовательностями символов - SL.str.2: Используйте
std::string_viewилиgsl::span<char>для ссылки на последовательности символов - SL.str.3: Используйте
zstringилиczstringдля ссылки на последовательность символов в стиле C, завершённую нулём - SL.str.4: Используйте
char*для обозначения одного символа - SL.str.5: Используйте
std::byteдля обозначения байтовых значений, которые не обязательно представляют символы
- SL.str.10: Используйте
std::string, когда нужно выполнять зависящие от локали операции со строками - SL.str.11: Используйте
gsl::span<char>вместоstd::string_view, когда нужно изменять строку - SL.str.12: Используйте суффикс
sдля строковых литералов, предназначенных для использования какstringстандартной библиотеки
См. также:
SL.str.1: Используйте std::string для владения последовательностями символов
Причина
string корректно управляет выделением памяти, владением, копированием, постепенным расширением и предоставляет разнообразные полезные операции.
Пример
vector<string> read_until(const string& terminator)
{
vector<string> res;
for (string s; cin >> s && s != terminator; ) // читаем слово
res.push_back(s);
return res;
}
Обратите внимание, что для string определены >> и != (в качестве примеров полезных операций) и нет явных выделений, освобождений памяти или проверок диапазона (string берёт всё это на себя).
В C++17 можно использовать string_view в качестве аргумента вместо const string&, что даёт большую гибкость вызывающей стороне:
vector<string> read_until(string_view terminator) // C++17
{
vector<string> res;
for (string s; cin >> s && s != terminator; ) // читаем слово
res.push_back(s);
return res;
}
Пример, плохой
Не используйте строки в стиле C для операций, требующих нетривиального управления памятью
char* cat(const char* s1, const char* s2) // осторожно!
// return s1 + '.' + s2
{
int l1 = strlen(s1);
int l2 = strlen(s2);
char* p = (char*) malloc(l1 + l2 + 2);
strcpy(p, s1, l1);
p[l1] = '.';
strcpy(p + l1 + 1, s2, l2);
p[l1 + l2 + 1] = 0;
return p;
}
Мы правильно всё сделали? Вспомнит ли вызывающий код вызвать free() для возвращённого указателя? Пройдёт ли этот код проверку безопасности?
Примечание
Не думайте, что string медленнее низкоуровневых техник, не проведя измерений — помните, что не весь код критичен по производительности. Не оптимизируйте преждевременно
Применение
???
SL.str.2: Используйте std::string_view или gsl::span для ссылки на последовательности символов
Причина
std::string_view или gsl::span<char> обеспечивают простой и (потенциально) безопасный доступ к последовательностям символов независимо от того, как эти последовательности выделены и хранятся.
Пример
vector<string> read_until(string_view terminator);
void user(zstring p, const string& s, string_view ss)
{
auto v1 = read_until(p);
auto v2 = read_until(s);
auto v3 = read_until(ss);
// ...
}
Примечание
std::string_view (C++17) доступен только для чтения.
Применение
???
SL.str.3: Используйте zstring или czstring для ссылки на последовательность символов в стиле C, завершённую нулём
Причина
Читаемость. Выражение намерения. Простой char* может быть указателем на один символ, указателем на массив символов, указателем на строку в стиле C (завершённую нулём) или даже на небольшое целое число. Различение этих альтернатив предотвращает недопонимание и ошибки.
Пример
void f1(const char* s); // s, вероятно, строка
Всё, что мы знаем — это то, что аргумент должен быть nullptr или указывать хотя бы на один символ
void f1(zstring s); // s — строка в стиле C или nullptr
void f1(czstring s); // s — константная строка в стиле C или nullptr
void f1(std::byte* s); // s — указатель на байт (C++17)
Примечание
Не преобразовывайте строку в стиле C в string без причины.
Примечание
Как и любой другой «простой указатель», zstring не должен представлять владение.
Примечание
Существуют миллиарды строк C++ «в мире», большинство из которых использует char* и const char* без документирования намерения. Они используются самыми разными способами, в том числе для представления владения и как обобщённые указатели на память (вместо void*). Разграничить эти способы использования сложно, поэтому следовать этому правилу трудно. Это один из основных источников ошибок в программах на C и C++, поэтому следовать этому правилу везде, где это осуществимо, стоит.
Применение
- Отмечайте использование
[]сchar* - Отмечайте использование
deleteсchar* - Отмечайте использование
free()сchar*
SL.str.4: Используйте char* для обозначения одного символа
Причина
Многообразие способов использования char* в современном коде является основным источником ошибок.
Пример, плохой
char arr[] = {'a', 'b', 'c'};
void print(const char* p)
{
cout << p << '\n';
}
void use()
{
print(arr); // ошибка времени выполнения; потенциально очень опасно
}
Массив arr не является строкой в стиле C, поскольку он не завершён нулём.
Альтернатива
См. zstring, string и string_view.
Применение
- Отмечайте использование
[]сchar*
SL.str.5: Используйте std::byte для обозначения байтовых значений, которые не обязательно представляют символы
Причина
Использование char* для представления указателя на нечто, не обязательно являющееся символом, приводит к путанице и блокирует полезные оптимизации.
Пример
???
Примечание
C++17
Применение
???
SL.str.10: Используйте std::string, когда нужно выполнять зависящие от локали операции со строками
Причина
std::string поддерживает средства работы с locale стандартной библиотеки.
Пример
???
Примечание
???
Применение
???
SL.str.11: Используйте gsl::span вместо std::string_view, когда нужно изменять строку
Причина
std::string_view доступен только для чтения.
Пример
???
Примечание
???
Применение
Компилятор будет отмечать попытки записи в string_view.
SL.str.12: Используйте суффикс s для строковых литералов, предназначенных для использования как string стандартной библиотеки
Причина
Прямое выражение идеи минимизирует ошибки.
Пример
auto pp1 = make_pair("Tokyo", 9.00); // {строка в стиле C, double} — это задумано?
pair<string, double> pp2 = {"Tokyo", 9.00}; // немного многословно
auto pp3 = make_pair("Tokyo"s, 9.00); // {std::string, double} // C++14
pair pp4 = {"Tokyo"s, 9.00}; // {std::string, double} // C++17
Применение
???
SL.io: Iostream
iostream — это типобезопасная, расширяемая библиотека форматированного и неформатированного ввода-вывода для потокового I/O. Она поддерживает множество (в том числе расширяемых пользователем) стратегий буферизации и несколько локалей. Её можно использовать для обычного ввода-вывода, чтения и записи в память (потоки строк) и пользовательских расширений, например потоковой передачи данных по сети (asio: ещё не стандартизировано).
Сводка правил для Iostream:
- SL.io.1: Используйте ввод на уровне символов только в случае необходимости
- SL.io.2: При чтении всегда учитывайте некорректный ввод
- SL.io.3: Предпочитайте iostream для ввода-вывода
- SL.io.10: Если вы не используете функции семейства
printf, вызывайтеios_base::sync_with_stdio(false) - SL.io.50: Избегайте
endl - ???
SL.io.1: Используйте ввод на уровне символов только в случае необходимости
Причина
Если только вы действительно не работаете с отдельными символами, использование ввода на уровне символов приводит к тому, что пользовательский код выполняет потенциально подверженную ошибкам и потенциально неэффективную сборку токенов из символов.
Пример
char c;
char buf[128];
int i = 0;
while (cin.get(c) && !isspace(c) && i < 128)
buf[i++] = c;
if (i == 128) {
// ... обработка слишком длинной строки ....
}
Лучше (гораздо проще и, вероятно, быстрее):
string s;
s.reserve(128);
cin >> s;
при этом reserve(128), скорее всего, и не нужен.
Применение
???
SL.io.2: При чтении всегда учитывайте некорректный ввод
Причина
Ошибки обычно лучше всего обрабатывать как можно раньше. Если ввод не проверяется, каждая функция должна уметь справляться с некорректными данными (что нереалистично).
Пример
???
Применение
???
SL.io.3: Предпочитайте iostream для ввода-вывода
Причина
iostream безопасен, гибок и расширяем.
Пример
// записать комплексное число:
complex<double> z{ 3, 4 };
cout << z << '\n';
complex — это пользовательский тип, и его ввод-вывод определён без изменения библиотеки iostream.
Пример
// читать файл комплексных чисел:
for (complex<double> z; cin >> z; )
v.push_back(z);
Исключение
??? производительность ???
Обсуждение: iostream vs. семейство printf()
Часто (и зачастую обоснованно) указывается, что семейство printf() имеет два преимущества по сравнению с iostream: гибкость форматирования и производительность. Это необходимо взвешивать в сравнении с преимуществами iostream: расширяемостью для пользовательских типов, устойчивостью к нарушениям безопасности, автоматическим управлением памятью и поддержкой locale.
Если вам нужна производительность ввода-вывода, вы почти всегда можете добиться лучших результатов, чем с printf().
gets(), scanf() с %s и printf() с %s являются угрозой безопасности (уязвимы к переполнению буфера и в целом подвержены ошибкам). C11 определяет некоторые «необязательные расширения», выполняющие дополнительную проверку своих аргументов. При наличии в вашей C-библиотеке gets_s(), scanf_s() и printf_s() могут быть более безопасными альтернативами, однако они всё равно не являются типобезопасными.
Применение
При желании отмечайте <cstdio> и <stdio.h>.
SL.io.10: Если вы не используете функции семейства printf, вызывайте ios_base::sync_with_stdio(false)
Причина
Синхронизация iostream с вводом-выводом в стиле printf может быть дорогостоящей. cin и cout по умолчанию синхронизированы с printf.
Пример
int main()
{
ios_base::sync_with_stdio(false);
// ... используем iostream ...
}
Применение
???
SL.io.50: Избегайте endl
Причина
Манипулятор endl в основном эквивалентен '\n' и "\n"; в типичном применении он лишь замедляет вывод, выполняя лишние вызовы flush(). Это замедление может быть значительным по сравнению с выводом в стиле printf.
Пример
cout << "Hello, World!" << endl; // две операции вывода и сброс буфера
cout << "Hello, World!\n"; // одна операция вывода без сброса буфера
Примечание
При взаимодействии через cin/cout (и аналоги) нет необходимости в явном сбросе буфера — он выполняется автоматически. При записи в файл потребность в flush возникает редко.
Примечание
Для строковых потоков (в частности, ostringstream) вставка endl полностью эквивалентна вставке символа '\n', но и в этом случае endl может работать значительно медленнее.
endl не обеспечивает вывод платформо-зависимой последовательности конца строки (например, "\r\n" в Windows). Поэтому для строкового потока s << endl вставляет лишь один символ: '\n'.
Примечание
Помимо (иногда важного) вопроса производительности, выбор между '\n' и endl практически полностью является делом вкуса.
SL.regex: Регулярные выражения
<regex> — это стандартная библиотека регулярных выражений C++. Она поддерживает разнообразные соглашения о синтаксисе паттернов регулярных выражений.
SL.chrono: Время
<chrono> (определён в пространстве имён std::chrono) предоставляет понятия time_point и duration, а также функции для вывода времени в различных единицах. Он предоставляет часы для регистрации time_point.
SL.C: Стандартная библиотека C
???
Сводка правил для стандартной библиотеки C:
SL.C.1: Не используйте setjmp/longjmp
Причина
longjmp игнорирует деструкторы, тем самым аннулируя все стратегии управления ресурсами, основанные на RAII.
Применение
Отмечайте все вхождения longjmp и setjmp.
A: Архитектурные идеи
Этот раздел содержит идеи о высокоуровневых архитектурных концепциях и библиотеках.
Сводка архитектурных правил: