如何将右值引用参数传递给 C++ 中的模板 operator() 函数?

Posted

技术标签:

【中文标题】如何将右值引用参数传递给 C++ 中的模板 operator() 函数?【英文标题】:How to pass a rvalue reference parameter to a template operator() function in C++? 【发布时间】:2021-05-23 19:03:31 【问题描述】:

我尝试编写一些代码来实现 C++17 中的柯里化函数。我的当前实现在下面(我将在这个问题的底部给你一个最小的工作示例)。

template <class Function, class... CapturedArgs>
class curried
private:
    using CapturedArgsTuple = std::tuple<std::decay_t<CapturedArgs>...>;
    template <class... Args>
    static auto capture_by_value(Args&&... args)
        return std::tuple<std::decay_t<Args>...>(std::forward<Args>(args)...);
    
public:
    curried(Function function, CapturedArgs&&... args)
        : m_function(function), m_capture(capture_by_value(std::move(args)...))

    curried(Function function, std::tuple<CapturedArgs...> args)
        : m_function(function), m_capture(std::move(args))

    template <class... NewArgs>
    auto operator()(NewArgs&&... args)
        auto new_args = capture_by_value(std::forward<NewArgs>(args)...);
        auto all_args = std::tuple_cat(m_capture, new_args);
        if constexpr(std::is_invocable_v<Function, CapturedArgs..., NewArgs...>)
            return std::apply(m_function, all_args);
        else
            return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
        
    
private:
    Function m_function;
    std::tuple<CapturedArgs...> m_capture;
;

这是一个测试函数:

void func(const string& str1, string& str2, string str3)
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 << endl;

int main()
    string str1 = "Hello ", str2 = "World", str3 = "!";
    auto test = curried(func);
    auto test_two = test(std::cref(str1))(std::ref(str2));
    cout << "result : ";
    test_two(str3);

到目前为止一切顺利。我可以在我的终端上看到一些日志打印,例如:

$ result : str1 = Hello , str2 = Worldstr2 , str3 = !

这里有两个问题:

第一个是如何通过传递右值引用来调用柯里化函数?我已经尝试了所有可以搜索的内容,但结果要么编译错误,要么什么也没有。 p>

void func_1(const string& str1, string& str2, string&& str3)
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 << endl;

int main()
    string str1 = "Hello ", str2 = "World", str3 = "!";
    auto test = curried(func_1);
    auto test_two = test(std::cref(str1))(std::ref(str2));
    cout << "result : ";
    // test_two(std::move(str3)); Compile Error
    // test_two(string("!"));     Compile Error
    test_two(std::bind(std::move<string&>, str3));  // Compile successfully, but there's nothing output

在解决第一个问题的过程中,我发现了一些奇怪的东西。这是一个例子:

void func_2(const string& str1, string& str2, string str3, string& str4)
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 
         << ", str4 = " << str4 << endl;

int main()
    string str1 = "Hello ", str2 = "World", str3 = "!", str4 = "abc";
    auto test = curried(func_2);
    auto test_two = test(std::cref(str1))(std::ref(str2))(str3);
    cout << "result : ";
    test_two(std::ref(str4)); 

当我使用 func_2 测试我的柯里化函数时,我收到了一些错误消息:

$ g++ curried.cc -std=c++17
curried.cc: In instantiation of ‘auto curried<Function, CapturedArgs>::operator()(NewArgs&& ...) [with NewArgs = std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&; Function = void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&); CapturedArgs = std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >]’:
curried.cc:60:15:   required from here
curried.cc:28:11: error: no matching function for call to ‘curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::curried(void (*&)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::tuple<std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >&)’
   28 |    return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
      |           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
curried.cc:18:2: note: candidate: ‘curried<Function, CapturedArgs>::curried(Function, std::tuple<_Elements ...>) [with Function = void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&); CapturedArgs = std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&]’
   18 |  curried(Function function, std::tuple<CapturedArgs...> args)
      |  ^~~~~~~
curried.cc:18:57: note:   no known conversion for argument 2 from ‘tuple<std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >>’ to ‘tuple<std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>’
   18 |  curried(Function function, std::tuple<CapturedArgs...> args)
      |                             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
