概念的第一印象



我决定处理新的C ++ 20功能-概念。

概念(或讲俄语的Wiki所写的概念)是一个非常有趣且有用的功能,长期以来一直缺乏。

本质上,它是在输入模板参数。

C ++ 20之前的模板的主要问题是您可以替换其中的任何内容,包括根本没有设计的内容。也就是说,模板系统是完全没有类型的。结果,将错误的参数传递给模板时,出现了难以置信的漫长且完全无法读取的错误消息。他们试图借助不同的语言技巧来解决这个问题,我什至不想提及(尽管我不得不处理)。

设计概念是为了纠正这种误解。他们在模板中添加了打字系统,功能非常强大。现在,在了解了该系统的功能之后,我开始研究Internet上的可用资料。

坦白说,我有点震惊:) C ++是一种已经很复杂的语言,但至少有一个借口:它发生了。设计语言时,发现而不是放下对模板的元编程。然后,在开发该语言的下一版本时,由于世界上编写了许多代码,因此他们不得不适应这种“发现”。概念从根本上来说是一个新的机会。而且,在我看来,它们的实现中已经存在一些不透明性。也许这是需要考虑大量继承功能的结果吗?让我们尝试弄清楚...

一般信息


概念是基于模板语法的新语言实体。概念具有名称,参数和主体-谓词,该谓词根据概念的参数返回常数(即在编译阶段计算的)逻辑值。像这样:

template<int I> 
concept Even = I % 2 == 0;  

template<typename T>
concept FourByte = sizeof(T)==4;

从技术上讲,概念与像bool这样的模板constexpr表达式非常相似:

template<int I>
constexpr bool EvenX = I % 2 == 0; 

template<typename T>
constexpr bool FourByteX = sizeof(T)==4;

您甚至可以在常用表达式中使用概念:

bool b1 = Even<2>; 

使用


该概念的主要思想是可以使用它们代替模板中的typename或class关键字。像元类型(“类型的类型”)。因此,静态类型被引入到模板中。

template<FourByte T>
void foo(T const & t) {}

现在,如果我们将int用作模板参数,那么绝大多数情况下的代码都可以编译;如果加倍,则将发出简短易懂的错误消息。简单清晰地输入模板,到目前为止一切正常。

要求


这是一个新的“上下文” C ++ 20关键字,具有双重用途:require子句和require表达式。如稍后所示,这种奇怪的关键字节省导致一些混乱。

需要表达


首先,考虑需要表达。这个想法非常好:这个词在括号中有一个块,其中的代码将进行编译评估。的确,那里的代码不应该用C ++编写,而应该使用一种接近C ++的特殊语言,但要有自己的特点(这是第一个奇怪的事情,很可能只编写C ++代码)。

如果代码正确-require表达式返回true,否则返回false。当然,代码本身永远也不会进入代码生成,就像sizeof或decltype的表达式一样。

不幸的是,这个词是上下文的,仅在模板内部有效,也就是说,在模板外部无法编译:

bool b = requires { 3.14 >> 1; };

并在模板中-请:

template<typename T>
constexpr bool Shiftable = requires(T i) { i>>1; };

它将起作用:

bool b1 = Shiftable<int>; // true
bool b2 = Shiftable<double>; // false

require表达式的主要用途是概念的创建。例如,这是检查类型中字段和方法是否存在的方式。一个非常受欢迎的案例。

template <typename T>
concept Machine = 
  requires(T m) {  //   `m` ,   Machine
	m.start();     //    `m.start()` 
	m.stop();      //   `m.stop()`
};  

顺便说一下,必须在括号中的require表达式中声明测试代码中可能需要的所有变量(不仅是模板参数)。由于某些原因,根本不可能声明变量。

内部类型检查要求


这就是要求代码和标准C ++之间的区别所在。为了检查返回的类型,使用了一种特殊的语法:将对象放在大括号中,放置一个箭头,然后在其后写出该类型必须满足的概念。此外,不允许直接使用类型。

检查函数的返回值可以转换为int:

requires(T v, int i) {
  { v.f(i) } -> std::convertible_to<int>;
}  

检查返回函数是否​​正好是int:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>; 
}  

(std :: same_as和std :: convertible_to是标准库中的概念)。

如果不使用大括号检查其类型的表达式,则编译器将无法理解它们的要求,而是将整个字符串解释为需要检查以进行编译的单个表达式。

需要里面要求


require关键字在require表达式内部具有特殊含义。嵌套的require-expressions(已经不带花括号)不进行编译检查,而是检查是否为true或false。如果发现这样的表达式为假,则封闭的表达式立即为假(并且进一步的编译分析将中断)。一般形式:

requires { 
  expression;         // expression is valid
  requires predicate; // predicate is true
};

作为谓词,例如,可以使用先前定义的概念或类型特征。例:

requires(Iter it) {
  //     (   Iter   *  ++)
  *it++;
 
  //    -  
  requires std::convertible_to<decltype(*it++), typename Iter::value_type>;
 
  //    -  
  requires std::is_convertible_v<decltype(*it++), typename Iter::value_type>;
}

同时,使用括号中的代码允许嵌套的require-expressions进行有效性检查。但是,如果您仅在另一个内部编写一个require-expression,则将仅检查嵌套表达式(包括嵌套的require关键字在内的所有内容)的有效性:

requires (T v) { 
  requires (typename T::value_type x) { ++x; }; //     , 
												//     !
};  

因此,出现了一个带有双重的奇怪形式:

requires (T v) { 
  requires requires (typename T::value_type x) { ++x; }; //       "++x"
};  

这是来自“ requires”的一个有趣的转义序列。

