如何实现返回混合右值与左值引用的元组且避免左值引用退化?
问题描述
我需要实现一个函数,使其返回包含右值与左值引用的std::tuple,避免左值引用退化为值类型。以下是最小示例代码:
struct RValue { int x = 12; auto operator*() const { return x; } } x; struct LValueRef { int y = 13; auto &operator*() { return y; } } y; auto z = std::make_tuple(x, y); auto f() { // Replace std::make_tuple with what? return std::apply([](auto &...x) { return std::make_tuple(*x...); }, z); } int main() { auto [a, b] = f(); }
当前f()返回的是std::tuple<int, int>,但我期望它返回std::tuple<int, int&>。该逻辑需要适配任意元组z,其元素解引用后可能是右值或左值引用。
原本元组z的元素均类似LValueRef类,使用std::tie即可满足返回要求,但现在需要兼容解引用返回右值的元素。我想实现的逻辑大概是:对左值引用类型的元素用std::ref包裹,否则直接返回元素,但不清楚如何在std::apply调用中实现;也考虑过转换元组类型后返回std::tuple<Ts_transformed...>(*x...)。
我曾尝试使用std::forward_as_tuple,它会返回std::tuple<int&&, int&>,但此时会出现访问已销毁临时对象的问题,我不清楚具体原因,也不明白为何无法使用int&&类型的值。
我的初衷是编写类似Python的range和zip的适配器,上述函数用于解引用迭代器元组,原本迭代器均返回左值引用,std::tie可正常工作,现在需要兼容返回右值的迭代器,适配器代码如下:
template <typename... WrappedRanges> class ZipWrappedRange { std::tuple<WrappedRanges...> r_; public: class iterator { std::tuple<typename WrappedRanges::iterator...> its_; public: explicit iterator(typename WrappedRanges::iterator &&...it) : its_(std::make_tuple(it...)) {} explicit iterator(std::tuple<typename WrappedRanges::iterator...> &&it) : its_(it) {} auto operator*() const { // I need to modify this to allow for cases where (*x) returns rvalues and not just l-value references. return std::apply([](auto &...x) { return std::tie((*x)...); }, its_); } }; ... }
请问实现该需求的理想方式是什么?
解决方案
核心思路是根据解引用结果的类型(左值引用/右值),在tuple中存储对应的正确类型:对于左值引用保留引用,对于右值直接存储值(避免悬垂引用)。
最小示例的修改实现
通过类型萃取和折叠表达式,为每个解引用结果生成正确的存储类型:
#include <tuple> #include <type_traits> struct RValue { int x = 12; auto operator*() const { return x; } } x; struct LValueRef { int y = 13; auto &operator*() { return y; } } y; auto z = std::make_tuple(x, y); // 辅助函数:根据解引用结果类型,返回正确的存储形式 template<typename T> auto resolve_deref_type(T&& elem) { using DerefResult = decltype(*elem); if constexpr (std::is_lvalue_reference_v<DerefResult>) { return *elem; // 保留左值引用 } else { return std::move(*elem); // 右值类型直接移动存储,减少拷贝 } } auto f() { return std::apply([](auto &...elems) { return std::tuple{resolve_deref_type(elems)...}; }, z); } int main() { auto [a, b] = f(); // a是int类型,b是int&类型,符合预期 }
Zip适配器的修改实现
将同样的逻辑整合到迭代器的operator*()中:
#include <tuple> #include <type_traits> template <typename... WrappedRanges> class ZipWrappedRange { std::tuple<WrappedRanges...> r_; public: class iterator { std::tuple<typename WrappedRanges::iterator...> its_; // 辅助函数:确定每个迭代器解引用后的正确存储类型 template<typename Iter> static auto get_deref_value(Iter&& iter) { using DerefResult = decltype(*iter); if constexpr (std::is_lvalue_reference_v<DerefResult>) { return *iter; // 保留左值引用 } else { return std::move(*iter); // 右值类型移动存储 } } public: explicit iterator(typename WrappedRanges::iterator &&...it) : its_(std::make_tuple(it...)) {} explicit iterator(std::tuple<typename WrappedRanges::iterator...> &&it) : its_(it) {} auto operator*() const { return std::apply([](auto &...iters) { return std::tuple{get_deref_value(iters)...}; }, its_); } // 补充迭代器必要的operator++、operator==等方法 iterator& operator++() { std::apply([](auto&... iters) { (++iters, ...); }, its_); return *this; } bool operator==(const iterator& other) const { // 这里以第一个迭代器的相等性作为判断依据,可根据需求调整 return std::get<0>(its_) == std::get<0>(other.its_); } }; // 补充Range接口的begin()、end()方法 iterator begin() { return std::apply([](auto&... ranges) { return iterator(std::begin(ranges)...); }, r_); } iterator end() { return std::apply([](auto&... ranges) { return iterator(std::end(ranges)...); }, r_); } explicit ZipWrappedRange(WrappedRanges&&... ranges) : r_(std::make_tuple(std::move(ranges)...)) {} };
为什么std::forward_as_tuple会出问题?
std::forward_as_tuple会直接返回包含右值引用的tuple,比如对于RValue的解引用结果(临时int),它会存储int&&。但这个临时int在operator*()返回后就会被销毁,后续访问这个右值引用会导致悬垂引用,触发未定义行为。而我们的方案中,对于右值会直接存储其值,从根源避免了这个问题。
内容的提问来源于stack exchange,提问作者darkspine

