std::visit 的众所周知的重载不适用于 Reference_wrapper

xyl*_*per 6 c++ std-variant

这是示例代码:http://coliru.stacked-crooked.com/a/5f630d2d65cd983e

#include <variant>
#include <functional>

template<class... Ts> struct overloads : Ts... { using Ts::operator()...; };
template<class... Ts> overloads(Ts &&...) -> overloads<std::remove_cvref_t<Ts>...>;

template<typename... Ts, typename... Fs>
constexpr inline auto transform(const std::variant<Ts...> &var, Fs &&... fs)
    -> decltype(auto) { return std::visit(overloads{fs...}, var); }

template<typename... Ts, typename... Fs>
constexpr inline auto transform_by_ref(const std::variant<Ts...> &var, Fs &&... fs)
    -> decltype(auto) { return std::visit(overloads{std::ref(fs)...}, var); }
    
int main()
{
    transform(
        std::variant<int, double>{1.0},
        [](int) { return 1; },
        [](double) { return 2; }); // fine
    transform_by_ref(
        std::variant<int, double>{1.0},
        [](int) { return 1; },
        [](double) { return 2; }); // compilation error
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

在这里,我采用了众所周知的重载帮助器类型来使用多个 lambda 调用 std::visit()。

transform()复制函数对象,因此我编写了一个新函数transform_by_ref(),用于std::reference_wrapper防止复制函数对象。

尽管原始 lambda 是临时对象,但在执行结束时可以确保生命周期transform_by_ref(),并且我认为生命周期在这里不应该成为问题。

transform()按预期工作,但transform_by_ref()会导致编译错误:

main.cpp: In instantiation of 'constexpr decltype(auto) transform_by_ref(const std::variant<_Types ...>&, Fs&& ...) [with Ts = {int, double}; Fs = {main()::<lambda(int)>, main()::<lambda(double)>}]':
main.cpp:18:21:   required from here
main.cpp:13:42: error: no matching function for call to 'visit(overloads<std::reference_wrapper<main()::<lambda(int)> >, std::reference_wrapper<main()::<lambda(double)> > >, const std::variant<int, double>&)'
   13 |     -> decltype(auto) { return std::visit(overloads{std::ref(fs)...}, var); }
      |                                ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from main.cpp:1:
/usr/local/include/c++/12.1.0/variant:1819:5: note: candidate: 'template<class _Visitor, class ... _Variants> constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...)'
 1819 |     visit(_Visitor&& __visitor, _Variants&&... __variants)
      |     ^~~~~
/usr/local/include/c++/12.1.0/variant:1819:5: note:   template argument deduction/substitution failed:
In file included from /usr/local/include/c++/12.1.0/variant:37:
/usr/local/include/c++/12.1.0/type_traits: In substitution of 'template<class _Fn, class ... _Args> using invoke_result_t = typename std::invoke_result::type [with _Fn = overloads<std::reference_wrapper<main()::<lambda(int)> >, std::reference_wrapper<main()::<lambda(double)> > >; _Args = {const int&}]':
/usr/local/include/c++/12.1.0/variant:1093:11:   required by substitution of 'template<class _Visitor, class ... _Variants> using __visit_result_t = std::invoke_result_t<_Visitor, std::__detail::__variant::__get_t<0, _Variants, decltype (std::__detail::__variant::__as(declval<_Variants>())), typename std::variant_alternative<0, typename std::remove_reference<decltype (std::__detail::__variant::__as(declval<_Variants>()))>::type>::type>...> [with _Visitor = overloads<std::reference_wrapper<main()::<lambda(int)> >, std::reference_wrapper<main()::<lambda(double)> > >; _Variants = {const std::variant<int, double>&}]'
/usr/local/include/c++/12.1.0/variant:1819:5:   required by substitution of 'template<class _Visitor, class ... _Variants> constexpr std::__detail::__variant::__visit_result_t<_Visitor, _Variants ...> std::visit(_Visitor&&, _Variants&& ...) [with _Visitor = overloads<std::reference_wrapper<main()::<lambda(int)> >, std::reference_wrapper<main()::<lambda(double)> > >; _Variants = {const std::variant<int, double>&}]'
main.cpp:13:42:   required from 'constexpr decltype(auto) transform_by_ref(const std::variant<_Types ...>&, Fs&& ...) [with Ts = {int, double}; Fs = {main()::<lambda(int)>, main()::<lambda(double)>}]'
main.cpp:18:21:   required from here
/usr/local/include/c++/12.1.0/type_traits:3034:11: error: no type named 'type' in 'struct std::invoke_result<overloads<std::reference_wrapper<main()::<lambda(int)> >, std::reference_wrapper<main()::<lambda(double)> > >, const int&>'
 3034 |     using invoke_result_t = typename invoke_result<_Fn, _Args...>::type;
      |           ^~~~~~~~~~~~~~~
main.cpp: In instantiation of 'constexpr decltype(auto) transform_by_ref(const std::variant<_Types ...>&, Fs&& ...) [with Ts = {int, double}; Fs = {main()::<lambda(int)>, main()::<lambda(double)>}]':
main.cpp:18:21:   required from here
/usr/local/include/c++/12.1.0/variant:1859:5: note: candidate: 'template<class _Res, class _Visitor, class ... _Variants> constexpr _Res std::visit(_Visitor&&, _Variants&& ...)'
 1859 |     visit(_Visitor&& __visitor, _Variants&&... __variants)
      |     ^~~~~
/usr/local/include/c++/12.1.0/variant:1859:5: note:   template argument deduction/substitution failed:
main.cpp:13:42: note:   couldn't deduce template parameter '_Res'
   13 |     -> decltype(auto) { return std::visit(overloads{std::ref(fs)...}, var); }
      |                                ~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Run Code Online (Sandbox Code Playgroud)

我想我可以通过不使用 std::visit() 并实现我自己的访问函数来解决这个问题。但是,我想知道为什么这段代码不能按预期工作。

为什么我的会transform_by_ref()导致编译错误以及如何在没有自定义访问函数实现的情况下修复它?

use*_*522 9

每个std::reference_wrapper都有一个operator()重载,可以使用引用的 lambda 接受作为参数的任何参数列表来调用该重载。

这意味着两者的引用包装器都[](int) { return 1; }具有[](double) { return 2; }接受operator()参数intdouble参数的重载,两者都无需转换参数。

因此,当std::visit尝试对变体的特定元素类型进行重载解析时,operator()通过 lambda 的两个引用包装器可见的重载using Ts::operator()...;将是可​​行的,但与非引用包装器情况相比,这两个重载都将是可行的,而无需参数的转换,这意味着它们同样好,因此重载解析不明确。

可以通过强制 lambda 只采用它们应该匹配的类型作为参数来避免歧义此处假设为 C++20):

