2

我有一个结构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参数(仍然只有右值引用)。

但有一个问题。

  • 第一个你不能拥有auto foo(X&&... args)这本来是理想的
  • 第二现在你被迫这样做,template <class... Args> auto foo(Args&&... args)但现在你最终得到了转发引用,它会很高兴地接受非临时的:
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_assert或者SFINAE这两种解决方案都会使事情变得有点复杂,而且在我看来,如果语言设计得当,就永远不需要。甚至不要让我开始std::initializer_list……我们又偏离了轨道。

所以我的问题是:是否有一个我被女巫遗漏的简单解决方案/技巧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]
4

3 回答 3

6

拥有一堆右值引用的方法是使用 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;

因为它们只删除采用所有左值引用的重载,而您想删除采用任何左值引用的重载。

于 2017-08-04T19:42:59.673 回答
6

我知道如何用 static_assert 或 SFINAE 解决这个问题,但是这两种解决方案都使事情变得有点复杂 [...] 是否有我缺少的简单解决方案/技巧 [...]?

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

让我们看看代码。只需使用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,只需将它们全部测试,然后使用原始参数即可。

于 2017-08-04T20:32:42.187 回答
1

如果你真的想避免 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]
}

在科利鲁

不幸的是,你必须抛弃const,所以这不是最漂亮的解决方案。

于 2017-08-04T19:42:33.970 回答