我决定处理新的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>;
bool b2 = Shiftable<double>;
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;
requires predicate;
};
作为谓词,例如,可以使用先前定义的概念或类型特征。例:requires(Iter it) {
*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; };
};
这是来自“ 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);
};
要求条款
现在,让我们继续使用需求一词的另一种用法-声明模板类型的限制。这是使用概念名称代替类型名称的替代方法。在下面的示例中,所有三种方法都是等效的:
template<typename Cont>
requires Sortable<Cont>
void sort(Cont& container);
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;
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);
fun(s);
}
但是,由于发生编译错误,只需反转条件之一: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);
}
因此,不仅当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-expressionakrzemi1.wordpress.com/2020/03/26/requires-clause(有更多示例和有趣功能)撰写,而我的补充来自其他来源,可以检查所有示例wandbox.org