哈喽,各位好!今天咱们聊聊 C++ 编译期 concept 的嵌套与组合,这玩意儿听起来有点高大上,但其实就像搭积木,把简单的东西组合起来,就能构建出复杂而强大的类型约束体系。别怕,我会用大白话把这事儿给各位掰开了揉碎了讲清楚。
一、concept 是啥?为啥要用它?
想象一下,你写了一个函数,这个函数要求传入的参数必须得支持加法操作。以前咋办?可能就是在函数里做一些运行时检查,比如判断是不是数字类型。但这样效率不高,而且错误要等到运行的时候才能发现。
concept 的出现就是为了解决这个问题。它允许你在编译期就对模板参数进行约束,只有满足特定条件的类型才能通过编译。这就像给函数参数套上了一层“类型过滤器”,不合格的直接拒之门外。
简单来说,concept 就是一个编译期的谓词(predicate),用来判断类型是否满足某种条件。
二、concept 的基本用法:搭积木的“砖头”
先来个最简单的例子,定义一个 Addable 的 concept,要求类型 T 支持加法操作:
#include <iostream>
#include <concepts>
template<typename T>
concept Addable = requires(T a, T b) {
    a + b; // 要求 T 类型的对象 a 和 b 可以进行加法运算
};
template<typename T>
requires Addable<T>
T add(T a, T b) {
    return a + b;
}
int main() {
    std::cout << add(1, 2) << std::endl;   // OK,int 满足 Addable
    //std::cout << add("hello", "world") << std::endl; // 编译错误,std::string 不满足 Addable
    return 0;
}这段代码里,requires 关键字后面跟着的就是 concept,它告诉编译器,只有满足 Addable<T> 的类型 T 才能被用来实例化 add 函数。如果传入的类型不支持加法,编译器会直接报错,省去了运行时的检查。
语法小贴士:
- template<typename T>:声明一个模板参数- T。
- concept Addable = ...:定义一个名为- Addable的- concept。
- requires(T a, T b) { ... }:- requires子句,用来描述类型- T必须满足的条件。括号里声明的是本地参数,只在- requires子句内部有效,用来辅助描述类型约束。
- a + b;:要求- T类型的对象- a和- b必须能够进行加法运算。注意,这里只是检查表达式是否合法,并不实际执行加法操作。
三、concept 的嵌套:砖头之上再盖砖
现在,我们有了基本的 concept,可以开始构建更复杂的约束了。concept 可以嵌套使用,就像搭积木一样,一层一层往上垒。
比如,我们想要定义一个 Incrementable 的 concept,要求类型 T 既要 Addable,还要支持自增操作:
#include <iostream>
#include <concepts>
template<typename T>
concept Addable = requires(T a, T b) {
    a + b;
};
template<typename T>
concept Incrementable = Addable<T> && requires(T a) {
    a++; // 要求 T 类型的对象 a 可以进行自增运算
    ++a; // 要求 T 类型的对象 a 可以进行前置自增运算
};
template<typename T>
requires Incrementable<T>
T increment(T a) {
    return ++a;
}
int main() {
    int i = 1;
    std::cout << increment(i) << std::endl; // OK,int 满足 Incrementable
    //std::string s = "hello";
    //std::cout << increment(s) << std::endl; // 编译错误,std::string 不满足 Incrementable (虽然 std::string 支持加法,但不支持自增)
    return 0;
}在这个例子中,Incrementable concept 依赖于 Addable concept。只有同时满足 Addable 和自增操作的类型,才会被认为是 Incrementable。
嵌套的两种方式:
- 直接引用:  concept Incrementable = Addable<T> && ...这种方式最直接,把Addable<T>当作一个整体来使用。
- 
requires表达式内部引用: 你也可以在requires表达式内部使用其他的concept:template<typename T> concept Incrementable = requires(T a) { requires Addable<T>; // 在 requires 表达式内部引用 Addable a++; ++a; };这两种方式效果基本相同,选择哪种取决于你的代码风格和可读性。 
四、concept 的组合:更灵活的类型约束
除了嵌套,concept 还可以通过逻辑运算符进行组合,比如 && (与)、|| (或)、! (非)。这使得我们可以构建出更加灵活和复杂的类型约束。
1. && (与) 组合:
上面 Incrementable 的例子已经展示了 && 的用法,它要求类型必须同时满足多个 concept。
2. || (或) 组合:
假设我们想要定义一个 Number concept,要求类型要么是整数类型,要么是浮点数类型:
#include <iostream>
#include <concepts>
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<typename T>
requires Number<T>
T abs_value(T a) {
    if (a < 0) {
        return -a;
    }
    return a;
}
int main() {
    std::cout << abs_value(10) << std::endl;   // OK,int 满足 Number
    std::cout << abs_value(3.14) << std::endl; // OK,double 满足 Number
    //std::cout << abs_value("hello") << std::endl; // 编译错误,std::string 不满足 Number
    return 0;
}在这个例子中,Number concept 使用了 || 运算符,表示类型 T 只要满足 std::integral<T> 或者 std::floating_point<T> 中的任意一个,就被认为是 Number。
3. ! (非) 组合:
有时候,我们想要排除某些类型。比如,我们想要定义一个 NonStringType concept,要求类型不是 std::string:
#include <iostream>
#include <concepts>
#include <string>
template<typename T>
concept NonStringType = !std::same_as<T, std::string>;
template<typename T>
requires NonStringType<T>
void print_type(T a) {
    std::cout << "Type is not std::string" << std::endl;
}
int main() {
    print_type(10);         // OK,int 满足 NonStringType
    print_type(3.14);       // OK,double 满足 NonStringType
    //print_type(std::string("hello")); // 编译错误,std::string 不满足 NonStringType
    return 0;
}在这个例子中,NonStringType concept 使用了 ! 运算符,表示类型 T 不能是 std::string。
五、更高级的 concept 用法:requires 表达式的威力
requires 表达式的功能远不止判断简单的表达式是否合法。它还可以:
- 检查成员的存在性:  比如,检查类型 T是否有某个特定的成员函数。
- 检查返回值类型:  比如,检查类型 T的某个成员函数的返回值类型是否满足某种条件。
- 使用 decltype推导类型: 利用decltype可以推导出表达式的类型,然后对这个类型进行约束。
1. 检查成员的存在性:
假设我们想要定义一个 HasSize concept,要求类型 T 必须有一个名为 size 的成员函数,并且该函数可以被调用:
#include <iostream>
#include <concepts>
template<typename T>
concept HasSize = requires(T a) {
    a.size(); // 要求 T 类型的对象 a 必须有一个名为 size 的成员函数,并且可以被调用
};
struct MyContainer {
    int size() const { return 10; }
};
struct MyOtherContainer {
    // 没有 size 成员函数
};
template<typename T>
requires HasSize<T>
int get_size(T a) {
    return a.size();
}
int main() {
    MyContainer c;
    std::cout << get_size(c) << std::endl; // OK,MyContainer 满足 HasSize
    //MyOtherContainer oc;
    //std::cout << get_size(oc) << std::endl; // 编译错误,MyOtherContainer 不满足 HasSize
    return 0;
}2. 检查返回值类型:
假设我们想要定义一个 SizeReturnTypeIsInt concept,要求类型 T 的 size 成员函数的返回值类型必须是 int:
#include <iostream>
#include <concepts>
template<typename T>
concept SizeReturnTypeIsInt = requires(T a) {
    { a.size() } -> std::same_as<int>; // 要求 a.size() 的返回值类型必须是 int
};
struct MyContainer {
    int size() const { return 10; }
};
struct MyOtherContainer {
    size_t size() const { return 10; } // 返回值类型是 size_t,不是 int
};
template<typename T>
requires SizeReturnTypeIsInt<T>
int get_size(T a) {
    return a.size();
}
int main() {
    MyContainer c;
    std::cout << get_size(c) << std::endl; // OK,MyContainer 满足 SizeReturnTypeIsInt
    //MyOtherContainer oc;
    //std::cout << get_size(oc) << std::endl; // 编译错误,MyOtherContainer 不满足 SizeReturnTypeIsInt
    return 0;
}语法解释:
- { a.size() } -> std::same_as<int>;:这个语法表示- a.size()的返回值类型必须和- int类型相同。- {}用来创建一个 转换后的表达式,它将表达式的结果转换为- void类型,但同时保留了表达式的类型信息,可以用于类型检查。
3. 使用 decltype 推导类型:
假设我们想要定义一个 Multipliable concept,要求类型 T 和 U 可以相乘,并且相乘的结果可以转换为 double 类型:
#include <iostream>
#include <concepts>
template<typename T, typename U>
concept Multipliable = requires(T a, U b) {
    { a * b } -> std::convertible_to<double>; // 要求 a * b 的结果可以转换为 double 类型
};
template<typename T, typename U>
requires Multipliable<T, U>
double multiply(T a, U b) {
    return a * b;
}
int main() {
    std::cout << multiply(10, 2.5) << std::endl; // OK,int 和 double 相乘的结果可以转换为 double
    //std::cout << multiply("hello", 10) << std::endl; // 编译错误,std::string 和 int 不能相乘
    return 0;
}语法解释:
- { a * b } -> std::convertible_to<double>;:这个语法表示- a * b的结果可以隐式转换为- double类型。- std::convertible_to<double>是一个标准库提供的- concept,用来检查类型是否可以转换为指定的类型。
六、实战案例:构建一个简单的迭代器 concept
现在,我们用所学的知识来构建一个稍微复杂一点的例子:一个简单的迭代器 concept。
#include <iostream>
#include <concepts>
template<typename I>
concept Iterator = requires(I i) {
    typename std::iter_value_t<I>;   // I 必须有一个 value_type
    typename std::iter_reference_t<I>; // I 必须有一个 reference 类型
    typename std::iter_difference_t<I>; // I 必须有一个 difference_type
    { *i } -> std::same_as<std::iter_reference_t<I>>; // *i 的返回值类型必须是 reference 类型
    ++i;                                           // I 必须支持自增操作
    i++;                                           // I 必须支持后置自增操作
};
template<typename I>
requires Iterator<I>
void print_iterator_value(I it) {
    std::cout << *it << std::endl;
}
#include <vector>
int main() {
    std::vector<int> v = {1, 2, 3};
    print_iterator_value(v.begin()); // OK,std::vector<int>::iterator 满足 Iterator
    //int i = 10;
    //print_iterator_value(i); // 编译错误,int 不满足 Iterator
    return 0;
}这个 Iterator concept 检查了类型 I 是否满足迭代器的一些基本要求:
- 必须有 value_type、reference和difference_type这些类型别名。
- 必须支持解引用操作 (*i),并且返回值类型必须是reference类型。
- 必须支持自增操作 (++i和i++)。
七、总结与建议
concept 是 C++20 引入的一项强大的特性,它允许我们在编译期对模板参数进行约束,提高代码的类型安全性和可读性。通过 concept 的嵌套和组合,我们可以构建出复杂而灵活的类型约束体系。
一些建议:
- 从小处着手:  刚开始学习 concept的时候,可以从简单的例子入手,逐步掌握其基本用法。
- 多看标准库的 concept: 标准库中定义了大量的concept,可以参考它们的实现方式,学习如何使用concept来约束类型。
- 合理使用 concept: 不要过度使用concept,只在需要的时候才使用它。过度使用concept可能会使代码变得过于复杂。
- 善用编译器的错误信息:  当 concept约束不满足时,编译器会给出详细的错误信息,仔细阅读这些错误信息,可以帮助你快速定位问题。
希望今天的讲解能够帮助大家更好地理解 C++ 编译期 concept 的嵌套与组合。 掌握了 concept,你的 C++ 代码将会更加健壮和优雅! 感谢各位的观看, 下次再见!