为什么要引入Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。
在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪,让编译器和程序员都能清晰地理解类型要求,从而高效地进行泛型编程。
Concepts的基本语法
在C++ 20中,我们可以使用concept关键字来定义一个概念。一个概念本质上是一个布尔表达式,用于检查一个类型是否满足特定的约束条件。在下面的示例代码中,我们使用requires子句定义了一个名为HasPlusOperator的概念。该概念会检查类型T是否支持加法操作,并且加法的结果可以转换为类型T自身。
template<typename T> concept HasPlusOperator = requires(T a, T b) { // 要求a + b的结果可以转换为T类型 { a + b } -> std::convertible_to<T>; };
为了进一步理解Concepts,我们再举一个例子。在这个例子中,我们可以编写任意合法的C++表达式,并使用->操作符来指定表达式的返回类型(如果需要的话)。该概念会检查类型为T的对象t是否具有一个返回类型为void的Print成员函数。
template<typename T> concept Printable = requires(T t) { // 检查是否存在void返回类型的Print成员函数 { t.Print() } -> std::same_as<void>; };
如何使用Concepts
定义好一个概念后,我们就可以在模板函数或模板类中使用它来指定类型约束。单纯讲理论比较枯燥,也难以理解。我们通过下面的示例代码,来介绍如何使用Concepts。
template<typename T> concept Printable = requires(T t) { // 检查是否存在void返回类型的Print成员函数 { t.Print() } -> std::same_as<void>; }; template<Printable T> void PrintObject(T obj) { obj.Print(); } struct MyObject { void Print() const { std::cout << "MyObject" << std::endl; } }; int main() { MyObject obj; PrintObject(obj); // 编译错误:int类型没有Print成员函数 PrintObject(66); return 0; }
在上面的例子中,我们定义了一个名为PrintObject的模板函数。它接受一个类型为T的参数,并要求T必须满足Printable概念。然后,我们创建了一个名为MyObject的类,该类具有一个名为Print的成员函数,且返回void类型。在main函数中,我们创建了一个MyObject对象,并将其传递给PrintObject函数。由于MyObject满足Printable概念,因此代码可以成功编译并运行。然而,如果我们尝试将一个整数66传递给PrintObject函数,编译器将会报错,因为整数类型不满足Printable概念。
Concepts的复合与继承
Concepts可以组合使用,形成更复杂的约束条件,这就是复合Concept。在C++ 20中,并没有直接的“概念继承”的语法,但我们可以通过约束的组合和扩展来实现类似的效果。如果一个类型需要同时满足多个概念,则可以使用&&操作符来组合它们。如果类型只需要满足多个概念中的一个或多个,可以使用||操作符。
比如,我们想要一个既能加也能减的类型,可以参考下面这样来定义。
template<typename T> concept HasPlusOperator = requires(T a, T b) { // 要求a + b的结果可以转换为T类型 { a + b } -> std::convertible_to<T>; }; template<typename T> concept HasMinusOperator = requires(T a, T b) { // 要求a - b的结果可以转换为T类型 { a - b } -> std::convertible_to<T>; }; template<typename T> concept AddableAndSubtractable = HasPlusOperator<T> && HasMinusOperator<T>; template<AddableAndSubtractable T> T Calculate(T a, T b) { return a * b - (a + b); } int main() { std::cout << Calculate(5, 6) << std::endl; return 0; }
在上面的例子中,AddableAndSubtractable概念要求类型T必须同时支持加法运算(由HasPlusOperator<T>表示)和减法运算(由HasMinusOperator<T>表示)。接着,我们定义了一个名为Calculate的模板函数。它接受两个类型为T的参数,并要求T必须满足AddableAndSubtractable概念。最后,我们在main函数中调用了Calculate函数。
💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注小红书和微信公众号“希望睿智”。