Search code examples
c++c++14variadic-templatesrvalue-referenceforwarding-reference

Have rvalue reference instead of forwarding reference with variadic template


I have a struct X and a function foo which must receive rvalue references of X.

At first I started with just one argument, and it was simple (oh... the simpler times):

auto foo(X&& arg) -> void {...};

X x;
foo(x);            // compile error [OK]
foo(std::move(x)); // accepted      [OK]
foo(X{});          // accepted      [OK]

But then I wanted to expand and accept a variable number of X arguments (still only rvalue rferences).

But there is a problem.

  • 1st you cannot have auto foo(X&&... args) which would have been ideal
  • 2nd Now you are forced to do template <class... Args> auto foo(Args&&... args) but now you end up with forwarding references, which will happily accept non-temporaries:
template <class... Args>
auto foo(Args&&... args) -> void { ... };

X x1, x2;
foo(x1, x2);                       // accepted [NOT OK]
foo(std::move(x1), std::move(x2)); // accepted [OK]
foo(X{}, X{});                     // accepted [OK]

Why they used this syntax and rules for forwarding references baffled me since the begging. This is one problem. The other problem with this syntax is that T&& and X<T>&& are completely different beasts. But we are getting off track here.

I know how to solve this with static_assert or SFINAE but both of these solutions complicate things a bit, and in my humble opinion should never have been needed if the language was designed right for once. And don't even get me started on std::initializer_list ... we are getting off track again.

So my question: is there a simple solution/trick I am missing by witch Args&& / args are treated as rvalue references?


Just as I was wrapping up this question, I thought I had a solution.

Add deleted overloads for lvalue references:

template <class... Args>
auto foo(const Args&... args) = delete;

template <class... Args>
auto foo(Args&... args) = delete;

Simple, elegant, should work, let's test it:

X x1, x2;

foo(x1, x2);                       // compile error [OK]
foo(std::move(x1), std::move(x2)); // accepted [OK]
foo(X{}, X{});                     // accepted [OK]

Ok, yeey, I have it!

foo(std::move(x1), x2);            // accepted [oh c'mon]

Solution

  • The way to have a bunch of rvalue references is with SFINAE:

    template <class... Args,
        std::enable_if_t<(!std::is_lvalue_reference<Args>::value && ...), int> = 0>
    void foo(Args&&... args) { ... }
    

    Fold-expressions are C++17, it is easy enough to write a metafunction to get that same behavior in C++14. This is your only option really - you want a constrained function template deducing to rvalue references, but the only available syntax is overloaded to mean forwarding references. We could make the template parameters non-deduced, but then you'd have to provide them, which seems like not a solution at all.

    With concepts, this is of course cleaner, but we're not really changing the underlying mechanism:

    template <class... Args>
        requires (!std::is_lvalue_reference<Args>::value && ...)
    void foo(Args&&... args) { ... }
    

    or better:

    template <class T>
    concept NonReference = !std::is_lvalue_reference<T>::value;
    
    template <NonReference... Args>
    void foo(Args&&... ) { ... }
    

    It's worth pointing out that neither of these work:

    template <class... Args> auto foo(const Args&... args) = delete;
    template <class... Args> auto foo(Args&... args) = delete;
    

    because they only delete overloads that take all lvalue references, and you want to delete overloads that take any lvalue references.