Force deduction of template parameter to reference

Viewed 264

The following code does not compile because the compiler deduce a template parameter to be int while it would be required to be int &. See it on Coliru here.

#include <iostream>
#include <utility>

template <class F, class... ArgsType>
void do_something(F f, ArgsType... args)
{
    f(std::forward<ArgsType>(args)...);
}

int main()
{
    int s = 2;
    int i = 1;
    auto add = [](const int a, int& sum) { sum += a; };
    do_something(add, i, s);
    std::cout << s << std::endl;

    return 0;
}

Error:

main.cpp: In instantiation of 'void do_something(F, ArgsType ...) [with F = main()::<lambda(int, int&)>; ArgsType = {int, int}]':

main.cpp:15:27:   required from here

main.cpp:7:6: error: no match for call to '(main()::<lambda(int, int&)>) (int, int)'

     f(std::forward<ArgsType>(args)...);

     ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

main.cpp:7:6: note: candidate: 'void (*)(int, int&)' <conversion>

main.cpp:7:6: note:   conversion of argument 3 would be ill-formed:

main.cpp:7:6: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

main.cpp:14:40: note: candidate: 'main()::<lambda(int, int&)>' <near match>

     auto add = [](const int a, int& sum) { sum += a; };

                                        ^

main.cpp:14:40: note:   conversion of argument 2 would be ill-formed:

main.cpp:7:6: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

     f(std::forward<ArgsType>(args)...);

     ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Ideally, the third argument of do_something should be deduced as int&. One way to do this is to explicitly pass the template parameters as

#include <iostream>
#include <utility>

template <class F, class... ArgsType>
void do_something(F f, ArgsType... args)
{
    f(std::forward<ArgsType>(args)...);
}

int main()
{
    int s = 2;
    int i = 1;
    auto add = [](const int a, int& sum) { sum += a; };
    do_something<decltype(add), const int, int&>(add, i, s);
    std::cout << s << std::endl;

    return 0;
}

See it on Coliru here.

While the solution works, I find it inconvenient, because it forces me to provide all template types of do_something, which is not optimal especially if, say, I have a more complex example with several parameters, or if I would like to insert the lambda function add directly as a parameter of do_something:

do_something([](const int a, int& sum) { sum += a; }, i, s);

Is there a more convenient way to force just the third parameter to be deduced as int &?

2 Answers

Try

template <class F, class... ArgsType>
void do_something(F f, ArgsType&&... args)
//                            ^^^^ ---------------> perfect forwarding
{
    f(std::forward<ArgsType>(args)...);
}

Understanding the problem

the second parameter (sum) of your lambda expression

auto add = [](const int a, int& sum) { sum += a; };

is an lvalue reference.

The parameter pack, ArgsType in

template <class F, class... ArgsType>
void do_something(F f, ArgsType... args);

is deduced to the argument pack int, int when passing the variables i and s as arguments, respectively.

Keeping in mind that std::forward<ArgsType>(args)... is the same as static_cast<ArgsType&&>(args)... (i.e., it is just cast to an rvalue reference type), the call to do_something() would be equivalent to calling the following function template:

template <class F>
void do_something(F f, int a, int b)
{
    f(static_cast<int&&>(a), static_cast<int&&>(b));
}

The expression static_cast<int&&>(b) is an rvalue (an xvalue, to be more precise). Since you cannot initialise an lvalue reference (the parameter) with an rvalue (the argument), it results in a compilation error.


Solution

You can use forwarding references instead:

template <class F, class... ArgsType>
void do_something(F f, ArgsType&&... args);

The type of the separate arguments of the argument pack args will always be a reference:

  • An lvalue reference type if an lvalue was passed as an argument.
  • An rvalue reference type if an rvalue was passed as an argument.

This way, for your function call, the parameter pack ArgsType will be deduced to the argument pack int&, int& which would be equivalent to calling the following function template:

template <class F>
void do_something(F f, int& a, int& b)
{
    f(static_cast<int& &&>(a), static_cast<int& &&>(b));
}

Since reference collapsing applies to static_cast<int& &&>(b), the expression results in static_cast<int&>(b), which is an lvalue. An lvalue reference type can be initialised with an lvalue.

However, note that the call:

do_something(add, i, 7);
                     ^
                     |
                     --- rvalue

won't compile now, since an rvalue is being passed to add as a second argument. The reasoning is similar to your original error.

Related