mirror of
https://github.com/Nekrolm/ubbook.git
synced 2025-12-16 20:37:03 +03:00
fix typos
This commit is contained in:
@@ -54,7 +54,7 @@
|
||||
5. [std::enable_if/std::void_t](syntax/enable_if_void_t.md)
|
||||
6. [Потерянный return](syntax/missing_return.md)
|
||||
7. [Эллипсис и функции с произвольным числом аргументов](syntax/c_variadic.md)
|
||||
8. [`operator[] ` ассоциативных котейнеров](syntax/map_subscript.md)
|
||||
8. [`operator[] ` ассоциативных контейнеров](syntax/map_subscript.md)
|
||||
9. [потоки ввода/вывода](syntax/iostreams.md)
|
||||
10. [`operator ,`](syntax/comma_operator.md)
|
||||
11. [function-try-block](syntax/function-try-catch.md)
|
||||
|
||||
@@ -34,7 +34,7 @@ void task2() {
|
||||
event_happened = true;
|
||||
}
|
||||
// Обратите внимание: вызов notify не обязан быть под захваченной блокировкой.
|
||||
// Однако, в ранних версиях msvc, а тажке в очень старой версии из boost были
|
||||
// Однако, в ранних версиях msvc, а также в очень старой версии из boost были
|
||||
// баги, требующие удерживать мьютекс захваченным во время вызова notify()
|
||||
// Но есть случай, когда делать вызов notify под блокировкой необходимо — если
|
||||
// другой тред может вызвать, например, завершаясь, деструктор объекта cv
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
Увы, так не работает. Правильность использования этих инструментов в C++ нужно контроллировать самостоятельно, пристально изучая каждую строчку кода.
|
||||
Мы все равно втыкаемся в проблемы синхронизации доступа, с аккуратным развешиванием мьютексов и атомарных переменных.
|
||||
|
||||
Ситуация (_race condition_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее премешаное значенее. Независимо от языка программирования.
|
||||
Ситуация (_race condition_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее премешаное значение. Независимо от языка программирования.
|
||||
|
||||
Но в C++ это не просто ошибка. Это неопределенное поведение. И «возможности» для оптимизации
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
----
|
||||
|
||||
Компиляторы `Clang` и `GCC` с влюченными флагами `-Wall -Wpedantic` [способны](https://godbolt.org/z/zM4r1s) находить некоторые ошибки.
|
||||
Компиляторы `Clang` и `GCC` с включенными флагами `-Wall -Wpedantic` [способны](https://godbolt.org/z/zM4r1s) находить некоторые ошибки.
|
||||
|
||||
----
|
||||
|
||||
@@ -65,7 +65,7 @@ static_assert((div_test(A, B), true)); // Compliation error, zero division
|
||||
----
|
||||
Тесты, различные сборки, статический и динамический анализ — способы немного поднять уверенность в том, что в вашем коде нет UB. Дать же точную гарантию может только коллегия экспертов, которые будут сверять каждую строчку кода с буквой стандарта и трижды друг друга перепроверять. И даже этого может быть недостаточно.
|
||||
|
||||
Еще есть путь отключения каких-либо оптимизаций флагами компилятора, флаги включащие различные нарушения стандарта (знаменитый `-fpermissive`), превращающие язык C++ во что-то совершенно иное. Но призываю вас никогда не идти этим путем. Ваш код станет непереносимым. Ваш код перестанет быть кодом на C++. Лучше сразу возьмите другой язык программирования.
|
||||
Еще есть путь отключения каких-либо оптимизаций флагами компилятора, флаги включающие различные нарушения стандарта (знаменитый `-fpermissive`), превращающие язык C++ во что-то совершенно иное. Но призываю вас никогда не идти этим путем. Ваш код станет непереносимым. Ваш код перестанет быть кодом на C++. Лучше сразу возьмите другой язык программирования.
|
||||
|
||||
### Полезные ссылки
|
||||
1. https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html
|
||||
|
||||
@@ -31,7 +31,7 @@ const int& y = std::max([](const int& a, const int& b) -> const int& {
|
||||
// Со ссылками на 4, 5 успешно отрабатывает std::max —
|
||||
// их время жизни еще не закончилось. Ссылки валидны.
|
||||
|
||||
// Ссылка-результат присваевается `y`. Продлений жизни не происходит —
|
||||
// Ссылка-результат присваивается `y`. Продлений жизни не происходит —
|
||||
// все временные объекты уже безуспешно попытали счастья на аргументах функций.
|
||||
// Дело доходит до ; Время жизни всех объектов 1,2,3,4,5 заканчивается.
|
||||
// `y` становится висячей. Занавес.
|
||||
@@ -118,7 +118,7 @@ auto&& container_ = MakeShape().vertexes;
|
||||
// Подобъект vertexes — считается таким же временным.
|
||||
// Его время жизни закончится на ;
|
||||
// Но он встречает && ссылку, область видимости которой простирается ниже
|
||||
// и продлевает ему жизнь. Причем придлевается жизнь не только лишь подобъекту
|
||||
// и продлевает ему жизнь. Причем продлевается жизнь не только лишь подобъекту
|
||||
// vertexes, а целиком временному объекту Shape, его содержащему
|
||||
```
|
||||
|
||||
@@ -127,7 +127,7 @@ auto&& container_ = MakeShape().vertexes;
|
||||
auto&& container_ = MakeShape().Vertexes();
|
||||
// временный объект Shape живет до ;. Но он встречает неявную const&
|
||||
// ссылку в методе Vertexes(). Ее область видимости ограничена телом метода.
|
||||
// Продления жини не происходит. Возвращается ссылка на часть временного объекта
|
||||
// Продления жизни не происходит. Возвращается ссылка на часть временного объекта
|
||||
// и присваивается ссылке `container_`.
|
||||
// Дело доходит до ;. Временный Shape умирает.
|
||||
// `container_` становится висячей ссылкой. Занавес.
|
||||
@@ -135,7 +135,7 @@ auto&& container_ = MakeShape().Vertexes();
|
||||
|
||||
Вот так все просто и сломано.
|
||||
|
||||
Кстати говоря: механизм продления жизни объекту с помощью ссылки на его подобъект — очень неочевидная штука. И, если, например, ваш код полагся на какие-то эффекты в деструкторах, можно получить не совсем то, [чего хотите](https://godbolt.org/z/9M946o).
|
||||
Кстати говоря: механизм продления жизни объекту с помощью ссылки на его подобъект — очень неочевидная штука. И, если, например, ваш код полагается на какие-то эффекты в деструкторах, можно получить не совсем то, [чего хотите](https://godbolt.org/z/9M946o).
|
||||
|
||||
---
|
||||
|
||||
@@ -152,7 +152,7 @@ for (auto cont = expr; auto x : cont)
|
||||
- Использовать `std::ranges::for_each`
|
||||
- Не использовать range-based for в C++, пока его не починят
|
||||
|
||||
## Полезыне ссылки
|
||||
## Полезные ссылки
|
||||
1. https://en.cppreference.com/w/cpp/algorithm/ranges/for_each
|
||||
2. https://en.cppreference.com/w/cpp/language/range-for
|
||||
3. https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary
|
||||
|
||||
@@ -64,7 +64,7 @@ ExtremelyLongClassName y {
|
||||
}()
|
||||
};
|
||||
|
||||
ExtremelyLongСlassName z {
|
||||
ExtremelyLongClassName z {
|
||||
[] ()-> decltype(z.Default()) { // Ok, well-defined
|
||||
// сложные вычисления
|
||||
return 1;
|
||||
|
||||
@@ -28,7 +28,7 @@ int count_char(const char* s, char c) {
|
||||
}
|
||||
```
|
||||
|
||||
И будем либо дублировать код, немножно адапритуя его под C-строки, либо сделаем функцию
|
||||
И будем либо дублировать код, немного адаптируя его под C-строки, либо сделаем функцию
|
||||
|
||||
```C++
|
||||
int count_char_impl(const char* s, size_t len, char c) {
|
||||
@@ -97,7 +97,7 @@ int main() {
|
||||
}
|
||||
```
|
||||
|
||||
Сутация такая же, как с [ранее рассмотренным](use_after_free_in_general.md) `std::min`.
|
||||
Ситуация такая же, как с [ранее рассмотренным](use_after_free_in_general.md) `std::min`.
|
||||
Только защититься от такой функции `common_prefix`, обернув ее в шаблон с помощью анализа rvalue/lvalue,
|
||||
намного сложнее: нам нужно разобрать случаи `const char*` и `std::string` для каждого аргумента — в общем, все то, от чего нас введение `std::string_view` «избавило».
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Объект жил на стеке и умер. Или объект жил в куче и умер. Разница по сути не очень большая: обобщенный сценарий воспроизведения ошибки один и тот же — где-то остались указатель или ссылка на уже мертвый объект. А потом этой ссылкой (или указателем) воспользовались, чтобы обратиться к мертвому объекту. Такой спиритический сеанс заканчивается неопределенным поведением. Если повезет — будет ошибка сегментации с возможностью узнать, кто именно обратился.
|
||||
|
||||
Но все же между мервым объектом со стека или мервым объектом из кучи есть разница в возможности обнаружения методами динамического анализа:
|
||||
Но все же между первым объектом со стека или первым объектом из кучи есть разница в возможности обнаружения методами динамического анализа:
|
||||
Для инструментации стека санитайзерами вообще говоря нужно перекомпилировать программу. Для инструментации кучи — можно подменить библиотеку с аллокатором.
|
||||
|
||||
------
|
||||
|
||||
@@ -26,10 +26,10 @@ void run_actions(std::vector<Action> actions) {
|
||||
```
|
||||
Красиво, коротко, с неопределенным поведением и неправильно.
|
||||
|
||||
- `push_back` может вызвать реаллокацию вектора. Итераторы begin/end ивалидируются — цикл продолжится по уничтоженным данным.
|
||||
- `push_back` может вызвать реаллокацию вектора. Итераторы begin/end инвалидируются — цикл продолжится по уничтоженным данным.
|
||||
- Если реаллокации не произойдет, цикл пройдет только по тому набору элементов, что были в векторе изначально. До добавленных в процессе дело не дойдет.
|
||||
|
||||
Корректый код:
|
||||
Корректный код:
|
||||
```C++
|
||||
void run_actions(std::vector<Action> actions) {
|
||||
for (size_t idx = 0; idx < actions.size(); ++idx) {
|
||||
|
||||
@@ -31,7 +31,7 @@ if (x > 0 && a > 0 && x + a <= 0) {
|
||||
|
||||
Но, увы, это неопределенное поведение. И компилятор имеет полное право [выкинуть](https://godbolt.org/z/dhs83T) такую проверку.
|
||||
|
||||
Искусственный пример может быть недостаточно убедительным, так что обратим внимание на следущую, вполне серьезную, функцию вычисления полиномиального хэша строки:
|
||||
Искусственный пример может быть недостаточно убедительным, так что обратим внимание на следующую, вполне серьезную, функцию вычисления полиномиального хэша строки:
|
||||
```C++
|
||||
int hash_code(std::string s) {
|
||||
int h = 13;
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
И еще многие другие, преимущественно работающие со строками, функции.
|
||||
|
||||
Эти функции доставляли и продолжают доставлять проблемы. Некоторые компиляторы (msvc) по умолчанию откажутся собирать ваш код, если увидят одну из них. Другие будут менее заботливыми и, возможно, выдадут предупреждение. По крайней мере про функцию `gets` уж точно. Если с другими фунциями у программиста есть возможность уберечься (проверка до вызова; у `scanf` можно указать размер в ограничение строке), то с `gets` — без вариантов.
|
||||
Эти функции доставляли и продолжают доставлять проблемы. Некоторые компиляторы (msvc) по умолчанию откажутся собирать ваш код, если увидят одну из них. Другие будут менее заботливыми и, возможно, выдадут предупреждение. По крайней мере про функцию `gets` уж точно. Если с другими функциями у программиста есть возможность уберечься (проверка до вызова; у `scanf` можно указать размер в ограничение строке), то с `gets` — без вариантов.
|
||||
|
||||
Для большинства старых небезопасных сишных функций сейчас есть «безопасные» аналоги с размерами буферов. Часть из них не стандартизирована, часть стандартизирована. Все это породило огромное количество костылей с макроподстановками для работы со всем этим зоопарком. Но сейчас не об этом.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Бесконечные циклы и проблема останова
|
||||
|
||||
Определить, завершается или не завершается программа на конкретном наборе данных — алгоритимически невозможно в общем случае.
|
||||
Определить, завершается или не завершается программа на конкретном наборе данных — алгоритмически невозможно в общем случае.
|
||||
|
||||
Но в стандартах C и C++ зачем-то сказано, что валидная программа должна либо гарантированно завершаться, либо гарантированно производить обозреваемые эффекты: запрашивать ввод-вывод, взаимодействовать с `volatile` переменными и подобное. А иначе поведение программы неопределенное. Так что «правильные» компиляторы C++ настолько суровы, что способны решать алгоритмически неразрешимые задачи.
|
||||
|
||||
@@ -40,7 +40,7 @@ int main () {
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
Комилятор увидел, что единственный выход из цикла — `return 1`. У цикла нет никаких видимых эффектов. Так что компилятор просто заменил его на `return 1`
|
||||
Компилятор увидел, что единственный выход из цикла — `return 1`. У цикла нет никаких видимых эффектов. Так что компилятор просто заменил его на `return 1`
|
||||
|
||||
Если же попытаться узнать, что за тройку «нашла» программа — цикл вернется.
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ private:
|
||||
}
|
||||
```
|
||||
|
||||
Эти функции в настоящеее время ничего не делают. Они нужны только для формального следования букве стандарта.
|
||||
Эти функции в настоящее время ничего не делают. Они нужны только для формального следования букве стандарта.
|
||||
|
||||
Если вы верите, что когда-нибудь в C++ появится сборщик мусора, будьте любезны пользоваться этими прекрасными функциями, чтобы ваша программа оставалась корректной и в далеком будущем.
|
||||
|
||||
@@ -107,10 +107,10 @@ private:
|
||||
|
||||
--------
|
||||
|
||||
Надо понимать, что сам по себе сборщик мусора для C++ не является чем-то сверхъестественным. На C/C++ написаны, например, сборщики мусора для JVM. Никто не мещает задействовать их же в C++-программах: просто используем альтернативные функции для выделения памяти. С их помощью даже можно переопределить поведение операторов `new` и `delete`. Но очень мало какой код на C++ пишется в предположении, что под этими операторами работает сборщик мусора.
|
||||
Надо понимать, что сам по себе сборщик мусора для C++ не является чем-то сверхъестественным. На C/C++ написаны, например, сборщики мусора для JVM. Никто не мешает задействовать их же в C++-программах: просто используем альтернативные функции для выделения памяти. С их помощью даже можно переопределить поведение операторов `new` и `delete`. Но очень мало какой код на C++ пишется в предположении, что под этими операторами работает сборщик мусора.
|
||||
|
||||
Проверить, не запустили ли вашу программу в светлом мире со сборщиком мусора, можно вызвав функцию `get_pointer_safety`. Она возвращает одно из трех значений:
|
||||
- `pointer_safety::strict` — играть с восставновлением указателей абы откуда просто так нельзя; сборщик мусора, возможно, работает.
|
||||
- `pointer_safety::strict` — играть с восстановлением указателей абы откуда просто так нельзя; сборщик мусора, возможно, работает.
|
||||
- `pointer_safety::relaxed` — с указателями нет никаких проблем, выделенная память никуда сама по себе не денется.
|
||||
- `pointer_safety::preferred` — с указателями нет никаких проблем, выделенная память никуда сама по себе не денется, но, возможно, работает детектор утечек, которому важны пометки `declare_reachable`/`undeclare_reachable`.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Разыменование нулевых указателей.
|
||||
|
||||
Самая крутая ошибка с самыми жуткими последствиями. `null` вообще называют [ошибкой на миллиард долларов](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/).
|
||||
От них страдает куча кода, на самых разных языках программирования. Но если в условной `Java` при обращении по `null`-ссылке вы получите исключение с вполне предсказуемыми последствиями (ну, упало и упало), то в великом и ужасном C++, а также в C за вами придет неопределенное поведение. И оно будет дествительно неопределенным!
|
||||
От них страдает куча кода, на самых разных языках программирования. Но если в условной `Java` при обращении по `null`-ссылке вы получите исключение с вполне предсказуемыми последствиями (ну, упало и упало), то в великом и ужасном C++, а также в C за вами придет неопределенное поведение. И оно будет действительно неопределенным!
|
||||
|
||||
Но для начала, конечно, надо отметить, что, после всех обсуждений туманных формулировок стандарта, в настоящее время есть некоторое [соглашение](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#315), что все-таки не сама по себе конструкция `*p`, где `p` — нулевой указатель, вызывает неопределенное поведение. А `lvalue-to-rvalue` преобразование. Ну или менее формально, кратко и не совсем правильно: пока нет чтения или записи значения по этому самому нулевому адресу — все нормально.
|
||||
|
||||
@@ -133,7 +133,7 @@ int main(int argc, char **argv) {
|
||||
На одних и тех же входных данных (вернее, их отсутствии), этот код завершается с [разными результатами](https://godbolt.org/z/zc4xGz)
|
||||
в зависимости от компилятора и уровня оптимизаций.
|
||||
|
||||
Если вы еще недостаточно напуганы, то вот еще замечательная [история](https://devblogs.microsoft.com/oldnewthing/?p=97635) о том, как весело и задорно падала фунция вида
|
||||
Если вы еще недостаточно напуганы, то вот еще замечательная [история](https://devblogs.microsoft.com/oldnewthing/?p=97635) о том, как весело и задорно падала функция вида
|
||||
|
||||
```C++
|
||||
void refresh(int* frameCount)
|
||||
|
||||
@@ -67,7 +67,7 @@ int fun() { // CE: redefinition
|
||||
|
||||
Что же делать?
|
||||
|
||||
В мире чистого C с этим борятся комплексом методов:
|
||||
В мире чистого C с этим борются комплексом методов:
|
||||
|
||||
1. Ручной имплементацией механизма пространств имен — каждой функции и структуре в проекте дописывают префиксом имя проекта.
|
||||
2. Настраивают видимость символов:
|
||||
|
||||
@@ -19,7 +19,7 @@ struct Node {
|
||||
};
|
||||
```
|
||||
|
||||
Такая структура совешенно законна для определения дерева, [компилируется и работает](https://godbolt.org/z/evecMd). И может быть удобнее, чем вариант с умными указателями.
|
||||
Такая структура совершенно законна для определения дерева, [компилируется и работает](https://godbolt.org/z/evecMd). И может быть удобнее, чем вариант с умными указателями.
|
||||
|
||||
Нам не нужно никак вручную управлять ресурсами, вектор позаботится обо всем самостоятельно. Пользуемся «правилом нуля» и не пишем ни деструктор, ни конструктора копирования, ни оператора перемещения/копирования, ничего — красота!
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ struct Numbers {
|
||||
```
|
||||
Правда, семантика `operator !=` стала странной. Да и нужно `end()` из чего-то конструировать. Если стейт нашего генератора будет более сложным, например, выделяющим что-то на куче, мы получим дополнительные накладные расходы. Не очень zero-cost.
|
||||
|
||||
Поэтому в C++17 `range-based-for` исправили. Теперь он может [работать](https://godbolt.org/z/7vYxc4K6q) с граничиными итераторами разных типов.
|
||||
Поэтому в C++17 `range-based-for` исправили. Теперь он может [работать](https://godbolt.org/z/7vYxc4K6q) с граничными итераторами разных типов.
|
||||
|
||||
Но STL-алгоритмы все также [не работают](https://godbolt.org/z/MddGYWMdq).
|
||||
```C++
|
||||
|
||||
@@ -37,7 +37,7 @@ public:
|
||||
В конструкторах и деструкторах в C++ виртуальная диспетчеризация методов не работает (В других языках — например, в C# или Java — наоборот, что доставляет свои проблемы).
|
||||
|
||||
Почему так? При конструировании часть объекта-наследника, используемая в переопределенном методе, может быть еще не создана: конструкторы вызываются в порядке от базового класса к производному.
|
||||
При деструктурировании наоборот — часть обекта-наследника уже уничтожена, и если позволить динамический вызов, можно легко получить use-after-free.
|
||||
При деструктурировании наоборот — часть объекта-наследника уже уничтожена, и если позволить динамический вызов, можно легко получить use-after-free.
|
||||
|
||||
Радуйтесь! Это одно из немногих мест в C++, где вас защитили от неопределенного поведения со временами жизни!
|
||||
|
||||
@@ -74,7 +74,7 @@ void Processor::start() {
|
||||
|
||||
Компиляторы уже [не выдают](https://godbolt.org/z/66fG6cjnd) замечательного предупреждения и подвох заметить стало сложнее. А ведь мы повысили уровень индирекции всего на один! А что будет если код нашего базового класса окажется сложнее?.. Наследование имплементаций — источник многих проблем, прячущихся за невинным желанием переиспользовать код.
|
||||
|
||||
Вызов виртуальных функций класса в его конструкторах и деструкторах почти всегдя является ошибкой сейчас или в будущем.
|
||||
Вызов виртуальных функций класса в его конструкторах и деструкторах почти всегда является ошибкой сейчас или в будущем.
|
||||
Если же это не ошибка и так и задумывалось, то стоит использовать явный статический вызов с указанием имени класса (name qualified call).
|
||||
|
||||
```C++
|
||||
|
||||
@@ -155,14 +155,14 @@ int count_if(const std::vector<int>& v, predicate p) {
|
||||
Вот менее тривиальный пример `const`, никак не способствующего оптимизации:
|
||||
```C++
|
||||
void find_last_zero_pos(const std::vector<int>& v,
|
||||
const int* *poiner_to_last_zero) {
|
||||
*poiner_to_last_zero = 0;
|
||||
const int* *pointer_to_last_zero) {
|
||||
*pointer_to_last_zero = 0;
|
||||
for (size_t i = 0; i < v.size(); ++i) { // мы опять не можем один раз
|
||||
// сохранить значение v.size()
|
||||
if (v[i] == 0) {
|
||||
// внутри вектора есть поля типа int* — begin, end
|
||||
// что если pointer_to_last_zero указывает на один из них?!?
|
||||
*poiner_to_last_zero = (v.data() + i);
|
||||
*pointer_to_last_zero = (v.data() + i);
|
||||
}
|
||||
// пересчитываем size!
|
||||
}
|
||||
@@ -257,8 +257,8 @@ std::cout << unit.back().id << "";
|
||||
p->~Unit();
|
||||
```
|
||||
|
||||
Но поддерживать указатель, взвращенный оператором `new`, не всегда возмножно.
|
||||
Он занимает место, его надо хранить, что неэффективно при реализации `optional`: для `int32_t` будет нужно в три раза больше места на 64хбитной системе (4 байта на `storage` + 8 байт на указатель)!
|
||||
Но поддерживать указатель, взвращенный оператором `new`, не всегда возможно.
|
||||
Он занимает место, его надо хранить, что неэффективно при реализации `optional`: для `int32_t` будет нужно в три раза больше места на 64-битной системе (4 байта на `storage` + 8 байт на указатель)!
|
||||
|
||||
Поэтому в стандартной библиотеке с C++17 есть функция «отмывания» невесть откуда взявшихся указателей — `std::launder`.
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ template <class T>
|
||||
using my_void_t = typename my_void<T>::type; // ok
|
||||
```
|
||||
|
||||
А вообще лучше переходить на C++20 и не заниматься всей это ерундой. Там специально для всех этих страшных конструкций читаемый синтактический сахар придумали. Конечно, не без затаившихся граблей, но об этом в другой раз.
|
||||
А вообще лучше переходить на C++20 и не заниматься всей это ерундой. Там специально для всех этих страшных конструкций читаемый синтаксический сахар придумали. Конечно, не без затаившихся граблей, но об этом в другой раз.
|
||||
|
||||
## Полезные ссылки
|
||||
1. https://en.cppreference.com/w/cpp/language/function_template
|
||||
|
||||
@@ -21,7 +21,7 @@ for (Word c : text) {
|
||||
map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1); // поиск трижды!
|
||||
map.put(key, map.getOrDefault(key, 0) + 1); // поиск дважды!
|
||||
```
|
||||
Оно, конечно, может быть, отоптимизируется JIT-комплятором... Но мы в C++ любим гарантии.
|
||||
Оно, конечно, может быть, отоптимизируется JIT-компилятором... Но мы в C++ любим гарантии.
|
||||
|
||||
|
||||
С другой стороны, этот вызов конструктора, если элемент не найден, может выйти боком:
|
||||
|
||||
@@ -65,7 +65,7 @@ struct Worker {
|
||||
|
||||
int main() {
|
||||
// ЭТО НЕ ВЫЗОВ КОНСТРУКТОРА!
|
||||
Worker w(Timer()); // предобъявление функции, которая возврщает Worker и принимает функцию, возвращающую Timer и не принимающую ничего!
|
||||
Worker w(Timer()); // предобъявление функции, которая возвращает Worker и принимает функцию, возвращающую Timer и не принимающую ничего!
|
||||
|
||||
std::cout << w; // имя функции неявно преобразуется к указателю, который неявно преобразуется к bool
|
||||
// будет выведено 1 (true)
|
||||
|
||||
@@ -69,7 +69,7 @@ void test_v2(){
|
||||
Во-вторых, стандарт C++ не специфицирует состояние, в котором должен остаться объект, _из_ которого произвели перемещение.
|
||||
Оно должно быть валидным в смысле вызова деструктора. Но более ничего не требуется. Объект не обязан быть пустым после перемещения. Его поля не обязаны быть зануленными. Так у `std::thread` после перемещения нельзя вызывать ни один из методов. А `std::unique_ptr` гарантированно становится пустым (`nullptr`).
|
||||
|
||||
Чаще всего и проще всего натолкнуться на use-after-move можно при реализцации конструкторов, заполняющих поля переданными аргументами — достаточно дать одинаковые (или почти одинаковые) имена полям и аргументам.
|
||||
Чаще всего и проще всего натолкнуться на use-after-move можно при реализации конструкторов, заполняющих поля переданными аргументами — достаточно дать одинаковые (или почти одинаковые) имена полям и аргументам.
|
||||
|
||||
```C++
|
||||
struct Person {
|
||||
|
||||
@@ -59,7 +59,7 @@ std::vector<int> v1 {3, 2}; // v1 == {3, 2}
|
||||
std::vector<int> v2 (3, 2); // v2 == {2,2,2}
|
||||
```
|
||||
|
||||
А еще у контейнеров есть конструктор, принимающий пару итераторов. И, казалось бы, с ними уж проблем-то не будет, но у нас есть указтели, которые также являются итераторами. А еще есть тип `bool`:
|
||||
А еще у контейнеров есть конструктор, принимающий пару итераторов. И, казалось бы, с ними уж проблем-то не будет, но у нас есть указатели, которые также являются итераторами. А еще есть тип `bool`:
|
||||
|
||||
```C++
|
||||
bool array[5] = {true, false, true, false, true};
|
||||
@@ -69,7 +69,7 @@ std::cout << vector.size() << "\n";
|
||||
|
||||
[Будет выведено](https://gcc.godbolt.org/z/jobeh6) 2, а не 5. Потому что указатели неявно приводятся к `bool`!
|
||||
|
||||
Собственно, эти прекрасные примеры показывают, почему «универсальная» иниациализация не универсальная.
|
||||
Собственно, эти прекрасные примеры показывают, почему «универсальная» инициализация не универсальная.
|
||||
|
||||
Чтобы не множить хаос в своих проектах, нужно быть осторожнее с объявлениями перегруженных конструкторов для своих типов. Лучше ввести статическую функцию, чем создавать перегруженные конструкторы, неожиданно взаимодействующие с неявным приведением типов и списками инициализации.
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ struct Image {
|
||||
};
|
||||
```
|
||||
|
||||
Поле `data` в структуре `Image` имеет [нулевой размер](https://godbolt.org/z/d3xfdj3Ke). Это FAM (flexible array member). Очень удобная штука, чтобы получать доступ к массиву статически не известной длины, размещенному сразу после некоторого заголовка в бинарном буфере. Длина массива обычно указавывается в самом заголовке. FAM может быть только последним полем в структуре.
|
||||
Поле `data` в структуре `Image` имеет [нулевой размер](https://godbolt.org/z/d3xfdj3Ke). Это FAM (flexible array member). Очень удобная штука, чтобы получать доступ к массиву статически не известной длины, размещенному сразу после некоторого заголовка в бинарном буфере. Длина массива обычно указывается в самом заголовке. FAM может быть только последним полем в структуре.
|
||||
|
||||
Стандарт C++ такие фичи не разрешает. Но ведь есть GCC с его нестандартными включенными по умолчанию расширениями.
|
||||
|
||||
@@ -135,7 +135,7 @@ struct S {
|
||||
char data[];
|
||||
};
|
||||
```
|
||||
Чему будет равень размер структуры `S`?
|
||||
Чему будет равен размер структуры `S`?
|
||||
|
||||
В стандартном C пустые структуры в принципе запрещены. И поведение программы с ними не определено. GCC определяет их размер нулевым при компиляции C программ. А при компиляции C++ — размер, как мы выяснили ранее, единичный. Дело пахнет страшными багами и ночными кошмарами при неосторожном проектировании C++ библиотек с сишным интерфейсом или использованием C-библиотек в C++!
|
||||
|
||||
@@ -190,7 +190,7 @@ int add(int x, int y) {
|
||||
в плане генерируемого кода?
|
||||
|
||||
|
||||
Краткий ответ: да. Есть разница. Зависит от конкретной иплементации. Стандарт не гарантирует оптимизацию пустых аргументов.
|
||||
Краткий ответ: да. Есть разница. Зависит от конкретной имплементации. Стандарт не гарантирует оптимизацию пустых аргументов.
|
||||
От перемены позиций тегов может меняться бинарный интерфейс. Поиграться с наиболее заметными изменениями можно на примере [msvc](https://godbolt.org/z/E68ojMb8f).
|
||||
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
Важно, что это «поведение не определено» означает, что произойти может что угодно: форматирование диска, ошибка компиляции, исключение, а может и все будет хорошо. Никаких гарантий не дается. Отсюда и происходят веселые, неожиданне и очень печальные
|
||||
в production-коде последствия.
|
||||
|
||||
И, конечно же, именно C и C++ наболее печально известны своим неопределенным поведением.
|
||||
И, конечно же, именно C и C++ наиболее печально известны своим неопределенным поведением.
|
||||
Однако надо понимать, что эта особенность присуща и другим языкам. Во многих языках можно найти какой-нибудь редкий особенный пример с неопределенным поведением. Но именно в C и C++ оно встречается при написании почти любой программы. Слишком много фич языка содержат пункты с неопределенным поведением.
|
||||
|
||||
----
|
||||
|
||||
И так, по каким же признакам можно заподозрить UB в программе и насколько неопределенное поведение дествительно неопределенное?
|
||||
И так, по каким же признакам можно заподозрить UB в программе и насколько неопределенное поведение действительно неопределенное?
|
||||
|
||||
Когда-то давно UB в коде могло повлечь действительно что угодно. Например, `gcc 1.17` [начинал запускать игрушечки](https://feross.org/gcc-ownage/).
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
1. Для данной конкретной платформы и компилятора в документации сказано что именно произойдет, несмотря на страшные слова «_undefined behavior_» в стандарте. И все будет хорошо. Вы знаете что делаете. Никакой неопределенности. Все классно.
|
||||
2. UB при работе с памятью чаще всего заканчиваются ошибкой сегментации и получением прекрасного сигнала SIGSEGV от операционной системы. Программа падает.
|
||||
3. Программа работает и штатно завершается. Но дает разные или неадекватные результаты от запуска к запуску. Также результаты меняются от сборки к сборке при изменении опций компилятора или самого компилятора. Никаких генераторов случайных чисел вы не использовали.
|
||||
4. Программа ведет себя неправильно, несмотря на то, что в коде наставлено огромное множество проверок, `assert`'ов, `try-catch` блоков, каждый из которых «подтвержает» что все корректно. В отладчике видно, что вычисления идут корректно, но совершенно внезапно все ломается.
|
||||
4. Программа ведет себя неправильно, несмотря на то, что в коде наставлено огромное множество проверок, `assert`'ов, `try-catch` блоков, каждый из которых «подтверждает» что все корректно. В отладчике видно, что вычисления идут корректно, но совершенно внезапно все ломается.
|
||||
5. Программа выполняет код, который в ней есть, но не вызывался. Отрабатывают ни разу не вызываемые функции.
|
||||
6. Компилятор «без причины» и без падаения отказывается собирать код. Линковщик выдает «невозможные и бессмысленные» ошибки.
|
||||
6. Компилятор «без причины» и без падения отказывается собирать код. Линковщик выдает «невозможные и бессмысленные» ошибки.
|
||||
7. Проверки в коде перестают исполняться. Под отладчиком видно, что исполнение не заходит внутрь веток `if` или `catch`, хотя по значениям переменных заход должен быть выполнен.
|
||||
8. Внезапный необоснованный вызов `std::terminate`.
|
||||
9. Бесконечные циклы становятся конечными и наоборот.
|
||||
@@ -39,7 +39,7 @@
|
||||
---
|
||||
|
||||
С неопределенным поведением часто путают два других понятия.
|
||||
1. Еще одна страшная аббревиатура UB — неуточненное (_unspecified_) поведение. Стандарт не уточняет, что именно может произойти, но описывает варианты. Так, например, порядок вычисления аргуметов фунции — поведение неуточненное.
|
||||
1. Еще одна страшная аббревиатура UB — неуточненное (_unspecified_) поведение. Стандарт не уточняет, что именно может произойти, но описывает варианты. Так, например, порядок вычисления аргументов функции — поведение неуточненное.
|
||||
2. Поведение, определяемое реализацией (_implementation-defined_) — надо смотреть документацию для вашей платформы и вашего компилятора.
|
||||
|
||||
Эта парочка намного лучше неопределенного, хотя и имеет с ним одну общую черту: программа, полагающаяся на любое из них, вообще говоря, непереносима.
|
||||
|
||||
Reference in New Issue
Block a user