curried.cc:15:2: note: candidate: ‘curried<Function, CapturedArgs>::curried(Function, CapturedArgs&& ...) [with Function = void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&); CapturedArgs = std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&]’
   15 |  curried(Function function, CapturedArgs&&... args)
      |  ^~~~~~~
curried.cc:15:2: note:   candidate expects 4 arguments, 2 provided
curried.cc:7:7: note: candidate: ‘constexpr curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::curried(const curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>&)’
    7 | class curried
      |       ^~~~~~~
curried.cc:7:7: note:   candidate expects 1 argument, 2 provided
curried.cc:7:7: note: candidate: ‘constexpr curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>::curried(curried<void (*)(const std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>&, std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char>&), std::reference_wrapper<const std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::reference_wrapper<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >&>&&)’
curried.cc:7:7: note:   candidate expects 1 argument, 2 provided
curried.cc: In function ‘int main()’:
curried.cc:60:10: error: void value not ignored as it ought to be
   60 |  test_two(str3)(std::ref(str4));
      |  ~~~~~~~~^~~~~~

所以第二个问题是为什么我不能将字符串参数str3放在字符串引用前一个str4 为什么参数定义的顺序很重要让我完全不知所措。


为方便起见,这里是一个最小的工作示例:

#include <iostream>
#include <functional>
#include <tuple>
using namespace std;

template <class Function, class... CapturedArgs>
class curried
private:
    using CapturedArgsTuple = std::tuple<std::decay_t<CapturedArgs>...>;
    template <class... Args>
    static auto capture_by_value(Args&&... args)
        return std::tuple<std::decay_t<Args>...>(std::forward<Args>(args)...);
    
public:
    curried(Function function, CapturedArgs&&... args)
        : m_function(function), m_capture(capture_by_value(std::move(args)...))

    curried(Function function, std::tuple<CapturedArgs...> args)
        : m_function(function), m_capture(std::move(args))

    template <class... NewArgs>
    auto operator()(NewArgs&&... args)
        auto new_args = std::make_tuple(std::forward<NewArgs>(args)...);
        auto all_args = std::tuple_cat(m_capture, std::move(new_args));
        if constexpr(std::is_invocable_v<Function, CapturedArgs..., NewArgs...>)
            return std::apply(m_function, all_args);
        else
            return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
        
    
private:
    Function m_function;
    std::tuple<CapturedArgs...> m_capture;
;

void func_1(const string& str1, string& str2, string&& str3)
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 << endl;


void func_2(const string& str1, string& str2, string str3, string& str4)
    str2 += "str2 ";
    cout << "str1 = " << str1 
         << ", str2 = " << str2 
         << ", str3 = " << str3 
         << ", str4 = " << str4 << endl;


int main()

    /* code */
    string str1 = "Hello ", str2 = "World", str3_for_func_1 = "!", 
           str3_for_func_2 = "!", str4 = "abc";
    auto question_1 = curried(func_1);  // For the first question
    auto question_2 = curried(func_2);  // For the second question
    auto question_1_two_params = question_1(std::cref(str1))(std::ref(str2));
    auto question_2_two_params = question_2(std::cref(str1))(std::ref(str2));   
    cout << "result : ";
    //question_1_two_params(std::move(str3_for_func_1));  // Compile Error  
    //question_1_two_params(string("abc"));  // Compile Error   
    //auto question_2_three_params = question_2_two_params(str3_for_func_2);  // Compile Error
    //question_2_three_params(std::ref(str4)); // It should output some log like "result : str1 = Hello, balabala..."
    return 0;

编译命令:

$ g++ curryied.cc -std=c++17 -o curried 

我的工作环境是:

操作系统:Ubuntu-20.04 编译器:gcc 9.3.0 版

【问题讨论】:

你试过看看Functional Programming in C++吗? Here's the repo 带有示例,使函数柯里化是示例之一。 顺便说一句,当涉及到作为用户实际需要此功能时(因为您想使用它,而不是编码它),请注意boost::hana::curry,至少如果您知道要柯里化的最大参数数。 是的,我正在读这本书。而事实上,柯里化类的实现来自于本书中的一个例子。这是一本好书 【参考方案1】:

问题 1

一个问题是在std::apply(m_function, all_args); 行,您将all_args 作为左值传递给std::apply,这会将它作为左值传递给func_1 的第三个参数,这将失败,因为@987654329 @ 的第三个参数是一个右值引用,它不能绑定到一个左值参数。

