C++ 20新特性之Concepts

简介: 在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪

为什么要引入Concepts

在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。

在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪,让编译器和程序员都能清晰地理解类型要求,从而高效地进行泛型编程。


Concepts的基本语法

在C++ 20中,我们可以使用concept关键字来定义一个概念。一个概念本质上是一个布尔表达式,用于检查一个类型是否满足特定的约束条件。在下面的示例代码中,我们使用requires子句定义了一个名为HasPlusOperator的概念。该概念会检查类型T是否支持加法操作,并且加法的结果可以转换为类型T自身。

#include <concepts>
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成员函数。

#include <concepts>
template<typename T>
concept Printable = requires(T t)
{
    // 检查是否存在void返回类型的Print成员函数
    { t.Print() } -> std::same_as<void>;
};


如何使用Concepts

定义好一个概念后,我们就可以在模板函数或模板类中使用它来指定类型约束。单纯讲理论比较枯燥,也难以理解。我们通过下面的示例代码,来介绍如何使用Concepts。

#include <concepts>
#include <iostream>
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中,并没有直接的“概念继承”的语法,但我们可以通过约束的组合和扩展来实现类似的效果。如果一个类型需要同时满足多个概念,则可以使用&&操作符来组合它们。如果类型只需要满足多个概念中的一个或多个,可以使用||操作符。

比如,我们想要一个既能加也能减的类型,可以参考下面这样来定义。

#include <concepts>
#include <iostream>
#include <concepts>
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函数。


💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注小红书和微信公众号“希望睿智”。

相关文章
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(三)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(二)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析多态机制(一)
【C++】面向对象编程的三大特性:深入解析多态机制
|
1月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
26天前
|
C++
C++ 20新特性之结构化绑定
在C++ 20出现之前,当我们需要访问一个结构体或类的多个成员时,通常使用.或->操作符。对于复杂的数据结构,这种访问方式往往会显得冗长,也难以理解。C++ 20中引入的结构化绑定允许我们直接从一个聚合类型(比如:tuple、struct、class等)中提取出多个成员,并为它们分别命名。这一特性大大简化了对复杂数据结构的访问方式,使代码更加清晰、易读。
32 0
|
1月前
|
存储 编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(三)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
编译器 C++
【C++】面向对象编程的三大特性:深入解析继承机制(二)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
安全 程序员 编译器
【C++】面向对象编程的三大特性:深入解析继承机制(一)
【C++】面向对象编程的三大特性:深入解析继承机制
|
1月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
6天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
29 4