Понимание композиции ленивых функций на основе диапазона
TL;DR
Что не так с последним закомментированным блоком строк ниже?
// headers and definitions are in the down the question
int main() {
std::vector<int> v{10,20,30};
using type_of_temp = std::vector<std::pair<std::vector<int>,int>>;
// seems to work, I think it does work
auto temp = copy_range<type_of_temp>(v | indexed(0)
| transformed(complex_keep_index));
auto w = temp | transformed(distribute);
print(w);
// shows undefined behavior
//auto z = v | indexed(0)
// | transformed(complex_keep_index)
// | transformed(distribute);
//print(z);
}
Или, другими словами, что делает конвейерную передачу четко определенным, а конвейерную передачу — неопределенным поведением?
Расширенная версия
У меня есть контейнер с вещами,
std::vector<int> v{10,20,30};
и у меня есть функция, которая генерирует еще один контейнер из каждой из этих вещей,
// this is in general a computation of type
// T -> std::vector<U>
constexpr auto complex_comput = [](auto const& x){
return std::vector{x,x+1,x+2}; // in general the number of elements changes
};
поэтому, если бы я применил к , я бы получил,
{{10, 11, 12}, {20, 21, 22}, {30, 31, 32}}
и если бы я также объединил результаты, я бы, наконец, получил это:
{10, 11, 12, 20, 21, 22, 30, 31, 32}
Однако я хочу отслеживать индекс, откуда взялось каждое число, чтобы результат кодировался примерно так:
0 10
0 11
0 12
1 20
1 21
1 22
2 30
2 31
2 32
Чтобы добиться этого, я (в конце концов) придумал это решение, в котором я попытался использовать диапазоны из Boost. В частности, я делаю следующее:
- использовать
boost::adaptors::indexed
чтобы прикрепить индекс к каждому элементуv
- преобразовать каждую полученную «пару» в
std::pair
хранениеindex
и результат примененияcomplex_comput
кvalue
, - и, наконец, преобразование каждого
std::pair<st::vector<int>,int>
вstd::vector<std::pair<int,int>>
.
Однако мне пришлось отказаться от диапазона между 2 и 3, используя хелпер "true"
std::vector
между двумя преобразованиями.
#include <boost/range/adaptor/indexed.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/iterator_range_core.hpp>
#include <iostream>
#include <utility>
#include <vector>
using boost::adaptors::indexed;
using boost::adaptors::transformed;
using boost::copy_range;
constexpr auto complex_comput = [](auto const& x){
// this is in general a computation of type
// T -> std::vector<U>
return std::vector{x,x+1,x+2};
};
constexpr auto complex_keep_index = [](auto const& x){
return std::make_pair(complex_comput(x.value()), x.index());
};
constexpr auto distribute = [](auto const& pair){
return pair.first | transformed([n = pair.second](auto x){
return std::make_pair(x, n);
});
};
template<typename T>
void print(T const& w) {
for (auto const& elem : w) {
for (auto i : elem) {
std::cout << i.second << ':' << i.first << ' ';
}
std::cout << std::endl;
}
}
int main() {
std::vector<int> v{10,20,30};
using type_of_temp = std::vector<std::pair<std::vector<int>,int>>;
auto temp = copy_range<type_of_temp>(v | indexed(0)
| transformed(complex_keep_index));
auto w = temp | transformed(distribute);
print(w);
//auto z = v | indexed(0)
// | transformed(complex_keep_index)
// | transformed(distribute);
//print(z);
}
Действительно, раскомментирование строк, определяющих и использующих, дает вам код, который компилируется, но генерирует бесполезные результаты, то есть поведение undefined. Обратите внимание, что применение
copy_range<type_of_temp>
к первому, рабочему, диапазону, иначе результирующий код по сути такой же, как тот, что справа от
auto z =
.
Почему я должен это делать? Какие детали делают oneliner неработоспособным?
Я частично понимаю причину, и я перечислю свое понимание/мысли ниже, но я задаю этот вопрос, чтобы получить подробное объяснение всех деталей этого.
- Я понимаю, что неопределенное поведение, которое я наблюдаю, связано с
z
являющийся диапазоном, определяющим представление о каком-то временном объекте, который был уничтожен; - учитывая рабочую версию кода, очевидно, что временный ;
- однако сам по себе не является временным, который подается на
transformed(complex_keep_index)
? - Вероятно, одна важная деталь состоит в том, что выражение
v | indexed(0)
является не более чем ленивым диапазоном, который ничего не оценивает, а просто настраивает все так, что при повторении диапазона выполняются вычисления; в конце концов, я могу легко сделатьv | indexed(0) | indexed(0) | indexed(0)
, который хорошо определен; - а также все хорошо определено, в противном случае код выше, использующий
w
вероятно, будет вести себя неправильно (я знаю, что UB не означает, что результат должен показывать, что что-то не так, и все может выглядеть нормально на этом оборудовании, в этот момент, и сломаться завтра). - Таким образом, что-то изначально неправильное - это передача rvalue в
transformed(distribute)
; - но что в этом плохого, так это , а не , потому что, например, изменение на
[](auto x){ return x; }
кажется, хорошо определен. - Так что же не так? Вот код
constexpr auto distribute = [](auto const& pair){
return pair.first | transformed([n = pair.second](auto x){
return std::make_pair(x, n);
});
};
- В чем проблема? Возвращаемый диапазон (вывод этого
transformed
) будет содержать некоторые итераторы/указатели/ссылки наpair.first
который является частью выходит за рамки, когдаdistribute
возвращает, но является ссылкой на что-то в вызывающей программе, которая продолжает жить, верно? - Однако я знаю, что хотя
const
ссылка (напримерpair
) может хранить временную (например, элементыv | indexed(0) | transformed(complex_keep_index)
) живым, это не означает, что временное остается живым, когда эта ссылка выходит за пределы области действия только потому, что на нее, в свою очередь, ссылается что-то еще (ссылки/указатели/итераторы в выводеtransformed([n = …](…){ … })
), что не выходит за рамки.
Я думаю/надеюсь, что, вероятно, ответ уже есть в том, что я написал выше, однако мне нужна помощь, чтобы упорядочить все это, чтобы я мог понять это раз и навсегда.