具有右值引用而不是使用可变参数模板的转发引用

Have rvalue reference instead of forwarding reference with variadic template

我有一个结构 X 和一个函数 foo,它们必须接收 X.

的右值引用

一开始我只从一个参数开始,而且很简单(哦……更简单的时候):

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

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

但后来我想扩展并接受可变数量的 X 个参数(仍然只有右值引用)。

但是有个问题

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]

为什么他们使用这种语法和规则来转发引用让我从一开始就感到困惑。这是一个问题。这种语法的另一个问题是 T&&X<T>&& 是完全不同的野兽。但我们在这里偏离了轨道。

我知道如何使用 static_assertSFINAE 来解决这个问题,但是这两种解决方案都会让事情变得有点复杂,而且我认为如果语言是为一次。甚至不要让我开始 std::initializer_list ...我们又偏离轨道了。

所以我的问题是:是否有一个简单的 solution/trick 我遗漏了 Args&& / args 被视为右值引用?


当我结束这个问题时,我想我有一个解决方案。

为左值引用添加已删除的重载:

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

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

简单,优雅,应该可以,让我们测试一下:

X x1, x2;

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

好的,好的,我有!

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

如果你真的想避免SFINAE,你可以const&&提出你的论点;它们不会成为通用参考:

template <class... Args>
auto foo(const Args&&... args) -> void { ... };

const&& 只能绑定到右值,不能绑定到左值。

#include <iostream>

template <class... Args>
auto foo(const Args&&... args) -> void
{
    std::cout << __PRETTY_FUNCTION__ << '\n';
}

struct X {};

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

On Coliru

不幸的是,您随后不得不放弃 const,因此这不是最漂亮的解决方案。

有一堆右值引用的方法是使用 SFINAE:

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

折叠表达式是 C++17,很容易编写一个元函数来在 C++14 中获得相同的行为。这确实是您唯一的选择——您想要一个推导出右值引用的受限函数模板,但唯一可用的语法被重载以表示转发引用。我们可以使模板参数成为非推导的,但是你必须提供它们,这似乎根本不是一个解决方案。

有了概念,这当然更干净了,但我们并没有真正改变底层机制:

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

或更好:

template <class T>
concept NonReference = !std::is_lvalue_reference<T>::value;

template <NonReference... Args>
void foo(Args&&... ) { ... }

值得指出的是,这些都不起作用:

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

因为它们只删除采用 all 左值引用的重载,而您想删除采用 any 左值引用的重载。

I know how to solve this with static_assert or SFINAE but both of these solutions complicate things a bit [...] is there a simple solution/trick I am missing [...]?

有一种非常好的方法可以做到这一点,而无需复杂的 SFINAE 表达式或 static_asserts。它还不需要您猜测哪些参数是 const,哪些不是(毕竟,如果您尝试使用变量的 const 性,它可能会很快将您带到 UB)。此外,如果您关心这一点,则不必为此包含 <type_traits>
它基于几乎存在的

让我们看看代码。只需使用 decltype 和一个只需要声明的测试函数,根本不需要定义:

#include<utility>

template<typename... T>
void test(const T &&...);

template <class... Args>
auto foo(Args&&... args)
-> decltype(test(std::forward<Args>(args)...), void())
{}

struct X {};

int main() {
    X x1, x2;
    //foo(x1, x2);
    foo(std::move(x1), std::move(x2));
    foo(X{}, X{});
}

如果您切换注释,该示例将不再按要求编译。

基本思想与其他答案中讨论的几乎相同:如果这是右值引用,则可以将其分配给 const 右值引用。不管怎样,因为你不想让那些原本不是const的参数成为const,就把它们test全部放在一起,然后使用原来的。