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.
auto foo(X&&... args)
which would have been idealtemplate <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]
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.