fix typos

This commit is contained in:
user
2022-03-24 08:42:53 +03:00
parent d492b0140d
commit 2a91e3df13
26 changed files with 49 additions and 49 deletions

View File

@@ -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)

View File

@@ -34,7 +34,7 @@ void task2() {
event_happened = true;
}
// Обратите внимание: вызов notify не обязан быть под захваченной блокировкой.
// Однако, в ранних версиях msvc, а тажке в очень старой версии из boost были
// Однако, в ранних версиях msvc, а также в очень старой версии из boost были
// баги, требующие удерживать мьютекс захваченным во время вызова notify()
// Но есть случай, когда делать вызов notify под блокировкой необходимо — если
// другой тред может вызвать, например, завершаясь, деструктор объекта cv

View File

@@ -9,7 +9,7 @@
Увы, так не работает. Правильность использования этих инструментов в C++ нужно контроллировать самостоятельно, пристально изучая каждую строчку кода.
Мы все равно втыкаемся в проблемы синхронизации доступа, с аккуратным развешиванием мьютексов и атомарных переменных.
Ситуация (_race condition_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее премешаное значенее. Независимо от языка программирования.
Ситуация (_race condition_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее премешаное значение. Независимо от языка программирования.
Но в C++ это не просто ошибка. Это неопределенное поведение. И «возможности» для оптимизации

View File

@@ -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

View File

@@ -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

View File

@@ -64,7 +64,7 @@ ExtremelyLongClassName y {
}()
};
ExtremelyLongСlassName z {
ExtremelyLongClassName z {
[] ()-> decltype(z.Default()) { // Ok, well-defined
// сложные вычисления
return 1;

View File

@@ -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` «избавило».

View File

@@ -4,7 +4,7 @@
Объект жил на стеке и умер. Или объект жил в куче и умер. Разница по сути не очень большая: обобщенный сценарий воспроизведения ошибки один и тот же — где-то остались указатель или ссылка на уже мертвый объект. А потом этой ссылкой (или указателем) воспользовались, чтобы обратиться к мертвому объекту. Такой спиритический сеанс заканчивается неопределенным поведением. Если повезет — будет ошибка сегментации с возможностью узнать, кто именно обратился.
Но все же между мервым объектом со стека или мервым объектом из кучи есть разница в возможности обнаружения методами динамического анализа:
Но все же между первым объектом со стека или первым объектом из кучи есть разница в возможности обнаружения методами динамического анализа:
Для инструментации стека санитайзерами вообще говоря нужно перекомпилировать программу. Для инструментации кучи — можно подменить библиотеку с аллокатором.
------

View File

@@ -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) {

View File

@@ -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;

View File

@@ -12,7 +12,7 @@
И еще многие другие, преимущественно работающие со строками, функции.
Эти функции доставляли и продолжают доставлять проблемы. Некоторые компиляторы (msvc) по умолчанию откажутся собирать ваш код, если увидят одну из них. Другие будут менее заботливыми и, возможно, выдадут предупреждение. По крайней мере про функцию `gets` уж точно. Если с другими фунциями у программиста есть возможность уберечься (проверка до вызова; у `scanf` можно указать размер в ограничение строке), то с `gets` — без вариантов.
Эти функции доставляли и продолжают доставлять проблемы. Некоторые компиляторы (msvc) по умолчанию откажутся собирать ваш код, если увидят одну из них. Другие будут менее заботливыми и, возможно, выдадут предупреждение. По крайней мере про функцию `gets` уж точно. Если с другими функциями у программиста есть возможность уберечься (проверка до вызова; у `scanf` можно указать размер в ограничение строке), то с `gets` — без вариантов.
Для большинства старых небезопасных сишных функций сейчас есть «безопасные» аналоги с размерами буферов. Часть из них не стандартизирована, часть стандартизирована. Все это породило огромное количество костылей с макроподстановками для работы со всем этим зоопарком. Но сейчас не об этом.

View File

@@ -1,6 +1,6 @@
# Бесконечные циклы и проблема останова
Определить, завершается или не завершается программа на конкретном наборе данных — алгоритимически невозможно в общем случае.
Определить, завершается или не завершается программа на конкретном наборе данных — алгоритмически невозможно в общем случае.
Но в стандартах C и C++ зачем-то сказано, что валидная программа должна либо гарантированно завершаться, либо гарантированно производить обозреваемые эффекты: запрашивать ввод-вывод, взаимодействовать с `volatile` переменными и подобное. А иначе поведение программы неопределенное. Так что «правильные» компиляторы C++ настолько суровы, что способны решать алгоритмически неразрешимые задачи.
@@ -40,7 +40,7 @@ int main () {
return 0;
}
```
Комилятор увидел, что единственный выход из цикла — `return 1`. У цикла нет никаких видимых эффектов. Так что компилятор просто заменил его на `return 1`
Компилятор увидел, что единственный выход из цикла — `return 1`. У цикла нет никаких видимых эффектов. Так что компилятор просто заменил его на `return 1`
Если же попытаться узнать, что за тройку «нашла» программа — цикл вернется.

View File

@@ -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`.

View File

@@ -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)

View File

@@ -67,7 +67,7 @@ int fun() { // CE: redefinition
Что же делать?
В мире чистого C с этим борятся комплексом методов:
В мире чистого C с этим борются комплексом методов:
1. Ручной имплементацией механизма пространств имен — каждой функции и структуре в проекте дописывают префиксом имя проекта.
2. Настраивают видимость символов:

View File

@@ -19,7 +19,7 @@ struct Node {
};
```
Такая структура совешенно законна для определения дерева, [компилируется и работает](https://godbolt.org/z/evecMd). И может быть удобнее, чем вариант с умными указателями.
Такая структура совершенно законна для определения дерева, [компилируется и работает](https://godbolt.org/z/evecMd). И может быть удобнее, чем вариант с умными указателями.
Нам не нужно никак вручную управлять ресурсами, вектор позаботится обо всем самостоятельно. Пользуемся «правилом нуля» и не пишем ни деструктор, ни конструктора копирования, ни оператора перемещения/копирования, ничего — красота!

View File

@@ -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++

View File

@@ -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++

View File

@@ -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`.

View File

@@ -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

View File

@@ -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++ любим гарантии.
С другой стороны, этот вызов конструктора, если элемент не найден, может выйти боком:

View File

@@ -65,7 +65,7 @@ struct Worker {
int main() {
// ЭТО НЕ ВЫЗОВ КОНСТРУКТОРА!
Worker w(Timer()); // предобъявление функции, которая возврщает Worker и принимает функцию, возвращающую Timer и не принимающую ничего!
Worker w(Timer()); // предобъявление функции, которая возвращает Worker и принимает функцию, возвращающую Timer и не принимающую ничего!
std::cout << w; // имя функции неявно преобразуется к указателю, который неявно преобразуется к bool
// будет выведено 1 (true)

View File

@@ -69,7 +69,7 @@ void test_v2(){
Во-вторых, стандарт C++ не специфицирует состояние, в котором должен остаться объект, _из_ которого произвели перемещение.
Оно должно быть валидным в смысле вызова деструктора. Но более ничего не требуется. Объект не обязан быть пустым после перемещения. Его поля не обязаны быть зануленными. Так у `std::thread` после перемещения нельзя вызывать ни один из методов. А `std::unique_ptr` гарантированно становится пустым (`nullptr`).
Чаще всего и проще всего натолкнуться на use-after-move можно при реализцации конструкторов, заполняющих поля переданными аргументами — достаточно дать одинаковые (или почти одинаковые) имена полям и аргументам.
Чаще всего и проще всего натолкнуться на use-after-move можно при реализации конструкторов, заполняющих поля переданными аргументами — достаточно дать одинаковые (или почти одинаковые) имена полям и аргументам.
```C++
struct Person {

View File

@@ -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`!
Собственно, эти прекрасные примеры показывают, почему «универсальная» иниациализация не универсальная.
Собственно, эти прекрасные примеры показывают, почему «универсальная» инициализация не универсальная.
Чтобы не множить хаос в своих проектах, нужно быть осторожнее с объявлениями перегруженных конструкторов для своих типов. Лучше ввести статическую функцию, чем создавать перегруженные конструкторы, неожиданно взаимодействующие с неявным приведением типов и списками инициализации.

View File

@@ -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).

View File

@@ -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_) — надо смотреть документацию для вашей платформы и вашего компилятора.
Эта парочка намного лучше неопределенного, хотя и имеет с ним одну общую черту: программа, полагающаяся на любое из них, вообще говоря, непереносима.