顺便说一句,这两个条件的另一个组合是此时间子句(请参见下文)和表达式:

template <typename T>
  requires requires(T x, T y) { bool(x < y); }
bool equivalent(T const& x, T const& y)
{
  return !(x < y) && !(y < x);
};

要求条款


现在,让我们继续使用需求一词的另一种用法-声明模板类型的限制。这是使用概念名称代替类型名称的替代方法。在下面的示例中,所有三种方法都是等效的:

//  require
template<typename Cont>
	requires Sortable<Cont>
void sort(Cont& container);

//   require (  )
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;

//    typename
template<Sortable Cont>
void sort(Cont& container)  

require声明可以使用逻辑运算符组合的多个谓词。

template <typename T>
  requires is_standard_layout_v<T> && is_trivial_v<T>
void fun(T v); 
 
int main()
{
  std::string s;
 
  fun(1);  // ok
  fun(s);  // compiler error
}

但是,由于发生编译错误,只需反转条件之一:

template <typename T>
  requires is_standard_layout_v<T> && !is_trivial_v<T>
void fun(T v); 

这是一个不会编译的示例

template <typename T>
  requires !is_trivial_v<T>
void fun(T v);	

原因是解析某些表达式时出现歧义。例如,在这样的模板中:

template <typename T> 
  requires (bool)&T::operator short unsigned int foo();

尚不清楚将unsigned归于什么-运算符或foo()函数的原型。因此,开发人员决定在没有括号的情况下(作为参数require子句使用),只能使用非常有限的实体集-true或false文字,bool类型的字段名称,其形式为value,value,T :: value,ns :: trait :: value,类型为Concept的概念名称,并需要表达式。其他所有内容都应放在括号中:

template <typename T>
  requires (!is_trivial_v<T>)
void fun(T v);

现在关于require子句中的谓词功能


考虑另一个例子。

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v); 

在此示例中,require使用取决于嵌套value_type类型的特征。事先不知道任意类型是否具有可以传递给模板的嵌套类型。例如,如果将简单的int类型传递给这样的模板,则会出现编译错误,但是,如果我们对模板进行了两种特殊化,则不会有错误。只会选择另一个专业。

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v) { std::cout << "1"; } 
 
template <typename T>
void fun(T v) { std::cout << "2"; } 
 
int main()
{
  fun(1);  // displays: "2"
}

因此,不仅当require子句谓词返回false时,而且在事实证明它不正确时,也将放弃专门化。

谓词周围的括号是一个重要的提醒,即在require子句中,谓词的逆与谓词本身不是相反的。所以,

requires is_trivial_v<typename T::value_type> 

表示特征正确,并返回true。其中

!is_trivial_v<typename T::value_type> 

表示“特征正确且返回假”
,第一个谓词真正逻辑反转不是(“特征正确且返回真”)==“特征不正确或返回假” –这是通过概念的显式定义以稍微复杂的方式实现的:

template <typename T>
concept value_type_valid_and_trivial 
  = is_trivial_v<typename T::value_type>; 
 
template <typename T>
  requires (!value_type_valid_and_trivial<T>)
void fun(T v); 

连词和析取词


逻辑合取运算符和析取运算符看起来像往常一样,但实际上工作方式与普通C ++略有不同。

考虑两个非常相似的代码段。

第一个是不带括号的谓词:

template <typename T, typename U>
  requires std::is_trivial_v<typename T::value_type>
		|| std::is_trivial_v<typename U::value_type>
void fun(T v, U u); 

第二个是带有方括号的:

template <typename T, typename U>
  requires (std::is_trivial_v<typename T::value_type>
		 || std::is_trivial_v<typename U::value_type>)
void fun(T v, U u); 

区别仅在于方括号。但是正因为如此,在第二个模板中,没有两个约束由“需求子句”联合,而是一个约束与通常的逻辑“或”联合。

该差异如下。考虑代码

std::optional<int> oi {};
int i {};
fun(i, oi);

在这里,模板由int和std ::可选类型实例化。

在第一种情况下,int :: value_type类型无效,因此不满足第一个限制。

但是类型可选:: :: value_type是有效的,第二个特征返回true,并且由于约束之间存在OR运算符,因此整个谓词都可以满足。

在第二种情况下,这是一个包含无效类型的单个表达式,由于该表达式通常无效,因此不满足该谓词。因此,简单的括号会在不知不觉中改变正在发生的事情的含义。

结论


当然,此处未显示概念的所有功能。我只是没有走得更远。但是,作为第一印象,这是一个非常有趣的想法,并且有些奇怪的实现。重复的有趣语法要求,这确实令人困惑。英语中真的有那么少的单词,您不得不将一个单词用于完全不同的目的吗?

带有编译代码的想法绝对是个好主意。它甚至有点类似于语法宏中的“准引用”。但是,值得混用检查返回类型的特殊语法吗?恕我直言,为此,只需要做一个单独的关键字。

在一个堆中隐式混合使用“ true / false”和“ compiles / not compile”的概念,因此,带有括号的笑话也是错误的。这些是不同的概念,它们必须严格存在于不同的上下文中(尽管我了解它的来源-SFINAE规则,未编译的代码只是默默地排除了特殊性)。但是,如果这些概念的目标是使代码尽可能明确,那么将所有这些隐式内容拖到新功能中是否值得?

这篇文章主要基于
akrzemi1.wordpress.com/2020/01/29/requires-expression
akrzemi1.wordpress.com/2020/03/26/requires-clause
(有更多示例和有趣功能)撰写
,而我的补充来自其他来源,
可以检查所有示例wandbox.org

All Articles