transform_by_ref(
    std::variant<int, double>{1.0},
    [](std::same_as<int> auto) { return 1; },
    [](std::same_as<double> auto) { return 2; });
Run Code Online (Sandbox Code Playgroud)

或者通过在其主体中使用单个重载来if constexpr根据参数的类型进行分支。

虽然可以使operator()包装类 SFINAE 友好,这样如果包装的可调用对象不友好,则它不会被视为可行,但不可能将调用的转换等级“转发”到此类包装器,至少一般而言。特别是对于非泛型 lambda,理论上可以提取包装器中的参数类型并将其用作重载的参数类型operator(),但这很混乱,并且不适用于泛型可调用对象。实现这样的包装器需要适当的反射。


在您的代码中,transform您直接使用fs作为左值,而不是通过 正确转发其值类别std::forward<Fs>(fs)。如果您使用它,则只会使用移动构造,而不是副本。

如果目标也是避免移动构造,则overloads在调用方中构造的常用方法已经实现了:

template<typename... Ts, typename Fs>
constexpr inline auto transform(const std::variant<Ts...> &var, Fs && fs)
    -> decltype(auto) { return std::visit(std::forward<Fs>(fs), var); }
    
int main()
{
    transform(
        std::variant<int, double>{1.0},
        overloads{
            [](int) { return 1; },
            [](double) { return 2; }});
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这使用纯右值的聚合初始化overloads,这意味着应用强制复制省略,并且不会复制或移动任何 lambda。

std::ref方法即使有效,也会浪费内存来存储非捕获 lambda 的引用。