确实,将该行更改为std::apply(m_function, std::move(all_args)); 会使前两行// Compile Error 实际编译并生成正确的输出。同样,我也会在 all_args 的其他用法上调用 std::move

问题 2

看起来std::make_tuple(std::forward&lt;NewArgs&gt;(args)...); 没有按照您的想法进行操作。将其更改为std::tuple&lt;NewArgs&amp;&amp;...&gt;(std::forward&lt;NewArgs&gt;(args)...); 即可解决问题;相当于std::forward_as_tuple(std::forward&lt;NewArgs&gt;(args)...);

为什么这个改变起作用的细节在于std::make_tuplestd::forward_as_tuple的返回类型:后者返回一个引用元组,而前者返回一个从参数复制/移动的值元组.

现在,按照我的推理:

首先,查看curried(Function function, std::tuple&lt;CapturedArgs...&gt; args):它需要args 类型的参数std::tuple&lt;CaptureArgs...&gt;。我们确定args 有这种类型吗?好吧,如果发生模板类型推导,那么答案显然是肯定的。但是,对该构造函数的调用从不利用类型推导的优势,因为唯一的调用是在return curried&lt;Function, CapturedArgs..., NewArgs...&gt;(m_function, all_args);显式提供模板参数。 所以问题仍然存在:all_args 是构造函数所期望的类型吗?嗯,递归调用中的模板实参CapturedArgs..., NewArgs...对应了类的class... CapturedArgs模板形参,用来构成构造函数的实参类型std::tuple&lt;CaptureArgs...&gt;。 所以这个问题的答案由static_asserting 给出,在递归return 之前,all_argsstd::tuple&lt;CapturedArgs..., NewArgs...&gt; 类型:
static_assert(std::is_same_v<decltype(all_args), std::tuple<CapturedArgs..., NewArgs...>>);
return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
不幸的是,您不能将此断言放入代码中,只要您传递包裹在std::ref/std::cref 中的值,因为那些未通过static_assertion 但仍然是有效输入,这正是因为@ 987654356@s 工作。你可以写一个更复杂的断言,或者你可以暂时将std::ref(bla)更改为bla等等,并检查我给你的static_assert在使用std::forward_as_tuple时是否通过,在使用std::make_tuple时失败。

感谢您提出这个问题。这是一个很好的机会让我再次深入这个复杂的话题并最终理解它!

还有一点

上面我建议你使用std::forward_as_tuple(std::forward&lt;NewArgs&gt;(args)...);

嗯,可能这个建议是错误的。

在第 238 页,作者明确声明他希望元组存储副本,以防止柯里化函数在其参数中幸存的情况。因此,最好改用这个(注意,传递给std::tuple 的模板参数中没有&amp;&amp;):

        auto new_args = std::tuple<NewArgs...>(std::forward<NewArgs>(args)...);

【讨论】:

首先,我试图搜索一些关于右值引用和 std::apply 的东西,这可以给我一些提示,谷歌给了我这个帖子:***.com/questions/34877699/…。你可以看到第二个答案,然后你就会明白我为什么这样做了。感谢您的回答,我的第一个问题已经解决,而第二个问题没有。我已经更正了我的代码。 我也不知道。如果我将std::ref(str3)std::ref(str4) 传递给question_2_two_paramsquestion_2_three_params,那么一切顺利。然而,这不是我的本意。代码可以按照我的意图工作,即当且仅当 only one 值参数被放置到参数列表中的最后一个位置。 @PhoenixChao,我已经给出了完整的答案。 @PhoenixChao,事后看来,我会更改您的问题的标题,因为事实证明该问题与operator()的界面无关。 就是这样!!而已!!!太感谢了!我还在思考这个问题,但我还没有想出任何东西。我觉得你的回答是对的。

以上是关于如何将右值引用参数传递给 C++ 中的模板 operator() 函数?的主要内容,如果未能解决你的问题,请参考以下文章

C++ 非 const 右值参数解决方法

如何将 lambda 表达式作为参数传递给 c++ 模板

如何将右值引用从调用者传递给被调用者

将模板参数传递给 MSVC 中的宏

如何评价 C++11 的右值引用(Rvalue reference)特性?

允许将右值绑定到非常量左值引用吗?