C++ variadic template function with move semantics

Viewed 897

In C++, I have a set of variadic template functions that I'd like to accept an arbitrary number of parameters, either constant references or as r-value references (so I can move things rather than copy when possible). However, I find that the version that accepts the constant reference is called even when I'm wrapping arguments in std::move().

// Accept end of parameters.
void example () {}

// Accept a non-constant R-value reference to do a move
template <typename... More>
void example (std::string &&value, More... parameters) {
    std::cout << value << ": moved" << std::endl;
    example (parameters...);
}

// Accept a constant reference parameter.    
template <typename... More>
void example (const std::string &value, More... parameters) {
    std::cout << value << ": copied" << std::endl;
    example (parameters...);
}

int main (int, char **) {
    std::string first { "first" };
    std::string second { "second" };
    std::string third { "third" };

    std::cout << "Trying variadic with move as second parameter: " << std::endl;
    example (first, std::move (second), third);
    // std::cout << "Trying variadic with move as first parameter: " << std::endl;
    // This next line won't even compile when uncommented
    // example (std::move (first), std::move (second), third);
    return 0;
}

The output is:

Trying variadic with move as second parameter: 
first: copied
second: copied
third: copied

instead of the expected:

Trying variadic with move as second parameter: 
first: copied
second: moved
third: copied

And as a bonus, when I wrap the first argument in std::move(), I get a compile error on both g++7 and clang 9.

What am I doing wrong?

3 Answers

There are several problems here:

  • More... parameters always receives arguments by value (as long as the template parameters are deduced), because types in typename ...More will never be deduced as references.

  • All arguments passed to example in example(parameters...); will always be lvalues.

  • The string && overload can't call the const string & one, because it's not yet declared at that point.

Instead of passing by value, you should use forwarding references and std::forward. And you need to declare the const string & overload before defining the string && one.

void example() {}

// Declare lvalue overload.
template <typename ...More>
void example(const std::string &value, More &&... parameters);

// Rvalue overload.
template <typename... More>
void example (std::string &&value, More &&... parameters) {
    std::cout << value << ": moved" << std::endl;
    example(std::forward<More>(parameters)...);
}

// Lvalue overload.
template <typename ...More>
void example(const std::string &value, More &&... parameters) {
    std::cout << value << ": copied" << std::endl;
    example(std::forward<More>(parameters)...);
}

This works:

void  example2 () {}

template<typename First, typename... More>
void  example2( First &&value, More&&... parameters ) {
    if constexpr( std::is_rvalue_reference_v<decltype(value)> ) {
        std::cout << value << ": moved" << std::endl;
    }
    else {
        std::cout << value << ": copied" << std::endl;
    }
    example2( std::forward<More>(parameters)... );
}

int main (int, char **) {
    std::string first  { "first" };
    std::string second { "second" };
    std::string third  { "third" };

    std::cout << "copy, move, copy: " << std::endl;
    example2( first, std::move(second), third );

    // if we really moved second in the previous call
    // then second will be an empty string here.
    std::cout << "move, move, copy: " << std::endl;
    example2(std::move(first), std::move(second), third);

    return 0;
}

If you don't forward parameters any && will be passed as &. This is the main issue with your attempt.

You also don't need two functions to handle value, if you make it a && template param it will handle both cases: lvalue and rvalue, which we can detect using is_rvalue_reference<>.

HolyBlackCat already mentioned why your code behaves as it does so I'm not going to explain that again here. Here is another solution using fold expressions:

template <typename T>
void process(T const& lvalue)
{
    std::cout << lvalue << ": copied\n";
}

// the enable_if is necessary because T&& is a forwarding reference, not a rvalue reference
template <typename T, typename = std::enable_if_t<!std::is_lvalue_reference_v<T>>>
void process(T&& rvalue)
{
    std::cout << rvalue << ": moved\n";
}

template <typename... Args>
void example(Args&&... args)
{
    (process(std::forward<Args>(args)), ...);    
}

Example

Related