C++ 20概念
什么是概念(Concepts)
C++20引入了一个重要的新特性:概念(Concepts)。概念是对模板参数的约束,它们可以用来限制模板参数必须满足的条件。通过使用概念,我们可以:
- 为模板参数设置明确的要求
- 在编译时进行类型检查
- 获得更清晰的错误消息
- 实现更直观的函数重载
在概念出现之前,C++模板错误通常非常复杂且难以理解,尤其对于初学者来说。现在通过概念,我们可以提供更友好的错误信息,明确告诉用户为什么某个类型不适合特定的模板。
基本语法
概念的定义语法如下:
cpp
template <typename T>
concept ConceptName = constraint-expression;
其中,constraint-expression
是一个返回布尔值的表达式,用于检查类型T
是否满足特定条件。
简单概念示例
让我们创建一个简单的概念,用于检查一个类型是否支持加法操作:
cpp
#include <iostream>
#include <string>
// 定义一个概念:类型必须支持加法操作
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
// 使用概念约束模板函数
template <Addable T>
T add(T a, T b) {
return a + b;
}
int main() {
// 正确:int 类型支持加法
std::cout << add(5, 3) << std::endl;
// 正确:std::string 类型支持加法
std::cout << add(std::string("Hello, "), std::string("C++20!")) << std::endl;
// 如果尝试使用不支持加法的类型,编译时会发生错误
// 例如,如果定义了一个不支持加法的类
// struct NoAdd {};
// std::cout << add(NoAdd{}, NoAdd{}) << std::endl; // 编译错误!
return 0;
}
输出:
8
Hello, C++20!
requires 表达式
requires
表达式是创建概念约束的强大工具。它有几种形式:
简单的 requires 表达式
cpp
template <typename T>
concept Incrementable = requires(T x) {
++x; // 要求 T 类型支持前缀增量操作
x++; // 要求 T 类型支持后缀增量操作
};
带类型要求的 requires 表达式
cpp
template <typename T>
concept HasValueType = requires {
typename T::value_type; // 要求 T 有一个名为 value_type 的嵌套类型
};
复合 requires 表达式
cpp
template <typename T>
concept Sortable = requires(T container) {
std::begin(container); // 要求容器支持 begin()
std::end(container); // 要求容器支持 end()
requires std::random_access_iterator<decltype(std::begin(container))>; // 要求迭代器是随机访问迭代器
};
标准库中的概念
C++20标准库提供了许多预定义的概念,位于<concepts>
头文件中。以下是一些常用的概念:
std::same_as
: 检查两个类型是否完全相同std::convertible_to
: 检查一个类型是否可转换为另一个类型std::integral
: 检查类型是否为整数类型std::floating_point
: 检查类型是否为浮点数类型std::invocable
: 检查类型是否可调用
使用标准库概念示例:
cpp
#include <iostream>
#include <concepts>
#include <string>
// 使用标准库概念
template <std::integral T>
T multiply(T a, T b) {
return a * b;
}
int main() {
// 正确:int 是整数类型
std::cout << multiply(5, 3) << std::endl;
// 错误:double 不是整数类型
// std::cout << multiply(5.5, 3.5) << std::endl;
// 错误:std::string 不是整数类型
// std::cout << multiply(std::string("Hello"), std::string("World")) << std::endl;
return 0;
}
输出:
15
概念的应用场景
1. 约束模板参数
cpp
#include <iostream>
#include <concepts>
#include <vector>
#include <list>
// 定义一个概念:容器必须支持随机访问
template <typename T>
concept RandomAccessContainer = requires(T container, typename T::size_type n) {
{ container[n] } -> std::convertible_to<typename T::value_type&>;
{ container.size() } -> std::convertible_to<typename T::size_type>;
};
// 只对支持随机访问的容器执行二分查找
template <RandomAccessContainer Container, typename T>
bool binary_search(const Container& container, const T& value) {
// 二分查找实现
// (简化实现,仅作演示)
std::cout << "执行二分查找\n";
return true;
}
// 对不支持随机访问的容器执行线性查找
template <typename Container, typename T>
requires (!RandomAccessContainer<Container>)
bool binary_search(const Container& container, const T& value) {
// 线性查找实现
// (简化实现,仅作演示)
std::cout << "执行线性查找\n";
return true;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::list<int> lst = {1, 2, 3, 4, 5};
binary_search(vec, 3); // 使用二分查找
binary_search(lst, 3); // 使用线性查找
return 0;
}
输出:
执行二分查找
执行线性查找
2. 更好的函数重载
在C++20之前,函数重载通常依赖于SFINAE(替换失败不是错误)技术,这种技术复杂且难以理解。使用概念,函数重载变得更加清晰:
cpp
#include <iostream>
#include <concepts>
#include <string>
// 使用概念进行函数重载
template <std::integral T>
void print_number(T value) {
std::cout << "整数: " << value << std::endl;
}
template <std::floating_point T>
void print_number(T value) {
std::cout << "浮点数: " << value << std::endl;
}
template <typename T>
requires (!std::integral<T> && !std::floating_point<T>)
void print_number(T value) {
std::cout << "其他类型: " << value << std::endl;
}
int main() {
print_number(42); // 调用整数版本
print_number(3.14); // 调用浮点数版本
print_number("Hello"); // 调用其他类型版本
return 0;
}
输出:
整数: 42
浮点数: 3.14
其他类型: Hello
3. 简化语法:缩写函数模板
C++20还引入了缩写函数模板语法,使用概念可以让代码更加简洁:
cpp
#include <iostream>
#include <concepts>
// 缩写函数模板语法
std::integral auto add(std::integral auto a, std::integral auto b) {
return a + b;
}
int main() {
std::cout << add(5, 3) << std::endl;
// std::cout << add(5.5, 3.5) << std::endl; // 错误:不是整数类型
return 0;
}
输出:
8
自定义复杂概念
在实际应用中,我们经常需要定义更复杂的概念,可以通过组合多个概念来实现:
cpp
#include <iostream>
#include <concepts>
#include <string>
#include <sstream>
// 定义一个可以转换为字符串的概念
template <typename T>
concept Stringable = requires(T a) {
{ std::to_string(a) } -> std::convertible_to<std::string>;
} || requires(T a) {
{ std::string(a) } -> std::convertible_to<std::string>;
} || requires(T a) {
// 支持流输出
{ std::declval<std::stringstream&>() << a } -> std::convertible_to<std::stringstream&>;
};
// 使用自定义概念
template <Stringable T>
std::string convert_to_string(const T& value) {
std::stringstream ss;
ss << value;
return ss.str();
}
struct Person {
std::string name;
int age;
friend std::ostream& operator<<(std::ostream& os, const Person& p) {
os << "Person{name='" << p.name << "', age=" << p.age << "}";
return os;
}
};
int main() {
std::cout << convert_to_string(42) << std::endl;
std::cout << convert_to_string(3.14159) << std::endl;
std::cout << convert_to_string("Hello") << std::endl;
std::cout << convert_to_string(std::string("World")) << std::endl;
Person p{"Alice", 30};
std::cout << convert_to_string(p) << std::endl;
return 0;
}
输出:
42
3.14159
Hello
World
Person{name='Alice', age=30}
实际案例:泛型数学库
下面是一个使用概念实现的简单泛型数学库示例:
cpp
#include <iostream>
#include <concepts>
#include <cmath>
// 定义数学运算所需的概念
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template <typename T>
concept SupportsAbsolute = requires(T a) {
{ std::abs(a) } -> std::convertible_to<T>;
};
template <typename T>
concept SupportsTrigonometry = std::floating_point<T>;
// 通用的数学函数库
namespace math {
// 绝对值函数
template <SupportsAbsolute T>
T abs(T value) {
return std::abs(value);
}
// 平方函数
template <Numeric T>
T square(T value) {
return value * value;
}
// 平方根函数,仅支持浮点类型
template <std::floating_point T>
T sqrt(T value) {
return std::sqrt(value);
}
// 三角函数,仅支持浮点类型
template <SupportsTrigonometry T>
T sin(T radians) {
return std::sin(radians);
}
template <SupportsTrigonometry T>
T cos(T radians) {
return std::cos(radians);
}
// 欧几里得距离
template <Numeric T>
auto distance(T x1, T y1, T x2, T y2) {
auto dx = x2 - x1;
auto dy = y2 - y1;
return std::sqrt(dx*dx + dy*dy);
}
}
int main() {
std::cout << "abs(-5): " << math::abs(-5) << std::endl;
std::cout << "square(4): " << math::square(4) << std::endl;
std::cout << "sqrt(16.0): " << math::sqrt(16.0) << std::endl;
std::cout << "sin(M_PI/2): " << math::sin(M_PI/2) << std::endl;
std::cout << "distance(0,0,3,4): " << math::distance(0, 0, 3, 4) << std::endl;
// 错误示例(如果取消注释):
// math::sqrt(16); // 错误:整数类型不支持
// math::sin(30); // 错误:整数类型不支持三角函数
return 0;
}
输出:
abs(-5): 5
square(4): 16
sqrt(16.0): 4
sin(M_PI/2): 1
distance(0,0,3,4): 5
概念的优势总结
使用概念的好处
- 更清晰的错误消息:概念提供了更容易理解的编译错误消息
- 文档作用:概念清楚地表达了模板参数必须满足的要求
- 提高代码可读性:代码更加自文档化,无需额外注释解释类型要求
- 改进的重载解析:基于概念的重载比基于SFINAE的重载更加清晰
- 编译时检查:在编译时而非运行时捕获类型错误
最佳实践
- 谨慎定义概念:概念应该是清晰、精确且有意义的
- 使用标准库概念:尽可能使用标准库提供的概念,而不是重新发明轮子
- 组合概念:使用逻辑运算符组合概念,而不是定义新的概念
- 良好的命名:使用描述性名称,让概念的用途一目了然
- 避免过度约束:只要求模板真正需要的功能,不要过度限制类型
练习
- 实现一个
Sortable
概念,检查一个类型是否可以被排序(提示:需要支持<
运算符和迭代) - 创建一个
Hashable
概念,检查类型是否可以用作std::unordered_map
的键 - 编写一个使用概念的泛型
print
函数,根据参数类型选择不同的打印格式 - 实现一个
Range
概念,检查一个类型是否表示一个范围(有开始和结束) - 创建一个简单的数学向量类,并使用概念确保只对向量类型执行向量运算
总结
C++20的概念是一项强大的特性,它极大地改进了C++的模板编程体验。概念通过提供清晰的类型约束,使代码更可读、更安全,同时提供了更友好的错误消息。随着概念的广泛应用,C++的泛型编程将变得更加易于理解和使用。
尽管概念的学习曲线可能较为陡峭,但掌握这一技术将使你能够编写更加健壮和可维护的C++代码。随着越来越多的库采用概念,理解并使用这一特性将变得越来越重要。
额外资源
- C++参考文档中的概念
- C++20标准文档
- 《Effective Modern C++》by Scott Meyers(虽然出版时间早于C++20,但对理解模板和类型约束有帮助)
- 《C++ Templates: The Complete Guide, 2nd Edition》by David Vandevoorde, Nicolai M. Josuttis, and Douglas Gregor