Fix a number of typos (#128)

Co-authored-by: Sergey Fukanchik <s.fukanchik@postgrespro.ru>
This commit is contained in:
Sergey Fukanchik
2025-09-29 17:13:45 +03:00
committed by GitHub
parent 61266dec35
commit 8c1499929a
27 changed files with 63 additions and 63 deletions

View File

@@ -23,7 +23,7 @@
Если вы собираетесь писать на C++ код, в работоспособности которого хотите быть хоть немного уверенными, стоит знать о существовании различных подводных камней и ловко расставленных мин в стандарте языка, его библиотеке, и всячески их избегать. Иначе ваши программы будут работать правильно только на конкретной машине и только по воле случая.
В этой книге я собрал множество самых разных примеров как в коде на C и C++ можно наткнуться на неопределенное, неожиданное и совершенно ошибочное поведение. И хотя основной фокус книги всё же на неопределенном поведении, в некоторых разделах описываются вещи вполе специфицированные, но довольно неочевидные.
В этой книге я собрал множество самых разных примеров как в коде на C и C++ можно наткнуться на неопределенное, неожиданное и совершенно ошибочное поведение. И хотя основной фокус книги всё же на неопределенном поведении, в некоторых разделах описываются вещи вполне специфицированные, но довольно неочевидные.
**Важно:** этот сборник **не является учебным пособием** по языку и рассчитан на тех, кто уже знаком с программированием, с C++, и понимает основные его конструкции.

View File

@@ -7,7 +7,7 @@
И почти всегда это нежелательная, ошибочная ситуация, последствия которой варьируются от просто неправильного результата до чудовищных уязвимостей в безопасности.
Иногда, конечно, система может быть толерантна к таким ошибкам и серьезных проблем не возникнет: например, если страничка магазина запрашивает список товаров на складе, а в этот момент база данных склада обновляется, то вы, как пользователь, возможно, увидите неполный или устаревший список товаров или смесь из старых и новых данных. Но критического ничего не произойдет, если разработчики предусмотрели дополнительные проверки с запросом к базе данных в момент взаимодейтсвия с конкретным товаром из списка... Чтобы вы не купили то, чего больше не существует.
Иногда, конечно, система может быть толерантна к таким ошибкам и серьезных проблем не возникнет: например, если страничка магазина запрашивает список товаров на складе, а в этот момент база данных склада обновляется, то вы, как пользователь, возможно, увидите неполный или устаревший список товаров или смесь из старых и новых данных. Но критического ничего не произойдет, если разработчики предусмотрели дополнительные проверки с запросом к базе данных в момент взаимодействия с конкретным товаром из списка... Чтобы вы не купили то, чего больше не существует.
Файловая система — один из таких ресурсов, гонки за которым естественны и должны учитываться при разработке приложений. Да, самые низкоуровневые проблемы синхронизации чтения и записи в файловую систему берет на себя операционная система и (или) конкретный драйвер. Можно «спокойно» одновременно из разных процессов читать и писать в один и тот же файл и получать мусор или штатные ошибки: низкоуровневые операции `read` и `write` будут как-то упорядочены планировщиком ввода-вывода. С самой файловой системой при этом всё будет в порядке.
@@ -33,7 +33,7 @@ int main() {
Это класcическая **TOCTOU** (*Time-of-Check-Time-of-Use*) ошибка. Между проверкой и открытием файла, файл может быть удален.
Но причем тут С++, если такая проблема существет для любого языка программирования?
Но причем тут С++, если такая проблема существует для любого языка программирования?
Действительно. Например, во всех версиях стандартной библиотеки Rust с 1.0 до 1.58.1 [была](https://blog.rust-lang.org/2022/01/20/cve-2022-21658.html) похожая ошибка в реализации функции `remove_dir_all`.

View File

@@ -6,10 +6,10 @@
Так почему бы не взять какую-нибудь готовую серьезную библиотеку (`boost`, `abseil`) — там наверняка умные люди уже пострадали многие часы, чтобы предоставить удобные и безопасные инструменты — и забот не знать?!
Увы, так не работает. Правильность использования этих инструментов в C++ нужно контроллировать самостоятельно, пристально изучая каждую строчку кода.
Увы, так не работает. Правильность использования этих инструментов в C++ нужно контролировать самостоятельно, пристально изучая каждую строчку кода.
Мы все равно втыкаемся в проблемы синхронизации доступа, с аккуратным развешиванием мьютексов и атомарных переменных.
Ситуация (_data race_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее премешаное значение. Независимо от языка программирования.
Ситуация (_data race_), в которой один поток программы модифицирует объект, а другой, в то же самое время, читает значения из этого объекта — или просто два потока одновременно пытаются модифицировать один объект — совершенно ясно, является ошибочной. Результат чтения может выдать какой-то странный промежуточный объект. Совместная запись — породить какое-то мутировавшее перемешанное значение. Независимо от языка программирования.
Но в C++ это не просто ошибка. Это неопределенное поведение. И «возможности» для оптимизации

View File

@@ -129,7 +129,7 @@ elapsed: 100
- Переданная функция каким-то образом начнет исполнятся асинхронно (в фоновом потоке или в пуле потоком ­— это тоже не специфировано). И такое поведение по умолчанию во всех современных версиях GCC, Clang и MSVC.
- Переданная функция не будет выполнятся до тех пор, пока вы не вызовете `wait` или `get` у возвращенной `std::future`. И такое поведение долгое время было со старыми версиями компиляторов. Например, [GCC 5.4](https://gcc.godbolt.org/z/nY6Kv4Gdz)
Какое именно поведение вы хотите можно и нужно контроллировать с помощью вызова перегрузки `std::async` с дополнительным первым параметром типа `std::launch`:
Какое именно поведение вы хотите можно и нужно контролировать с помощью вызова перегрузки `std::async` с дополнительным первым параметром типа `std::launch`:
- `std::launch::async` — если вы действительно хотите асинхронное исполнение
- `std::launch::deferred` — если нужно отложить до точки вызова `wait` или `get`

View File

@@ -137,7 +137,7 @@ awaitable<void> handle_request(const Request& r)
Корутины — очень сложные объекты, которые обманчиво просты в использовании из-за синтаксического сахара. В этом же весь смысл! Поддержка `async/await` на уровне языка и компиляторов делает простым то, что всегда было делать сложно вручную... Так в происходит в высокоуровневых и безопасных языках с автоматическим управлением памятью: Python, C#, JavaScript, Kotlin.
Но не в C++. И не в Rust. (И не в Zig).
В примере выше есть как минимуи **три** точки отказа, содержащих ошибки. Можете подумать об этом, пока мы будем разворачивать проблемы корутин С++.
В примере выше есть как минимум **три** точки отказа, содержащих ошибки. Можете подумать об этом, пока мы будем разворачивать проблемы корутин С++.
### Что такое корутина?
@@ -635,7 +635,7 @@ batch_processor 15ms
```
------
Отслеживать время жизни ссылок в асинхронном коде вручную крайне тяжело. Автоматика, как в Rust, делает это намного лучше, но при этом может выдавать совершенно непонятные репорты, в которых можно разобраться только если знаешь, что именно могло пойти не так — за это async в Rust и не любят и критикуют. А в качестве самого простого и продуктивного решения, чтоб убложить borrow checker, выбирается копирование всего подряд (`.clone()`, везде `.clone()`)
Отслеживать время жизни ссылок в асинхронном коде вручную крайне тяжело. Автоматика, как в Rust, делает это намного лучше, но при этом может выдавать совершенно непонятные репорты, в которых можно разобраться только если знаешь, что именно могло пойти не так — за это async в Rust и не любят и критикуют. А в качестве самого простого и продуктивного решения, чтоб ублажить borrow checker, выбирается копирование всего подряд (`.clone()`, везде `.clone()`)
С++ отдает полный контроль вам с невероятной кучей неявных захватов ссылок! Делайте с ними что хотите и как хотите. Скомпилируется без проблем и проверок. Вы можете приложить ментальные усилия, отследить все ссылки и убедиться что объекты не умрут не вовремя. Либо можно отчаяться, прочитать гайдлайны и передавать все и всегда by value. Копируя и перемещая. Никаких ссылок.

View File

@@ -31,7 +31,7 @@ auto p = std::make_unique<Pair>(1, 5);
```C++
// https://godbolt.org/z/fTM4n7nqz
auto p = std::make_unique<Pair>(1, 5); // теперь компилирутся в C++20
auto p = std::make_unique<Pair>(1, 5); // теперь компилируется в C++20
```
Ну добавили и добавили... Стало же удобнее? Можно теперь всегда круглые скобки использовать?...
@@ -90,7 +90,7 @@ struct Widget {
};
// Эта строчка после вашей оптимизации продолжает молча компилироваться
// но теперь влечет неопределенное поведенине
// но теперь влечет неопределенное поведение
// пример: https://gcc.godbolt.org/z/q73erhYWs
auto parent_widget = std::make_unqiue<Widget>(read_config());
// И статические анализаторы пока молчат https://gcc.godbolt.org/z/aMsT3afxb
@@ -119,7 +119,7 @@ int main() {
## Значения по умолчанию для ссылочных полей
Закончить, пожалуй, нужно еще одним недоразуменинем с ссылочными полями. Им же можно задать значения по умолчанию...
Закончить, пожалуй, нужно еще одним недоразумением с ссылочными полями. Им же можно задать значения по умолчанию...
```C++
struct Config {

View File

@@ -139,7 +139,7 @@ test_default_getter(m, 42, []{ return "default"sv; });
```
`const string& : string_view` — первый неявно приводим ко второму. Взятие string_view от string происходит без копирования. Все отлично... И вроде безопасно.
А что если наша таблица с конфигурацией отпимизирована хранить string_view на части одной большой json-конфигурационной строки и ключа в ней нет?
А что если наша таблица с конфигурацией оптимизирована хранить string_view на части одной большой json-конфигурационной строки и ключа в ней нет?
```C++
using RefMap = std::map<int, std::string_view>;

View File

@@ -19,7 +19,7 @@ struct CharTable {
};
```
Все ли впорядке с этим безобидным методом `is_whitespace`? Ну кроме того, что `char` в C/C++ обычно восьмибитный, а в unicode [есть](https://jkorpela.fi/chars/spaces.html) пробельные символы, кодируемые 16 битами.
Все ли в порядке с этим безобидным методом `is_whitespace`? Ну кроме того, что `char` в C/C++ обычно восьмибитный, а в unicode [есть](https://jkorpela.fi/chars/spaces.html) пробельные символы, кодируемые 16 битами.
Давайте [потестируем](https://godbolt.org/z/75rTW1nMG)
@@ -89,4 +89,4 @@ reference operator[]( size_type pos );
1. https://en.cppreference.com/w/cpp/language/types
2. https://en.cppreference.com/w/cpp/container/array/operator_at
3. https://en.cppreference.com/w/cpp/types/climits
4. https://docs.oracle.com/cd/E19205-01/819-5265/bjamz/index.html
4. https://docs.oracle.com/cd/E19205-01/819-5265/bjamz/index.html

View File

@@ -146,7 +146,7 @@ usummate_squares(unsigned long): # @usummate_squares(unsigned l
*/
```
GCC 13 на данный момент *(2024 год)* в принципе [не делает](https://godbolt.org/z/xjf7zj768) таких оптимизаций по умолчанию. При этом последнии версии Clang 18
GCC 13 на данный момент *(2024 год)* в принципе [не делает](https://godbolt.org/z/xjf7zj768) таких оптимизаций по умолчанию. При этом последние версии Clang 18
уже способны свернуть цикл суммирования квадратов и для беззнаковых:
```asm

View File

@@ -1,10 +1,10 @@
# ODR-violation и разделяемые библиотеки
[Ранее](odr_violation.md) я рассматривал ODR-violation в общих чертах и предупреждал о том, что может произойти, если случайно выбрать не то имя переменной, структуры или функции в C++. В этой же части я бы хотел продемострировать более изящный пример, не требующий приложения никаких усилий по написанию кривого кода. Достаточно просто иметь кривой код в ваших third-party зависимостях.
[Ранее](odr_violation.md) я рассматривал ODR-violation в общих чертах и предупреждал о том, что может произойти, если случайно выбрать не то имя переменной, структуры или функции в C++. В этой же части я бы хотел продемонстрировать более изящный пример, не требующий приложения никаких усилий по написанию кривого кода. Достаточно просто иметь кривой код в ваших third-party зависимостях.
Недавно я имел дело со странным баг-репортом:
Во внутренем репозитории с пакетами обновилcя пакет с библиотекой [gtest](https://github.com/google/googletest) -- известная уважаемая библиотека для написания самых разных тестов на C++. И в результате обновления некоторые тесты в конечных приложениях стали внезапно падать.
Во внутреннем репозитории с пакетами обновилcя пакет с библиотекой [gtest](https://github.com/google/googletest) -- известная уважаемая библиотека для написания самых разных тестов на C++. И в результате обновления некоторые тесты в конечных приложениях стали внезапно падать.
Падать они стали по-разному. У одних стали валиться проверяющие ассерты. У других же все работало, проверки проходили, но [ctest](https://cmake.org/cmake/help/latest/manual/ctest.1.html) рапортовал что тестирующий процесс вышел с ненулевым кодом возврата.
@@ -190,7 +190,7 @@ Segmentation fault (core dumped)
```
Ура! Падает!
Обе библиотеки имеют свою собсвенную версию глобальной переменной с одним и тем же неявно экспортируемым именем. Использоваться опять-таки будет только **одна**.
Обе библиотеки имеют свою собственную версию глобальной переменной с одним и тем же неявно экспортируемым именем. Использоваться опять-таки будет только **одна**.
После загрузки библиотеки, после конструирования глобальной переменной, стандарт C++ требует зарегистрировать (например через `__cxa_atexit`) функцию для вызова деструктора. У нас две библиотеки -- значит две функции будут вызваны. На одном и том же объекте. Double free. Конструктор, кстати, также вызывается дважды по одному и тому же адресу:
```C++

View File

@@ -2,7 +2,7 @@
Команде однажды завели баг-репорт: "Сервис упал c segmentation fault, в core dump стэк-трейс указывает как последнюю функцию перед падением что-то из вашей библиотеки. Разберитесь!". Упал сервис ровно один раз за полгода.
Этим чем-то был вызов `free` где-то глубоко-глубоко внутри библиотери Protobuf. И несколько последующих стэк-фреймов указывали на вызов деструктора уже в нашей библиотеке. Потратив некоторое время на анализ кода деструктора, дежурный инженер не нашел ничего подозрительного и предположил, что это похоже на какую-то ранее встреченную проблему в Protobuf. И как воспроизвести никто не представлял. Тупик...
Этим чем-то был вызов `free` где-то глубоко-глубоко внутри библиотеки Protobuf. И несколько последующих стэк-фреймов указывали на вызов деструктора уже в нашей библиотеке. Потратив некоторое время на анализ кода деструктора, дежурный инженер не нашел ничего подозрительного и предположил, что это похоже на какую-то ранее встреченную проблему в Protobuf. И как воспроизвести никто не представлял. Тупик...
Я заинтересовался этой загадочной историей и залез в core dump поглубже.

View File

@@ -13,7 +13,7 @@ public:
};
```
Пользователи интерфейса нареализовывали свойх имплементаций. Все были счастливы, пока кто-то не сделал асинхронную реализацию. С ней почему-то приложение стало падать. Проведя небольшое расследование, вы выяснили, что пользователи интерфейса не позаботились вызвать метод `stop()` перед разрушением объекта. Какая досада!
Пользователи интерфейса нареализовывали своих имплементаций. Все были счастливы, пока кто-то не сделал асинхронную реализацию. С ней почему-то приложение стало падать. Проведя небольшое расследование, вы выяснили, что пользователи интерфейса не позаботились вызвать метод `stop()` перед разрушением объекта. Какая досада!
Вы были уставши и злы. А быть может это были и не вы, а какой-то менее опытный коллега, которому поручили доработать интерфейс. В общем, на свет родилась правка

View File

@@ -11,7 +11,7 @@ int32_t foo(int32_t x, int32_t y) {
```
При вызове функции `foo` на стеке, после аргументов (хотя не факт что аргументы будут переданы через стек), будет выделено (просто вершина стека будет сдвинута) еще 4 байта (а может быть и больше, кто ж знает, что там настроено у компилятора!) под переменную z. А может быть и не будет выделено (например, если компилятор оптимизирует переменную и сложит результат сразу в регистр `rax`). Дикая природа удивительна, неправда ли?
Освобождается память со стека тоже автоматически. Причем уже не обычно, а всегда. Если только, конечно, вы случайно не сломали стек, не сделали чудовищную ассемблерную вставку и теперь адрес возврата не ведет куда-то не туда или не используете `attribute ( ( naked ) )`. Но, мне кажется, в этих случаях у вас куда более серьезные проблемы... Во всех остальных случаях память со стека освобожается автоматически. Потому, как известно, вот такой код порождает висячий указатель
Освобождается память со стека тоже автоматически. Причем уже не обычно, а всегда. Если только, конечно, вы случайно не сломали стек, не сделали чудовищную ассемблерную вставку и теперь адрес возврата не ведет куда-то не туда или не используете `attribute ( ( naked ) )`. Но, мне кажется, в этих случаях у вас куда более серьезные проблемы... Во всех остальных случаях память со стека освобождается автоматически. Потому, как известно, вот такой код порождает висячий указатель
```C
int32_t* foo(int32_t x, int32_t y) {
@@ -78,7 +78,7 @@ int main() {
Тут каждый вызов `alloca` не приводит к переполнению стека сам по себе. Но
если `use_alloca` будет заинлайнена компилятором по какой-либо причине, мы получим [SIGSEGV](https://godbolt.org/z/1xWsjqK4G)
Использование `alloca` и VLA крайне не рекомендуется. [man](https://man7.org/linux/man-pages/man3/alloca.3.html) упоминает случай, когда их использование может быть оправдано: ваш код полагается на setjmp/longjump и нормальный менеджмент динамически выделенной памяти можеь быть осложнен, а стек все равно будет очищен даже при longjmp. Не буду спрашивать, зачем оно вам...
Использование `alloca` и VLA крайне не рекомендуется. [man](https://man7.org/linux/man-pages/man3/alloca.3.html) упоминает случай, когда их использование может быть оправдано: ваш код полагается на setjmp/longjump и нормальный менеджмент динамически выделенной памяти может быть осложнен, а стек все равно будет очищен даже при longjmp. Не буду спрашивать, зачем оно вам...
alloca и vla действительно в среднем быстрее чем динамическая аллокация. Но если уж нужно действительно быстро, то вариант с преаллоцированным массивом или массивом фиксированной длины [получше будет](https://quick-bench.com/q/JWSPzPFknaSECE2W1fPiQvnEdGs)
@@ -106,4 +106,4 @@ int main(int argc, char* argv[]) {
1. https://lwn.net/Articles/749064/
2. https://man7.org/linux/man-pages/man3/alloca.3.html
3. https://nullprogram.com/blog/2019/10/27/
4. https://en.cppreference.com/w/c/language/array
4. https://en.cppreference.com/w/c/language/array

View File

@@ -129,14 +129,14 @@ auto f = [](float a) -> float {
int main() {
return integrate((float(*)(float))(f));
// комилируется и работает
// компилируется и работает
}
// -----------------------------
// https://godbolt.org/z/fqzdse1Ya
// ниблоиды в std чаще определяются так, а не с помощью лямбл
// ниблоиды в std чаще определяются так, а не с помощью лямбд
struct {
static float operator()(float x) {
return x;
@@ -210,7 +210,7 @@ float integrate<float (*)(float)>(float (*)(float)):
pxor xmm0, xmm0
cvtsi2ss xmm0, ebx
add ebx, 1
call rbp // ! нет информации о функии -- вызов по указателю
call rbp // ! нет информации о функции -- вызов по указателю
addss xmm0, DWORD PTR [rsp+12]
movss DWORD PTR [rsp+12], xmm0
cmp ebx, 26

View File

@@ -127,7 +127,7 @@ NULL-терминированные строки -- унаследованная
В новых C-библиотеках стараться проектировать API, использующие пару указатель + длина, а не только лишь указатель на NULL-терминированную последовательность.
От этого наследия страдают программы на всех языках, вынужденные взаимодействовать с C API.
Rust, например, использует отдельные типы `CStr` и `CString` для подобных строк и переход к ним из нормального кода всегда сопровоздается мучительными тяжелыми преобразованиями.
Rust, например, использует отдельные типы `CStr` и `CString` для подобных строк и переход к ним из нормального кода всегда сопровождается мучительными тяжелыми преобразованиями.
Использование NULL-терминатора встречается не только для текстовых строк. Так, например, библиотека [SRILM](http://www.speech.sri.com/projects/srilm/) активно использует 0-терминированные последовательности числовых идентификаторов, создавая этим дополнительные проблемы.
Семейство функций exec в Linux принимают NULL-терминированные последовательности указателей.

View File

@@ -83,7 +83,7 @@ free(): invalid pointer
Program terminated with signal: SIGSEGV
```
`std::shared_ptr<Button>(this)` создает новый shared_ptr, который ничего знаеть не знает о существовании другого умного указателя, управляющего объектом. Что разумеется приводит к попытке повторного освобождения памяти: сначала одним указателем, затем другим. Что иногда даже может работать успешно... Неопределенное поведение все-таки!
`std::shared_ptr<Button>(this)` создает новый shared_ptr, который ничего знает не знает о существовании другого умного указателя, управляющего объектом. Что разумеется приводит к попытке повторного освобождения памяти: сначала одним указателем, затем другим. Что иногда даже может работать успешно... Неопределенное поведение все-таки!
Хорошо, давайте чинить. Забудем пока про конструктор. Попробуем хотя бы починить метод `click`.
@@ -156,7 +156,7 @@ int main() {
Но что-то все еще не то. Полная красота не достигнута... Ну разумеется!
Ведь в вашем приложении будут не только кнопки. Но и комбо-боксы, текстовые поля и прочее...
И что к каждому классу поотдельности `public std::enable_shared_from_this<ClassName>` пририсовывать?
И что к каждому классу по отдельности `public std::enable_shared_from_this<ClassName>` пририсовывать?
Нет. Разумнее прицепить его к базовому классу и забыть. Давайте так сделаем.
@@ -320,7 +320,7 @@ event received
widget valid
```
Осталось пойти и сделать конструтор приватным с помощью приватного-тэга. Ведь иначе кто-нибудь обязательно создаст кнопку на стэке. От чего либо наполучает нулевых указателей
Осталось пойти и сделать конструктор приватным с помощью приватного-тэга. Ведь иначе кто-нибудь обязательно создаст кнопку на стэке. От чего либо наполучает нулевых указателей
```C++
int main() {

View File

@@ -8,9 +8,9 @@ std::function<R(Args)> f = /* все что угочно,
и результатом будет R */
```
Благодаря технике _type-erasure_ (стирание типа), `std::function` может хранить в себе что угодно. У этого, конечно, есть цена — посредственная производительность: конкретный вызываемый объет должен быть перемещен в кучу, выделение памяти, динамическая диспетчеризация вызова... Если мы не пишем чего-то высоконагруженного, то цена не очень высока.
Благодаря технике _type-erasure_ (стирание типа), `std::function` может хранить в себе что угодно. У этого, конечно, есть цена — посредственная производительность: конкретный вызываемый объект должен быть перемещен в кучу, выделение памяти, динамическая диспетчеризация вызова... Если мы не пишем чего-то высоконагруженного, то цена не очень высока.
Однако, благодаря тому же самомуму стиранию типов и тому, как оно реализовано, `std::function` обладает еще некоторыми потрясающими спецэффектами!
Однако, благодаря тому же самому стиранию типов и тому, как оно реализовано, `std::function` обладает еще некоторыми потрясающими спецэффектами!
### Спецэффект 1. Вариантность
@@ -107,7 +107,7 @@ std::function<void(Keyboard*)> junion = senior;
```C++
std::function<void(InputDevice*)> senior = [](auto){}; // Аллокация и перемещение лямбды на кучу! Стерли тип лямбды внутри
std::function<void(Keyboard*)> junion = std::move(senior); // Типы разные. Шаблоны инвариантны. Еще одна аллокация! и перемещенине исходной std::function на кучу. Стираем ее тип.
std::function<void(Keyboard*)> junion = std::move(senior); // Типы разные. Шаблоны инвариантны. Еще одна аллокация! и перемещение исходной std::function на кучу. Стираем ее тип.
```
А если цепочки передачи таких функций с изменением типов будут более длинными — становится уже не так здорово.
@@ -133,7 +133,7 @@ Program terminated with signal: SIGSEGV
```
И разумеется при наличии таких цепочек сохранять переданные в аргументах ссылки становится особенно сомнительным занятием.
Проблему с переизбытком аллокаций C++26 пердлагает решать с помощью `std::function_ref` — невладеющих ссылок на вызаваемый объект. Создайте ваш объект один раз и храните, а дальше передавайте на него ссылку с удобным интерфейсом. Вариантность остается в комплекте с возможностью получить dangling reference на другой `std::function_ref` в цепочке присваиваний.
Проблему с переизбытком аллокаций C++26 предлагает решать с помощью `std::function_ref` — невладеющих ссылок на вызаваемый объект. Создайте ваш объект один раз и храните, а дальше передавайте на него ссылку с удобным интерфейсом. Вариантность остается в комплекте с возможностью получить dangling reference на другой `std::function_ref` в цепочке присваиваний.
### Спецэффект 2. Отломанный const
@@ -227,4 +227,4 @@ std::function<int(int)> f = [data = std::make_unique<int>(42)](int x) { return *
## Полезные ссылки
1. [Copyable Function proposal](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2548r6.pdf)
2. [Variance](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science))
3. [Type Erasure](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Erasure)
3. [Type Erasure](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Erasure)

View File

@@ -1,6 +1,6 @@
# transform | filter — zero-cost абстракция, за которую нужно платить дважды
Когда С++20 анонсировал добавление `ranges` в стандарнтую библиотеку, вокруг было очень много обсуждений.
Когда С++20 анонсировал добавление `ranges` в стандартную библиотеку, вокруг было очень много обсуждений.
Кто-то радовался (как, например, я, по наивности), кто-то наоборот высказывал опасения насчет этой новой и якобы удобной функциональности. Особенно много возмущений и критики было со стороны разработчиков игр:
- Эти шаблоны на шаблонах компилируются долго! (и это правда)
@@ -9,7 +9,7 @@
Пример с бэнчмарком `transform | filter` против обычного старого дедовского for-loop обсуждался на форумах, показывался на конференциях, ну и здесь тоже без него не обойдется.
Как известно, [std::ranges примудали чтоб в C++ было удобнее реализовывать печать календаря](https://www.youtube.com/watch?v=8yV2ONeWXyI&t=1s), а также суммировать квадраты чисел. Календарь печатать слишком долго, поэтому будем суммировать квадраты.
Как известно, [std::ranges придумали чтоб в C++ было удобнее реализовывать печать календаря](https://www.youtube.com/watch?v=8yV2ONeWXyI&t=1s), а также суммировать квадраты чисел. Календарь печатать слишком долго, поэтому будем суммировать квадраты.
```C++
// Суммировать будем "маленькие" квадраты, чтоб как-то оправдать использование filter после transform
@@ -54,11 +54,11 @@ static int sum_small_squares_ranges(std::vector<int>& v) {
----
Обычно в этом сборнике я овещаю случаи undefined и unspecified поведения. И очень редко какие-то well-defined вещи. Однако, ситуация с `transform | filter` сочетанием оказалась настолько неожиданной, что обойти ее стороной нельзя. И речь не о производительности.
Обычно в этом сборнике я оcвещаю случаи undefined и unspecified поведения. И очень редко какие-то well-defined вещи. Однако, ситуация с `transform | filter` сочетанием оказалась настолько неожиданной, что обойти ее стороной нельзя. И речь не о производительности.
Давайте попробуем другой пример, более приближенный к реальности. Ведь на C++ не только квадраты считают, но еще и какую-то бизнес-логику программируют.
Вы подготовили список API calls запросов к AWS и собиратесь их исполнить, какие-то могут завершиться ошибкой, какие-то выполниться успешно.
Вы подготовили список API calls запросов к AWS и собираетесь их исполнить, какие-то могут завершиться ошибкой, какие-то выполниться успешно.
Конструкция
`requests | transform(execute) | filter(succeded)` выглядит очень чисто, красиво и понятно.
@@ -184,7 +184,7 @@ https://github.com/ericniebler/range-v3/issues/1090
Вернемся к началу. Цепочка `filter | transform` медленнее в 2 раза. Успешные реквесты выполняются два раза. Так и задумано. Из лучших побуждений. И из проблемного дизайна итераторов в C++.
`Range` `r` в современном C++ это абстактная штука, у которой есть `begin(r)`, возвращающий итератор `it`. И `end(r)`, возвращающий так называемый _sentinel_ `s` — еще одна абстрактная штука, с которой сравнивается итератор, чтоб понять, закончилась ли последовательность.
`Range` `r` в современном C++ это абстрактная штука, у которой есть `begin(r)`, возвращающий итератор `it`. И `end(r)`, возвращающий так называемый _sentinel_ `s` — еще одна абстрактная штука, с которой сравнивается итератор, чтоб понять, закончилась ли последовательность.
Так что процесс итерации по какому-либо range выглядит следующим образом
@@ -202,7 +202,7 @@ while (it != s) {
2. Разыменование — извлечение элемента
3. Продвижение к следующему элементу
Посмотрим, что пишут в стадарте про итератор `views::filter`
Посмотрим, что пишут в стандарте про итератор `views::filter`
```
@@ -219,7 +219,7 @@ current_ = ranges::find_if(std::move(++current_),
return *this;
```
Ага, то есть при продвижении итератора выполняется `find_if`, который разымeyет `++current_`, чтоб пременить к нему предикат фильтрации.
Ага, то есть при продвижении итератора выполняется `find_if`, который разымeyет `++current_`, чтоб применить к нему предикат фильтрации.
И потом мы его опять разыменуем, чтоб достать значение.
Вот оно удвоение!
@@ -260,7 +260,7 @@ console.log(result); // Array [1, 1, 3]
`flatMap` — довольно общая операция, трансформирующая каждый элемент последовательности в какую-то новую последовательность и объединяющая их в одну "плоскую" последовательность.
В стандартной библиотеке С++ непосредственно одного комбинатора `flatMap` (тут бы он неверняка назывался `flat_transform`) нету. Но того же эффекта можно достичь с помощью цепочки `transform | join`. Остенется лишь найти подходящий контейнер для представления последовательности из одного либо нуля элементов.
В стандартной библиотеке С++ непосредственно одного комбинатора `flatMap` (тут бы он наверняка назывался `flat_transform`) нету. Но того же эффекта можно достичь с помощью цепочки `transform | join`. Остенется лишь найти подходящий контейнер для представления последовательности из одного либо нуля элементов.
И такой контейнер есть — `std::optional`. В C++26 ему добавили методы `.begin()` и `.end()`. Этот диапазон состоит либо из одного элемента, если `optional` содержит объект, или из нуля, если не содержит. И его можно использовать для фильтрации элементов!
@@ -427,7 +427,7 @@ address of grades after: 37319408 // ! same address, no copy
```Rust
// Rust вариант страдает от своих особенностей с лайфтаймами
// Которые частично рещаются с помощью Generic Associated Types
// Которые частично решаются с помощью Generic Associated Types
// Но как абстрактный пример сойдет
trait Iterator {
type Value;

View File

@@ -97,7 +97,7 @@ int main() {
Я не смог найти ни одного компилятора доступного онлайн, на котором бы можно было бы воспроизвести последовательность оптимизаций, приводящих к падению невероятной красоты. Но я видел несколько закрытых bug репортов в отношении Apple Clang, который такое проворачивал.
LLVM может генерировать под x86 инструкцию `ud2` -- это недопустимая инструкция, часто используема как индикатор недостижимого кода. Если программа попытается ее выполнить, она умрет от сигнала SIGILL.
LLVM может генерировать под x86 инструкцию `ud2` -- это недопустимая инструкция, часто используемая как индикатор недостижимого кода. Если программа попытается ее выполнить, она умрет от сигнала SIGILL.
Код, который провоцирует неопределенное поведение может быть помечен как недостижимы и в дальнейшем заменен на `ud2` или выброшен.
В нашем замечательном примере компилятору вполне известно, что `buffer.size() == 0`. И его не меняли.

View File

@@ -1,6 +1,6 @@
# Атрибут [[assume]]
"Есть некоротая вселенская несправедливость", — подумали в комитете стандартизации C++, — "мы так много всего в языке назначили быть неопределенным поведением, чтоб помочь компиляторам генерировать оптимальный код. Но не дали такую же стандартную возможность нашим пользователям — программистам!"
"Есть некоторая вселенская несправедливость", — подумали в комитете стандартизации C++, — "мы так много всего в языке назначили быть неопределенным поведением, чтоб помочь компиляторам генерировать оптимальный код. Но не дали такую же стандартную возможность нашим пользователям — программистам!"
Да, С++23 наконец-то дал простым пользователям инструмент целенаправленного **внедрения** неопределенного поведения в их код. Такой инструмент, правда, давно уже был и так, но специфичный для конкретного компилятора. C++23 же всего лишь стандартизировал его. Так что радуйтесь, никаких больше уродливых `__builtin_assume`!
@@ -8,9 +8,9 @@
На самом деле, конечно, причина есть: компиляторы глупые, быстрый и оптимальный код получить хочется, а на ассемблере писать не очень хочется. Хотя, конечно, разработчики ffmpeg с этим не согласятся — они поэтому целенаправленно делают ассемблерные вставки, не доверяя компиляторам С.
Несмотря на то что мы говорим о C и C++, я позволю себе привести пример на Rust, поскольку считаю, что он наиболее ярко может продемонстрировать логику новвовведения С++23.
Несмотря на то что мы говорим о C и C++, я позволю себе привести пример на Rust, поскольку считаю, что он наиболее ярко может продемонстрировать логику нововведения С++23.
Возьмем достаточно простую функцию, которая выполняет семплирование отсортированной выборки: разбивает ее на группы равной величины и из каждой группы выбирает медианну величину
Возьмем достаточно простую функцию, которая выполняет семплирование отсортированной выборки: разбивает ее на группы равной величины и из каждой группы выбирает медианную величину
```Rust
use std::num::NonZeroUsize;

View File

@@ -97,7 +97,7 @@ void sum(int count, /* чтобы получить доступ к списку
C простой, маленький язык. В нем не так много типов. Примитивы, указатели, да пользовательские структуры.
В C++ есть ссылки. Есть объекты с интересными конструкторами и деструкторами. И вы уже наверняка догадались о том, что будет неопеределенное поведение, если засунуть ссылку или такой объект в качестве аргумента вариативной функции. Еще больше возможностей для веселой отладки!
В C++ есть ссылки. Есть объекты с интересными конструкторами и деструкторами. И вы уже наверняка догадались о том, что будет неопределённое поведение, если засунуть ссылку или такой объект в качестве аргумента вариативной функции. Еще больше возможностей для веселой отладки!
Но C++ не был бы самим собой, если бы в нем эту проблему не «решили». И так у нас есть C++-style вариадики:
@@ -145,4 +145,4 @@ ProcessBy([](auto&&...){ do_something(); });
# Полезные ссылки
1. https://habr.com/ru/post/430064/
2. https://en.cppreference.com/w/c/variadic/va_list
2. https://en.cppreference.com/w/c/variadic/va_list

View File

@@ -205,7 +205,7 @@ struct Unit {
```
Из-за константного поля объекты `Unit` теряют операцию присваивания.
Их нелья менять местами — `std::swap` не работает больше.
Их нельзя менять местами — `std::swap` не работает больше.
`std::vector<Unit>` нельзя больше просто так отсортировать... В общем, сплошное удобство.
Но самое интересное начинается, если сделать что-то такое
@@ -224,7 +224,7 @@ std::cout << unit.back().id << "";
Компилятор имеет право воспринимать происходящее следующим образом:
- В векторе 1 элемент
- Вектор не реаллоцировался.
- Указатель на элемет в первом `cout` и во втором `cout` один и тот же.
- Указатель на элемент в первом `cout` и во втором `cout` один и тот же.
- И там и там используется константное поле
- Я его уже читал при первом `cout`
- Зачем мне его читать еще раз, это же константа!
@@ -279,4 +279,4 @@ std::cout << unit.back().id << "";
## Полезные ссылки
1. https://isocpp.org/wiki/faq/const-correctness
2. https://miyuki.github.io/2016/10/21/std-launder.html
3. https://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-func
3. https://stackoverflow.com/questions/123758/how-do-i-remove-code-duplication-between-similar-const-and-non-const-member-func

View File

@@ -217,7 +217,7 @@ int main() {
m.set("Metric", val, std::move(comm)); // компилируется, как и хотели
m.set("MName", val, std::string_view("comment")); // не компилируется, хорошо
auto gen_comment = []()->std::string { return "comment"; };
m.set("MName", val, gen_comment()); // рабоатет отлично
m.set("MName", val, gen_comment()); // работает отлично
}
// https://godbolt.org/z/zjWGWY4xh
```

View File

@@ -72,7 +72,7 @@ int main() {
std::cout << (5 * OptionalPositive { 5 });
}
```
Это компилируется и выдает результат `6005`. Потому как выполняется user-defined неявное приведенине к `bool`, который далее неявно приводится к `int`. Все правильно.
Это компилируется и выдает результат `6005`. Потому как выполняется user-defined неявное приведение к `bool`, который далее неявно приводится к `int`. Все правильно.
Последние версии Clang хотя бы вывают частично [предупреждения](https://gcc.godbolt.org/z/fs98G3o3f)
```
@@ -86,13 +86,13 @@ int main() {
Правда, если поменять тип константы слева на `double`, в Clang 19. предупреждение исчезнет. Но компилироваться оно [не перестанет](https://gcc.godbolt.org/z/MhafPcTvv).
Никогда, если только у вас не C++98, не определяйте неявный `operator bool`! Он всегда должен быть `explicit`. Если вы боитесь, что это заставит вас делать `static_cast<bool>` там, где этого не хочется делать, то не переживайте!
С++ определяет несколько контекстов, в которых `explicit operator bool` все равно может быть вызван неявно: в условиях `if`, `for` и `while`, а также в логических операциях. Этого достаточно для большинства использнований `operator bool`.
С++ определяет несколько контекстов, в которых `explicit operator bool` все равно может быть вызван неявно: в условиях `if`, `for` и `while`, а также в логических операциях. Этого достаточно для большинства использований `operator bool`.
Если у вас C++98... Я вам очень соболезную. Но и даже в вашем печальном случае есть решение. Чудовищно громоздкое, но решение — можете ознакомиться с устаревшей [Safe Bool Idiom](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Safe_bool) в свободное время в качестве домашнего задания. Если коротко, вместо `operator bool` предгалалось определить
Если у вас C++98... Я вам очень соболезную. Но и даже в вашем печальном случае есть решение. Чудовищно громоздкое, но решение — можете ознакомиться с устаревшей [Safe Bool Idiom](https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Safe_bool) в свободное время в качестве домашнего задания. Если коротко, вместо `operator bool` предлагалось определить
```C++
// Указатель на метод в приватном классе!
typedef void (SomePrivateClass::*bool_type) () const;
operator bool_type(); // неявное приведение к этому указателю
```
И тогда бы ваш объект в условных операциях неявно приводился бы к указателю, а указатель бы далее неявно приводился к `bool`.
И тогда бы ваш объект в условных операциях неявно приводился бы к указателю, а указатель бы далее неявно приводился к `bool`.

View File

@@ -50,7 +50,7 @@ fn add(x: i32, y: i32) -> i32 {
```
Обоснования, почему не обязательно писать в конце функции `return`, следующие:
1. В функции может быть ветвление логики. В одной из веток может вызываться код, который не предполагает возврата: бесконечный цикл, исключение, `std::exit`, `std::longjmp` или что-то иное, помеченное аттрибутом `[[noreturn]]`. Проверить на наличие такого кода не всегда возможно.
1. В функции может быть ветвление логики. В одной из веток может вызываться код, который не предполагает возврата: бесконечный цикл, исключение, `std::exit`, `std::longjmp` или что-то иное, помеченное атрибутом `[[noreturn]]`. Проверить на наличие такого кода не всегда возможно.
2. Функция может содержать ассемблерную вставку со специальным кодом финализации и инструкцией `ret`.
Проверить наличие формального `return`, конечно, можно. Но нам разрешили не писать иногда (очень иногда!) чисто формальную строчку, а компиляторам разрешили не считать это ошибкой.

View File

@@ -10,8 +10,8 @@ template <class T>
struct STagged {};
using S1 = STagged<struct Tag1>; // преобъявление струкруты Tag1
using S2 = STagged<struct Tag2*>; // преобъявление струкруты Tag2
using S1 = STagged<struct Tag1>; // предобъявление струкруты Tag1
using S2 = STagged<struct Tag2*>; // предобъявление струкруты Tag2
void fun(struct Tag3*); // предобъявление структуры Tag3
@@ -93,7 +93,7 @@ int main() {
}
```
Clang способен предепреждать о подобном.
Clang способен предупреждать о подобном.
С++11 и новее предлагают *universal initialization* (через `{}`), которая не совсем *universal* и имеет свои проблемы.
C++20 предлагает еще одну *universal* инициализацию, но уже снова через `()`...

View File

@@ -55,7 +55,7 @@ struct Matrix {
};
}
// И вот месяц назад вы добавили восхитетельную
// И вот месяц назад вы добавили восхитительную
// перегрузку для доступа к элементу.
// Библиотека используется с разными версиями C++, так что
// перегрузка под feature-control флагом -- все отлично