第十二章
约 2035 字大约 7 分钟
2025-05-28
本章概述
C++20引入了Concepts,这是模板编程的一个重大改进。Concepts提供了一种声明式的方式来约束模板参数,使模板代码更加清晰、易读,并提供更好的错误信息。
学习目标
- 理解Concepts的基本概念和语法
- 掌握标准库提供的Concepts
- 学会定义自定义Concepts
- 了解requires表达式的用法
- 掌握约束模板的最佳实践
12.1 Concepts基础
什么是Concepts
Concepts是对模板参数的命名约束。它们允许我们:
- 明确表达模板参数的要求
- 提供更清晰的错误信息
- 改善代码的可读性和可维护性
- 实现更好的重载决议
基本语法
// 定义concept
template<typename T>
concept ConceptName = 约束表达式;
// 使用concept约束模板
template<ConceptName T>
void function(T value) { /* ... */ }
// 或者使用requires子句
template<typename T>
requires ConceptName<T>
void function(T value) { /* ... */ }
简单示例
#include <concepts>
// 定义一个简单的concept
template<typename T>
concept Integral = std::is_integral_v<T>;
// 使用concept约束函数模板
template<Integral T>
T add(T a, T b) {
return a + b;
}
// 使用示例
int result1 = add(5, 3); // OK,int是整数类型
// double result2 = add(5.0, 3.0); // 编译错误,double不是整数类型
12.2 标准库Concepts
核心语言Concepts
C++20标准库在<concepts>
头文件中提供了许多有用的concepts:
基本Concepts
#include <concepts>
// 类型关系
std::same_as<T, U> // T和U是相同类型
std::derived_from<T, U> // T派生自U
std::convertible_to<T, U> // T可以转换为U
// 数值类型
std::integral<T> // T是整数类型
std::signed_integral<T> // T是有符号整数类型
std::unsigned_integral<T> // T是无符号整数类型
std::floating_point<T> // T是浮点类型
// 可调用性
std::invocable<F, Args...> // F可以用Args...调用
std::predicate<F, Args...> // F是返回bool的谓词
对象Concepts
// 对象属性
std::destructible<T> // T可以被销毁
std::constructible_from<T, Args...> // T可以从Args...构造
std::default_initializable<T> // T可以默认初始化
std::move_constructible<T> // T可以移动构造
std::copy_constructible<T> // T可以复制构造
// 赋值
std::assignable_from<T, U> // T可以从U赋值
std::swappable<T> // T支持swap操作
迭代器Concepts
#include <iterator>
// 迭代器类型
std::input_iterator<T> // 输入迭代器
std::output_iterator<T, U> // 输出迭代器
std::forward_iterator<T> // 前向迭代器
std::bidirectional_iterator<T> // 双向迭代器
std::random_access_iterator<T> // 随机访问迭代器
// 范围
std::ranges::range<T> // T是范围类型
std::ranges::sized_range<T> // T是有大小的范围
12.3 自定义Concepts
简单约束
// 检查类型是否有特定成员函数
template<typename T>
concept HasSize = requires(T t) {
t.size();
};
// 检查类型是否支持特定操作
template<typename T>
concept Addable = requires(T a, T b) {
a + b;
};
// 组合多个约束
template<typename T>
concept Container = requires(T t) {
t.begin();
t.end();
t.size();
typename T::value_type;
};
复杂约束
// 检查返回类型
template<typename T>
concept Numeric = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
{ a - b } -> std::convertible_to<T>;
{ a * b } -> std::convertible_to<T>;
{ a / b } -> std::convertible_to<T>;
};
// 检查嵌套类型
template<typename T>
concept IteratorLike = requires {
typename T::value_type;
typename T::difference_type;
typename T::iterator_category;
} && std::input_iterator<T>;
// 检查静态成员
template<typename T>
concept HasStaticMember = requires {
T::static_member;
{ T::static_function() } -> std::same_as<int>;
};
12.4 requires表达式
基本语法
requires (参数列表) {
要求序列
}
四种要求类型
1. 简单要求
template<typename T>
concept Printable = requires(T t) {
std::cout << t; // 简单要求:表达式必须有效
};
2. 类型要求
template<typename T>
concept HasValueType = requires {
typename T::value_type; // 类型要求:类型必须存在
};
3. 复合要求
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>; // 复合要求:表达式有效且返回类型满足约束
{ a == b } -> std::same_as<bool>;
};
4. 嵌套要求
template<typename T>
concept EvenNumber = std::integral<T> && requires(T t) {
requires (t % 2 == 0); // 嵌套要求:编译时条件
};
12.5 约束模板的方式
1. 模板参数约束
// 直接约束模板参数
template<std::integral T>
void process(T value) { /* ... */ }
// 多个约束
template<std::integral T, std::floating_point U>
auto calculate(T a, U b) { /* ... */ }
2. requires子句
// 在模板参数后使用requires
template<typename T>
requires std::integral<T>
void process(T value) { /* ... */ }
// 在函数声明后使用requires
template<typename T>
void process(T value) requires std::integral<T> { /* ... */ }
// 复杂约束
template<typename T>
requires std::integral<T> && (sizeof(T) >= 4)
void process(T value) { /* ... */ }
3. 缩写函数模板
// C++20缩写语法
void process(std::integral auto value) {
// value的类型自动推导为满足std::integral的类型
}
// 等价于
template<std::integral T>
void process(T value) { /* ... */ }
12.6 实际应用示例
数学库示例
#include <concepts>
#include <type_traits>
// 定义数学类型concept
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
template<typename T>
concept SignedArithmetic = Arithmetic<T> && std::is_signed_v<T>;
// 安全的数学运算
template<Arithmetic T>
constexpr T abs(T value) {
if constexpr (std::is_signed_v<T>) {
return value < 0 ? -value : value;
} else {
return value;
}
}
// 向量类型concept
template<typename T>
concept Vector = requires(T v) {
{ v.x } -> std::convertible_to<double>;
{ v.y } -> std::convertible_to<double>;
{ v.z } -> std::convertible_to<double>;
};
// 向量运算
template<Vector V>
double magnitude(const V& v) {
return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
容器算法示例
// 定义容器concept
template<typename C>
concept Container = requires(C c) {
c.begin();
c.end();
c.size();
typename C::value_type;
typename C::iterator;
};
template<typename C>
concept SortableContainer = Container<C> && requires(C c) {
std::sort(c.begin(), c.end());
};
// 通用查找函数
template<Container C, typename T>
requires std::equality_comparable_with<typename C::value_type, T>
auto find_element(const C& container, const T& value) {
return std::find(container.begin(), container.end(), value);
}
// 安全的容器访问
template<Container C>
requires requires(C c, size_t i) { c.at(i); }
auto safe_get(const C& container, size_t index)
-> std::optional<typename C::value_type> {
if (index < container.size()) {
return container.at(index);
}
return std::nullopt;
}
序列化示例
// 可序列化concept
template<typename T>
concept Serializable = requires(T t, std::ostream& os, std::istream& is) {
{ os << t } -> std::same_as<std::ostream&>;
{ is >> t } -> std::same_as<std::istream&>;
};
// 序列化函数
template<Serializable T>
void save_to_file(const T& object, const std::string& filename) {
std::ofstream file(filename);
file << object;
}
template<Serializable T>
T load_from_file(const std::string& filename) {
std::ifstream file(filename);
T object;
file >> object;
return object;
}
12.7 Concepts与SFINAE的比较
SFINAE方式(C++17及之前)
// 使用SFINAE检查类型是否有size()方法
template<typename T>
auto get_size(const T& container)
-> decltype(container.size()) {
return container.size();
}
template<typename T>
auto get_size(const T& container)
-> std::enable_if_t<!std::is_same_v<decltype(container.size()), void>, size_t> {
return std::distance(container.begin(), container.end());
}
Concepts方式(C++20)
// 使用concepts,更清晰易读
template<typename T>
concept HasSize = requires(T t) { t.size(); };
template<HasSize T>
auto get_size(const T& container) {
return container.size();
}
template<typename T>
requires (!HasSize<T>) && requires(T t) { t.begin(); t.end(); }
auto get_size(const T& container) {
return std::distance(container.begin(), container.end());
}
12.8 最佳实践
1. 命名约定
- 使用PascalCase命名concepts
- 名称应该清晰表达约束的含义
- 避免过于泛化的名称
2. 约束设计
// 好的设计:具体且有意义
template<typename T>
concept Drawable = requires(T t, Canvas& canvas) {
t.draw(canvas);
};
// 避免:过于泛化
template<typename T>
concept HasMethod = requires(T t) {
t.method();
};
3. 组合约束
// 使用逻辑运算符组合简单concepts
template<typename T>
concept NumericContainer = Container<T> &&
Arithmetic<typename T::value_type>;
// 分层设计concepts
template<typename T>
concept BasicShape = requires(T t) {
t.area();
t.perimeter();
};
template<typename T>
concept ColoredShape = BasicShape<T> && requires(T t) {
t.color();
t.setColor(Color{});
};
4. 错误信息优化
// 提供清晰的约束信息
template<typename T>
concept PrintableToStream = requires(T t, std::ostream& os) {
{ os << t } -> std::same_as<std::ostream&>;
};
template<PrintableToStream T>
void print(const T& value) {
std::cout << value << std::endl;
}
练习建议
基础练习:
- 定义基本的数值类型concepts
- 创建容器相关的concepts
- 实现简单的约束函数
进阶练习:
- 设计一个图形库的concepts体系
- 实现类型安全的数学运算库
- 创建序列化框架的concepts
实战项目:
- 重构现有的模板代码,使用concepts替代SFINAE
- 设计一个通用的算法库
- 实现类型安全的配置系统
总结
C++20 Concepts是模板编程的重大进步:
- 可读性:代码意图更加清晰
- 错误信息:编译错误更容易理解
- 重载决议:更精确的函数选择
- 文档化:concepts本身就是很好的文档
- 性能:编译时检查,无运行时开销
掌握Concepts将让你的C++模板代码更加现代化、可维护和用户友好。