精简概念将如何与通用参考交互?

How will concepts lite interact with universal references?

我最近看了 this video explaining the ideas of concepts lite in C++, which are likely to appear this year as a TS. Now, I also learned about universal references/forwarding references (as described here) 并且 T&& 根据上下文可以有两种含义(即是否正在执行类型推导)。这自然会引出概念将如何与通用引用交互的问题?

为了具体化,在下面的例子中我们有

void f(int&& i) {}

int i = 0;
f(i);    // error, looks for f(int&)
f(0);    // fine, calls f(int&&)

template <typename T>
void f(T&& test) {}

int i = 0;
f(i);    // fine, calls f(T&&) with T = int& (int& && = int&)
f(0);    // fine, calls f(T&&) with T = int&& (int&& && = int&&)

但是如果我们使用概念会发生什么?

template <typename T>
    requires Number<T>
void f(T&& test) {}

template <Number T>
void g(T&& test) {}

void h(Number&& test) {}

int i = 0;
f(i);    // probably should be fine?
f(0);    // should be fine anyway
g(i);    // probably also fine?
g(0);    // fine anyway
h(i);    // fine or not?
h(0);    // fine anyway

尤其是最后一个例子让我有点困扰,因为有两个冲突 原则。首先,以这种方式使用的概念应该像类型一样工作,其次,如果 T 是推导类型,则 T&& 表示通用引用而不是右值引用。

提前感谢您对此的澄清!

T&& 始终具有相同的 "meaning" -- 它是对 T.

的右值引用

T 本身就是一个引用时,有趣的事情就发生了。如果 T=X&&,则 T&& = X&&。如果 T=X& 那么 T&& = X&。对左值引用的右值引用是左值引用的规则允许转发引用技术存在。这称为引用折叠1.

至于

template <typename T>
  requires Number<T>
void f(T&& test) {}

这取决于 Number<T> 的含义。如果 Number<T> 允许左值引用通过,那么 T&& 将像转发引用一样工作。如果不是,T&& 它只会绑定到右值。

由于其余示例(我最后检查过)是根据第一个示例定义的,所以您已经知道了。

概念规范中可能还有额外的"magic",但我不知道。


1 从来没有 实际上 引用对引用。事实上,如果您输入 int y = 3; int& && x = y; 那是一个非法表达式:但是 using U = int&; U&& x = y; 是完全合法的,因为会发生引用折叠。

类比 const 的工作原理有时会有所帮助。如果 T const x;const 而不管 T 是否是 const。如果 T_constconst,那么 T_const x; 也是 constT_const const x; 也是常量。 xconstness 是类型 T 和任何 "local" 修饰符的 constness 的最大值。

类似地,引用的左值是 T 和任何 "local" 修饰符的左值的最大值。想象一下,如果该语言有两个关键字,reflvalue。将 & 替换为 lvalue ref,将 && 替换为 ref。使用 lvalue 而没有 ref 在此翻译下是非法的..

T&& 表示 T ref。如果 Tint lvalue ref,那么引用折叠会导致 int lvalue ref ref -> int lvalue ref,转换回 int&。同样,T& 翻译成 int lvalue ref lvalue ref -> int lvalue ref,如果 T=int&&,则 T& 翻译成 int ref lvalue ref - > int lvalue ref -> int&.

这完全取决于概念本身是如何编写的。 Concepts-Lite 本身(latest TS 在撰写本文时)在这个问题上是不可知的:它定义了可以在语言中定义和使用概念的机制,但不向库中添加库存概念。

另一方面,文档 N4263 Toward a concept-enabled standard library 是标准委员会一些成员的意向声明,它建议在 Concepts-Lite 之后的自然步骤是一个单独的 TS,用于将概念添加到标准库中,以便约束例如算法。

那个 TS 可能有点落后,但我们仍然可以看看到目前为止是如何编写概念的。我见过的大多数例子都多少遵循了一个悠久的传统,即一切都围绕着一个假定的、通常不会被认为是引用类型的候选类型。例如,一些较旧的 Concepts-Lite 草稿(例如 N3580) mention concepts such as Container which have their roots in the SGI STL 甚至今天以 23.2 容器要求的形式在标准库中仍然存在。

一个明显的预转发参考标志是关联类型的描述如下:

Value type X::value_type The type of the object stored in a container. The value type must be Assignable, but need not be DefaultConstructible.

如果我们天真地将其转换为 Concepts-Lite,它可能看起来像:

template<typename X>
concept bool Container = requires(X x) {
   typename X::value_type;
   // other requirements...
};

在这种情况下,如果我们写

template<typename C>
    requires Container<C>
void example(C&& c);

然后我们有以下行为:

std::vector<int> v;

// fine
// this checks Container<std::vector<int>>, and finds
// std::vector<int>::value_type
example(std::move(v));

// not fine
// this checks Container<std::vector<int>&>, and
// references don't have member types
example(v);

有几种方式可以表达 value_type 优雅地处理这种情况的要求。例如。我们可以将要求调整为 typename std::remove_reference_t<X>::value_type

我相信委员会成员都知道这一情况。例如。 Andrew Sutton 在他的概念库中留下了一个 insightful comment 来展示确切的情况。他的首选解决方案是让概念定义适用于非引用类型,并删除约束中的引用。对于我们的示例:

template<typename C>
    // Sutton prefers std::common_type_t<C>,
    // effectively the same as std::decay_t<C>
    requires<Container<std::remove_reference_t<C>>>
void example(C&& c);

这是一件很难的事情。大多数情况下,当我们写概念时,我们希望专注于类型定义(我们可以用 T 做什么)而不是它的各种形式(const TT&T const& , ETC)。你一般问的是,"can I declare a variable like this? Can I add these things?"。无论参考资料或简历资格如何,这些问题往往都是有效的。除非他们不是。

通过转发,模板参数推导通常会为您提供超出形式的参数(引用和 cv 限定类型),因此您最终会询问有关错误类型的问题。 感叹。怎么办?

您要么尝试定义概念以适应这些形式,要么尝试获得核心类型。