From c14e748fb4e7d89c6569acdb0f173d15183a11eb Mon Sep 17 00:00:00 2001 From: Nekrolm Date: Tue, 3 Dec 2024 16:47:27 +0000 Subject: [PATCH] add forward --- README.md | 1 + README_ENG.md | 2 +- standard_lib/forward.md | 125 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 standard_lib/forward.md diff --git a/README.md b/README.md index bfce450..29cbee8 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ 12. [std::ranges::transform | filter](standard_lib/transform_filter_ranges.md) 13. [vector::reserve и vector::resize](standard_lib/vector_resize_reserve.md) 14. [std::function](standard_lib/std_function_const.md) + 15. [std::forward](standard_lib/forward.md) 7. Исполнение программы 1. [Бесконечные циклы](runtime/endless_loop.md) 2. [Рекурсия](runtime/recursion.md) diff --git a/README_ENG.md b/README_ENG.md index c171df0..96206f7 100644 --- a/README_ENG.md +++ b/README_ENG.md @@ -14,7 +14,7 @@ In cooperation with [PVS-studio](https://pvs-studio.com/), I'm working on extend - [Part 7](https://pvs-studio.com/en/blog/posts/cpp/1174/) - [Part 8](https://pvs-studio.com/en/blog/posts/cpp/1178/) - [Part 9](https://pvs-studio.com/en/blog/posts/cpp/1182/) -- Part 10 -- comming soon +- [Part 10](https://pvs-studio.com/en/blog/posts/cpp/1193/) - Part 11 -- comming soon ------- diff --git a/standard_lib/forward.md b/standard_lib/forward.md new file mode 100644 index 0000000..d1a317e --- /dev/null +++ b/standard_lib/forward.md @@ -0,0 +1,125 @@ +# Как неправильно использовать std::forward + +Однажды я увидел код, который демонстрирует красоту и удобство concepts в C++20/23 и мощь шаблонов. Код их действительно демонстрировал. Давайте я его покажу. + +```C++ +template +struct Matrix { + F generator; + auto operator[](uint32_t r, uint32_t c) { + return generator(r, c); + } +}; + +// Make matrix via 2 args generator function +template +auto MakeMatrix(std::invocable auto&& fn) { + using F = std::remove_cvref_t; + return Matrix(std::forward(fn)); +} +``` + +Красиво, неправда ли? Давайте его протестируем + +```C++ +int main() { + std::function generator = [](auto...) { + return 42; + }; + auto m1 = MakeMatrix<3, 3>(generator); + std::cout << "m1[1,1]=" << m1[1,1] << std::endl; + auto m2 = MakeMatrix<2, 2>(generator); + std::cout << "m2[1,1]=" << m2[1,1] << std::endl; +} +``` + +Вы могли бы подумать, что обе операции вывода успешно напечатают число 42... +Но произойдет кое-что [неожиданное](https://godbolt.org/z/6TKEP1caE)! + +``` +Program returned: 139 +terminate called after throwing an instance of 'std::bad_function_call' + what(): bad_function_call +Program terminated with signal: SIGSEGV +m1[1,1]=42 +``` + +``` +==1==ERROR: AddressSanitizer: SEGV on unknown address (pc 0x7a3c0ee28898 bp 0x7a3c0f01be90 sp 0x7ffe7360aa00 T0) +==1==The signal is caused by a READ memory access. +==1==Hint: this fault was caused by a dereference of a high value address (see register values below). Disassemble the provided pc to learn which register was used. + #0 0x7a3c0ee28898 in abort (/lib/x86_64-linux-gnu/libc.so.6+0x28898) (BuildId: 490fef8403240c91833978d494d39e537409b92e) + #1 0x7a3c0f3abbfc (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xadbfc) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #2 0x7a3c0f3bd169 (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xbf169) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #3 0x7a3c0f3ab7a8 in std::terminate() (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xad7a8) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #4 0x7a3c0f3bd3e6 in __cxa_throw (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xbf3e6) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #5 0x7a3c0f3ae6c3 in std::__throw_bad_function_call() (/opt/compiler-explorer/gcc-14.2.0/lib64/libstdc++.so.6+0xb06c3) (BuildId: 998334304023149e8c44e633d4a2c69800a2eb79) + #6 0x401256 in std::function::operator()(unsigned int, unsigned int) const /opt/compiler-explorer/gcc-14.2.0/include/c++/14.2.0/bits/std_function.h:590 + #7 0x401256 in Matrix<2u, 2u, std::function >::operator[](unsigned int, unsigned int) /app/example.cpp:12 + #8 0x401256 in main /app/example.cpp:31 +``` + + +Все совершенно определенно и соответствует спецификации. `operator()` у `std::function` бросает исключение, если объект-функция оказался пустым... Да, если вы не знали, `std::function` это неявно nullable тип. +Но какого черта объект оказался пустым?! + +Посмотрим еще раз на эти две прекрасные строчки + +```C++ +auto MakeMatrix(std::invocable auto&& fn) { + using F = std::remove_cvref_t; + return Matrix(std::forward(fn)); +} +``` +Шаблон принимает на вход так называемую «универсальную» ссылку. И использует `std::forward` чтобы передать ее дальше «универсальным» способом: передай rvalue как rvalue, lvalue как lvalue. +Вот только программист решил для красоты и читаемости сэкономить на буквах и использовал его неправильно. + +Тип-параметр у шаблона `std::forward` — обязателен и его нужно указать правильно. Им должен быть тип ссылки, который мы хотим сохранить и прокинуть далее. + +Но разработчик подсунул туда `using F = std::remove_cvref_t;` То есть буквально отбросил ссылку. +И если мы посмотрим на объявление `std::forward` + +```C++ +template< class T > +constexpr T&& forward( typename std::remove_reference::type& t ) noexcept; +template< class T > +constexpr T&& forward( typename std::remove_reference::type&& t ) noexcept; +``` + +Станет понятно, что именно пошло не так: бессылочный `T` всегда превращается в `T&&` rvalue-ссылка! Вместо `std::forward` разработчик получил `std::move`. А мы получили use-after-move. + +Универсально корректное использование `std::forward` выглядит так: + +```C++ +std::forward(value) +``` + +Либо, конечно, можно использовать явный параметр шаблона + +```C++ +template Gen> +auto MakeMatrix(Gen&& fn) { + using F = std::remove_cvref_t; + return Matrix(std::forward(fn)); +} +``` +Но такой вариант не защищен от ошибок при рефакторинге. + +---- +Я не большой фанат макросов, но конкретно для этого случая крайне рекомендую завести макрос +```C++ +#define FORWARD(x) ::std::forward(x) +``` +И никогда больше не допускать ошибку. + +В C++23 в стандартную библиотеку добавили еще функцию `std::forward_like`, чтобы навешивать на ваш объект ссылку той же категории, как у другого объекта. Соответствущий макрос может выглядеть так + +```C++ +#define FORWARD_LIKE(target, value) ::std::forward_like(value) +``` + + +## Полезные ссылки +1. https://en.cppreference.com/w/cpp/utility/forward +2. https://en.cppreference.com/w/cpp/utility/forward_like +