C++概念约束模板的基础认知
C++20之前,模板参数约束通常依赖SFINAE或者静态断言,这两种方式要么语法复杂,要么错误提示不友好。概念的出现让模板约束变得直观,它本质是一个编译期的谓词,用来描述模板参数需要满足的条件。当模板参数不符合概念要求时,编译器会直接给出清晰的错误信息,而不是输出一堆难以理解的模板实例化堆栈。
定义简单的概念
定义概念需要使用concept关键字,后面跟概念名称和约束表达式。约束表达式可以是类型特征、其他概念的组合,或者返回布尔值的编译期表达式。
#include <concepts>
#include <type_traits>
// 定义一个概念,要求类型是整数类型
template <typename T>
concept Integer = std::is_integral_v<T>;
// 定义一个概念,要求类型支持加法操作
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
使用概念约束函数模板
概念可以直接用在函数模板的参数声明位置,替代原来的typename或者class关键字,明确指定模板参数需要满足的条件。
基础函数模板约束
下面的示例定义了一个求和函数,要求传入的参数类型满足Addable概念,只有支持加法操作且结果可转换为自身类型的参数才能调用这个函数。
#include <iostream>
#include <string>
// 使用Addable概念约束模板参数T
template <Addable T>
T sum(T a, T b) {
return a + b;
}
int main() {
int x = 10, y = 20;
std::cout << sum(x, y) << std::endl; // 合法调用,int支持加法
std::string s1 = "hello", s2 = "world";
std::cout << sum(s1, s2) << std::endl; // 合法调用,string支持加法
// float f1 = 1.5, f2 = 2.5;
// sum(f1, f2); // 如果Addable概念要求结果严格是T类型,float可能不符合,具体看概念定义
return 0;
}
多个概念组合约束
可以通过&&和||运算符组合多个概念,实现更复杂的约束条件。比如要求类型既是整数类型,又支持加法操作。
// 组合Integer和Addable概念,要求类型既是整数又支持加法
template <typename T>
concept IntegerAddable = Integer<T> && Addable<T>;
// 约束模板参数为整数且支持加法的类型
template <IntegerAddable T>
T int_sum(T a, T b) {
return a + b;
}
int main() {
int a = 5, b = 3;
std::cout << int_sum(a, b) << std::endl; // 合法,int满足条件
// std::string s1 = "a", s2 = "b";
// int_sum(s1, s2); // 编译错误,string不是整数类型
return 0;
}
使用概念约束类模板
概念同样可以约束类模板的参数,确保类模板实例化时使用的类型符合要求,避免类内部使用不符合要求的类型导致编译错误。
#include <vector>
#include <concepts>
// 定义一个概念,要求类型可以默认构造
template <typename T>
concept DefaultConstructible = std::default_initializable<T>;
// 约束容器模板的参数T必须可以默认构造
template <DefaultConstructible T>
class MyContainer {
private:
std::vector<T> data;
public:
void add_default() {
data.emplace_back(); // 这里需要T可以默认构造,概念约束保证了这一点
}
};
int main() {
MyContainer<int> c1; // 合法,int可以默认构造
c1.add_default();
// MyContainer<int(*)()> c2; // 编译错误,函数指针类型不能默认构造,不符合概念要求
return 0;
}
模板进阶技巧:自定义复杂概念
除了使用标准库提供的概念,还可以根据业务需求自定义复杂概念,比如约束类型必须有特定的成员函数、特定的成员变量等。
约束成员函数存在
通过requires表达式可以检查类型是否包含指定的成员函数,比如下面的概念要求类型必须有size成员函数,且返回值是整数类型。
#include <concepts>
// 定义概念:类型必须有size成员函数,返回值可转换为size_t
template <typename T>
concept HasSizeMethod = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
};
// 约束模板参数为有size方法的类型
template <HasSizeMethod T>
std::size_t get_size(const T& obj) {
return obj.size();
}
约束模板参数的关系
概念还可以约束多个模板参数之间的关系,比如要求两个类型可以互相转换,或者一个类型是另一个类型的迭代器。
#include <concepts>
#include <iterator>
// 定义概念:要求It是Container的迭代器类型
template <typename Container, typename It>
concept IteratorOf = std::same_as<It, typename Container::iterator> ||
std::same_as<It, typename Container::const_iterator>;
// 约束迭代器属于对应的容器类型
template <typename Container, IteratorOf<Container> It>
It find_in_container(Container& c, It begin, It end) {
// 这里可以安全地使用begin和end遍历容器c
return begin;
}
概念约束的注意事项
- 概念是编译期检查的,不会引入运行时开销,和普通的模板约束性能一致。
- 概念的定义可以递归引用其他概念,方便复用已有的约束逻辑。
- 如果模板参数同时匹配多个重载的函数模板,编译器会优先选择约束更严格(满足更多概念条件)的版本。
- 使用概念时不需要额外引入复杂的头文件,标准库的概念都在
<concepts>头文件中,部分类型特征相关的概念也在<type_traits>中。
需要注意的是,概念是C++20的特性,使用的时候需要编译器支持C++20及以上标准,比如GCC 10+、Clang 13+、MSVC 2019+都可以正常使用概念特性。