struct Buffer {
std::vector<int> data{1, 2, 3};
const std::vector<int>& Items() const {
return data;
}
};
Buffer MakeBuffer() {
return {};
}
int main() {
for (int x : MakeBuffer().Items()) {
std::cout << x << ' ';
}
std::cout << '\n';
}
Что будет выведено?
Если запускать gcc/clang с
-fsanitize=address -fsanitize=undefined
То они взворвуться всякими ошибками на доступ к памяти.
==1==ERROR: AddressSanitizer: stack-use-after-scope on address 0x6cc5bdef0060 at pc 0x5806274e084a bp 0x7ffc913eefb0 sp 0x7ffc913eefa8
READ of size 8 at 0x6cc5bdef0060 thread T0
#0 0x5806274e0849 in __gnu_cxx::__normal_iterator<int const*, std::vector<int, std::allocator<int>>>::__normal_iterator(int const* const&) /opt/compiler-explorer/gcc-snapshot/lib/gcc/x86_64-linux-gnu/16.0.1/../../../../include/c++/16.0.1/bits/stl_iterator.h:1059:20
Без них
clang выводит:
692359176 -2055096448 -2055096448
icc выводит:
163141 0 -143200117
gcc и msvc не выводит ничего....
Ну это да, на самом деле тут дырявый ад, что Items возвращает ссылку на внутреннюю переменную временного объекта, который должен помереть до того как по нему пройдется цикл.
А под капотом - так называемый "13-летний баг", историю которого можно отследить вот по этим ссылкам:
https://cplusplus.github.io/CWG/issues/900.html
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2012r2.pdf
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2644r1.pdf
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2718r0.html
Какие выводы мы можем сделать по итогу этих ссылок? Что в С++23 починили таки эту проблему.
Содержательно fix такой: в [class.temporary] добавили четвёртый специальный контекст, в котором временный объект живёт дольше обычного — если он создан в for-range-initializer range-based for, то его lifetime продлевается на жизнь скрытой ссылки, то есть фактически на весь цикл.
The fourth context is when a temporary object other than a function parameter object is created in the for-range-initializer of a range-based for statement. If such a temporary object would otherwise be destroyed at the end of the for-range-initializer full-expression, the object persists for the lifetime of the reference initialized by the for-range-initializer.
И оно поддержано уже в gcc/clang (но не других компиляторах).
Пример с теми же санитайзерами выводит:
1 2 3
Приятно, что всего через 13 лет один из самых болезненных багов из С++ ушел...