C++20 concept使用介绍

一、concept是什么

concept是C++20引入的一项革命性特性,它是一种编译期布尔表达式,用于指定模板参数必须满足的约束条件。concept本质上是一种类型谓词(type predicate),它可以在编译期对模板参数进行静态检查,如果参数不满足约束条件,编译器会给出清晰的错误信息,而不会触发复杂的模板错误实例化。

在传统的C++模板编程中,我们只能通过SFINAE(Substitution Failure Is Not An Error)机制来限制模板参数的类型,这种方式通常会在模板实例化失败时产生冗长且难以理解的错误信息。concept的出现彻底改变了这一局面,它提供了一种更加直观、声明式的方式来表达模板约束条件,使得代码的可读性和可维护性得到了显著提升。

concept的定义使用concept关键字 followed by a boolean constant expression。concept可以在命名空间中定义,也可以在类内部定义,但通常我们将其定义在全局命名空间或专门的命名空间中以便复用。下面是一个最基本的concept定义示例:

1
2
3
4
5
6
// 定义一个检查类型是否可比较的concept
template<typename T>
concept Comparable = requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ a != b } -> std::convertible_to<bool>;
};

这个concept名为Comparable,它要求类型T必须支持==!=运算符,并且这些运算符的返回类型必须可以转换为boolrequires表达式是concept定义的核心语法,它允许我们使用一种更加灵活和表达力更强的方式来指定约束条件。

concept的求值结果是一个编译期的布尔常量,这意味着我们可以在条件语句中使用concept来选择不同的模板实现,也可以将多个concept组合成更复杂的约束条件。C++标准库在<concepts>头文件中提供了大量预定义的concept,涵盖了从基本类型特性到复杂关系约束的各种场景,这些标准concept为我们的日常编程提供了极大的便利。

二、concept有什么作用

concept的核心作用是提供编译期的类型检查和约束验证机制,它从根本上改变了我们编写和设计泛型代码的方式。通过使用concept,我们可以将模板参数的要求明确地表达出来,使得代码的意图更加清晰,同时也让编译器能够在编译期捕获类型不匹配的错误。

第一个重要作用是改善编译错误信息。当传统的模板约束被违反时,编译器通常会产生大量令人困惑的错误信息,这些信息往往指向模板实例化的深层内部结构,使得定位问题变得异常困难。而当约束使用concept表达时,编译器可以在第一时间给出清晰的错误提示,告诉开发者哪个concept约束没有被满足,甚至可以指出具体是哪个操作无法编译。

第二个重要作用是实现更精细的模板分派机制。通过将concept作为模板约束条件,我们可以在同一个函数名下提供多个重载版本参数,编译器会根据传入的类型自动选择最合适的实现。这种机制类似于函数重载,但是作用在模板层面,能够处理更广泛的类型场景。例如,我们可以为支持加法的类型提供一个版本,为不支持加法的类型提供另一个版本。

第三个重要作用是提升代码的文档性和可读性。当我们查看一个使用concept约束的函数签名时,可以立即了解该函数对模板参数有哪些要求,而不需要深入阅读函数实现来推断这些约束。这种声明式的约束表达方式使得代码自文档化(self-documenting)成为可能,团队成员可以更快地理解和使用他人编写的泛型代码。

第四个重要作用是支持更安全的泛型编程。在没有concept的时代,泛型代码往往会在模板实例化的深层位置出现难以追踪的错误,这些错误可能在代码部署后才被发现,给系统稳定性带来隐患。concept通过将类型检查提前到编译期,大大降低了运行时出现类型错误的风险,提高了整个代码库的健壮性。

第五个重要作用是促进泛型库的标准化和互操作性。由于concept提供了一种标准化的约束表达方式,不同开发者编写的泛型组件可以更容易地相互配合使用。只要两个组件使用了相同的concept来表达它们的约束要求,它们就可以无缝地协同工作,这极大地促进了C++泛型编程生态系统的健康发展。

三、concept怎么使用

concept的使用是C++20泛型编程的核心技能,掌握它需要从定义、约束组合、函数模板约束、类模板约束、模板别名约束等多个维度进行学习。下面我们将详细讲解concept的各种使用方法,这些内容是本文的重点部分。

3.1 定义基本的concept

定义concept的基本语法使用template参数列表 followed by concept关键字和concept名称,最后是一个编译期布尔表达式。最简单的concept可以只包含一个布尔常量表达式:

1
2
3
4
5
6
7
// 最简单的concept定义
template<typename T>
concept Integral = std::is_integral_v<T>;

// 使用类型别名简化concept定义
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

然而,更强大和灵活的concept定义需要使用requires表达式。requires表达式允许我们使用一种类似于函数体的语法来指定约束条件,它可以包含多个约束子句,每个子句检查一个特定的要求是否被满足。requires表达式有四种形式,分别适用于不同的场景。

第一种形式是简单约束(Simple Requirement),它只是要求某个表达式是有效的:

1
2
3
4
template<typename T>
concept Hashable = requires(T t) {
std::hash<T>{}(t); // 要求T类型的对象可以用于std::hash
};

第二种形式是类型约束(Type Requirement),使用typename关键字来要求某个类型名是有效的:

1
2
3
4
5
6
7
template<typename T>
concept Container = requires(T t) {
typename T::value_type; // 要求T有value_type类型成员
typename T::iterator; // 要求T有iterator类型成员
{ t.begin() }; // 要求T有begin()成员函数
{ t.end() }; // 要求T有end()成员函数
};

第三种形式是复合约束(Compound Requirement),使用大括号包围表达式,followed by约束后置条件:

1
2
3
4
5
template<typename T>
concept Incrementable = requires(T x) {
{ x++ } -> std::same_as<T>; // 后置递增返回原类型
{ ++x } -> std::same_as<T&>; // 前置递增返回引用
};

第四种形式是嵌套约束(Nested Requirement),使用requires关键字嵌套更复杂的条件:

1
2
3
4
template<typename T>
concept Addable = requires(T a, T b) {
requires std::same_as<decltype(a + b), T>; // 加法结果类型与操作数相同
};

3.2 在函数模板中使用concept约束

concept最常见的用途是约束函数模板。约束函数模板有三种主要语法,每种语法都有其适用场景和特点。第一种语法是将concept名称放在template参数列表后面:

1
2
3
4
5
// 使用concept约束函数模板
template<Comparable T>
T max(T a, T b) {
return (a > b) ? a : b;
}

第二种语法是使用requires子句,这是最灵活的方式,允许指定多个concept的组合:

1
2
3
4
5
6
// 使用requires子句约束函数模板
template<typename T>
requires Integral<T> || FloatingPoint<T>
T abs(T value) {
return value >= 0 ? value : -value;
}

第三种语法是使用尾置返回类型:

1
2
3
4
5
// 使用尾置返回类型和concept约束
template<typename T>
auto sum(T a, T b) -> Addable<T> {
return a + b;
}

对于需要返回类型约束的情况,我们可以将concept与尾置返回类型结合使用,这特别适用于返回类型依赖于参数类型的场景:

1
2
3
4
5
6
// 返回类型受concept约束
template<typename T>
requires Numeric<T>
std::common_type_t<T> multiply(T a, T b) {
return a * b;
}

3.3 在类模板中使用concept约束

concept同样可以用于约束类模板,这使得我们可以根据类型约束来选择不同的类实现:

1
2
3
4
5
6
7
8
9
10
11
12
// 约束类模板
template<typename T>
requires Regular<T>
class MyContainer {
// T是常规类型(可默认构造、可赋值、可比较相等)
};

template<typename T>
requires (!Regular<T>)
class MyContainer {
// 处理非Regular类型的特化版本
};

类模板的成员函数也可以独立地使用concept约束,这允许我们为某些成员函数提供更严格的要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class DataHolder {
public:
// 这个成员函数要求T是可拷贝的
T getCopy() requires Copyable<T> {
return data_;
}

// 这个成员函数要求T是可移动的
T getMoved() requires Moveable<T> {
return std::move(data_);
}

private:
T data_;
};

3.4 concept的组合使用

多个concept可以通过逻辑运算符组合使用,这使得我们可以构建复杂的约束条件。C++提供了三种组合运算符:&&(逻辑与)、||(逻辑或)和!(逻辑非):

1
2
3
4
5
6
7
8
9
10
11
12
// 组合多个concept
template<typename T>
concept NumericContainer = Container<T> && Numeric<typename T::value_type>;

// 使用组合concept约束函数
template<NumericContainer T>
void process(T& container) {
// T既是容器,又包含数值类型元素
for (auto& elem : container) {
// 可以对elem进行数值运算
}
}

对于需要多个concept同时满足的场景,使用&&运算符可以优雅地表达这种需求。例如,我们需要一个类型既支持排序又支持随机访问:

1
2
3
4
5
6
7
8
template<typename T>
concept SortableRandomAccessContainer =
RandomAccessContainer<T> && Sortable<T>;

template<SortableRandomAccessContainer T>
void sortContainer(T& container) {
std::sort(container.begin(), container.end());
}

我们也可以使用||运算符来表达”或”的关系,这在需要支持多种类型接口的场景中非常有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
concept HasFirst = requires(T t) { { t.first } -> std::convertible_to<int>; };

template<typename T>
concept HasValue = requires(T t) { { t.value } -> std::convertible_to<int>; };

// 只要满足其中一个即可
template<typename T>
requires HasFirst<T> || HasValue<T>
int getIntValue(T t) {
if constexpr (HasFirst<T>) {
return t.first;
} else {
return t.value;
}
}

3.5 在requires表达式中检查可变参数模板

对于需要处理任意数量参数的模板函数,我们可以使用可变参数模板结合requires表达式来实现更灵活的约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 要求所有参数类型都支持加法
template<typename... Args>
requires (Addable<Args> && ...)
Args sumAll(Args... args) {
return (args + ...);
}

// 要求至少有一个参数且所有参数可比较相等
template<typename T, typename... Rest>
requires (EqualityComparableWith<T, Rest> && ...)
bool allEqual(T first, Rest... rest) {
return ((first == rest) && ...);
}

3.6 使用concept选择函数重载

concept的一个强大特性是允许编译器根据concept约束自动选择最优的函数重载。这种机制被称为约束分派(constrained dispatch),它使得我们可以为一组相关但不完全相同的类型提供不同的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 为支持加法的类型提供泛型实现
template<Addable T>
T add(T a, T b) {
return a + b;
}

// 为字符串类型提供特化实现(优先匹配)
template<>
std::string add<std::string>(std::string a, std::string b) {
return a + b;
}

// 使用concept进行重载解析
template<typename T>
requires std::same_as<T, int>
T add(T a, T b) {
return a + b + 1; // 整数有特殊处理
}

当存在多个重载版本时,编译器会选择约束条件最具体(最严格)的那个版本。这种机制类似于函数重载的解析过程,但是作用在模板约束层面。

四、concept有哪些使用场景

concept在实际编程中有广泛的应用场景,从简单的类型检查到复杂的泛型库设计,都能看到concept的身影。理解这些使用场景对于有效地运用concept至关重要。

第一个常见场景是约束容器类型参数。当我们设计泛型容器算法时,通常需要确保传入的类型满足容器的基本要求,例如拥有value_typeiterator等类型成员,以及begin()end()等成员函数。使用concept可以清晰地表达这些要求:

1
2
3
4
5
6
7
template<Container C>
void printContainer(const C& container) {
for (const auto& elem : container) {
std::cout << elem << " ";
}
std::cout << std::endl;
}

第二个常见场景是约束算法模板参数。不同的算法对操作数有不同的要求,例如排序算法要求元素支持<运算符,数值算法要求元素支持算术运算符。使用concept可以明确地表达这些要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 要求容器元素支持小于比较
template<Sortable T>
void quickSort(std::vector<T>& vec) {
// 排序实现
}

// 要求元素支持加减运算
template<Addable T>
T accumulate(const std::vector<T>& vec) {
T sum = T{};
for (const auto& elem : vec) {
sum += elem;
}
return sum;
}

第三个常见场景是实现策略模式(Strategy Pattern)。concept使得我们可以在运行时根据类型约束选择不同的策略,这是对传统策略模式的一种编译期扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义不同的处理策略concept
template<typename T>
concept ParallelProcessable = requires(T t) {
{ t.parallelProcess() };
};

template<typename T>
concept SequentialProcessable = requires(T t) {
{ t.sequentialProcess() };
};

// 根据concept选择不同的处理方式
template<Processor P>
void execute(P& processor) {
if constexpr (ParallelProcessable<P>) {
processor.parallelProcess();
} else if constexpr (SequentialProcessable<P>) {
processor.sequentialProcess();
}
}

第四个常见场景是约束模板元编程的输入类型。在模板元编程中,我们经常需要确保输入类型满足某些条件,否则后续的计算可能没有意义。concept提供了一种优雅的方式来表达这些先决条件:

1
2
3
4
5
6
7
8
9
10
// 确保输入是有效的矩阵类型
template<Matrix M>
class MatrixCalculator {
public:
// 矩阵乘法运算
auto multiply(const M& other) -> MatrixResult<M> {
static_assert(M::rows == M::cols, "必须是方阵");
// 乘法实现
}
};

第五个常见场景是实现概念检查库。许多现代C++库(如Ranges库、Eigen库等)都使用concept来定义它们的接口约束,这使得用户可以清楚地了解使用这些库需要满足的条件:

1
2
3
4
5
6
7
8
9
10
// 使用Ranges库的concept
#include <ranges>

void processRange(std::ranges::input_range auto& range) {
// 处理输入范围
}

void sortRandomAccess(std::ranges::random_access_range auto& range) {
std::sort(range.begin(), range.end());
}

第六个常见场景是在模板库中提供更好的错误信息。传统的模板库在类型不匹配时会产生冗长的错误信息,而使用concept可以显著改善这种情况:

1
2
3
4
5
6
7
8
9
10
11
// 没有concept约束时,错误信息可能指向模板内部的深处
template<typename T>
void legacyFunction(T t) {
t.complexOperation(); // 如果T没有这个方法,错误信息可能很长
}

// 使用concept约束后,错误信息更加清晰
template<HasComplexOperation T>
void modernFunction(T t) {
t.complexOperation();
}

第七个常见场景是实现类型安全的异构容器。concept使得我们可以在编译期检查容器元素的类型约束,从而实现更严格的类型安全保证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T>
concept Serializable = requires(T t) {
{ t.serialize() } -> std::convertible_to<std::string>;
};

template<Serializable T>
class SerializationAwareContainer {
std::vector<T> items_;

public:
void add(const T& item) {
items_.push_back(item);
}

void saveAll(const std::string& filename) {
std::ofstream file(filename);
for (const auto& item : items_) {
file << item.serialize() << std::endl;
}
}
};

五、concept与SFINAE之间的等效替代方式与优势

SFINAE(Substitution Failure Is Not An Error)是C++11引入的一种机制,它允许在模板参数替换失败时不产生错误,而是简单地不匹配该模板。concept与SFINAE在功能上有很大的重叠,但concept提供了更加优雅和可维护的解决方案。理解两者的等效替代方式以及concept的优势,对于从传统SFINAE代码迁移到concept至关重要。

5.1 SFINAE的基本机制

在concept出现之前,SFINAE是实现模板约束的主要方式。SFINAE有两种主要的表现形式:一种是使用enable_if模板,另一种是使用函数返回类型或参数列表中的表达式求值失败。

使用enable_if进行约束的典型模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
// SFINAE模式:使用enable_if约束模板
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
abs(T value) {
return value >= 0 ? value : -value;
}

template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
abs(T value) {
return value >= 0 ? value : -value;
}

另一种SFINAE模式是利用函数模板参数替换时的失败:

1
2
3
4
5
6
7
8
9
10
11
// SFINAE模式:利用替换失败
template<typename T>
auto process(T t) -> decltype(t.compile_check(), void()) {
// t必须支持compile_check成员函数
}

template<typename T>
auto process(T t) -> void(*)() {
// T不支持compile_check时的替代实现
return [] { /* 默认行为 */ };
}

5.2 使用concept替代SFINAE

使用concept可以完全替代SFINAE的功能,同时代码更加清晰。下面的代码展示了如何用concept重写上面的例子:

1
2
3
4
5
6
7
8
9
10
// Concept模式:使用concept约束模板
template<Integral T>
T abs(T value) {
return value >= 0 ? value : -value;
}

template<FloatingPoint T>
T abs(T value) {
return value >= 0 ? value : -value;
}

对于需要根据类型选择不同实现的场景,concept提供了更优雅的表达方式:

1
2
3
4
5
6
7
8
9
10
// 使用concept的分派机制替代SFINAE分派
template<typename T>
void process(T t) requires HasCompileCheck<T> {
t.compile_check();
}

template<typename T>
void process(T t) requires (!HasCompileCheck<T>) {
// 默认实现
}

5.3 concept相比SFINAE的优势

concept相对于SFINAE有多个显著优势,这些优势使得concept成为现代C++编程的首选方案。

第一个优势是错误信息的可读性。当SFINAE模板匹配失败时,错误信息往往包含大量的模板内部实现细节,开发者需要仔细分析才能找到问题根源。而concept失败时,编译器会明确指出哪个concept约束没有被满足:

1
2
3
4
5
// SFINAE的错误信息示例(假设)
// error: no type named 'type' in 'struct std::enable_if<false, void>'

// Concept的错误信息示例
// error: candidate function template not viable: requires 'Integral' concept was not satisfied

第二个优势是代码的可维护性。使用enable_if的代码往往冗长且难以理解,需要编写复杂的模板元代码来表达简单的类型约束。而concept允许我们使用声明式的语法来表达这些约束,代码更加简洁明了。

第三个优势是命名和复用的便利性。一旦定义了concept,就可以在多个地方复用它,而SFINAE的约束条件通常是内联写的,难以复用和共享:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Concept可以定义后复用
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

// 在多个地方使用同一个concept
template<Numeric T>
void process1(T value);

template<Numeric T>
T calculate(T a, T b);

// SFINAE的条件难以复用
template<typename T>
typename std::enable_if<std::is_arithmetic_v<T>>::type
process1(T value);

template<typename T>
typename std::enable_if<std::is_arithmetic_v<T>, T>::type
calculate(T a, T b);

第四个优势是支持组合和逻辑运算。concept可以通过&&||!等运算符优雅地组合,而SFINAE需要嵌套使用enable_if来实现相同的逻辑:

1
2
3
4
5
6
7
8
9
10
// Concept的组合使用
template<typename T>
concept ComplexConstraint = SimpleConcept<T> && OtherConcept<T>;

// SFINAE的组合使用
template<typename T>
typename std::enable_if<
SimpleConcept<T>::value && OtherConcept<T>::value
>::type
process(T t);

第五个优势是与C++20 Ranges库的深度集成。C++20引入的Ranges库大量使用concept来定义其接口,学习concept对于有效使用这些现代库是必不可少的。

5.4 渐进式的迁移策略

从SFINAE迁移到concept可以采用渐进式的策略,不需要一次性重写所有代码。以下是一种推荐的迁移方法:

首先,将现有的enable_if约束转换为简单的concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 原始SFINAE代码
template<typename T>
typename std::enable_if<std::is_copy_constructible_v<T>>::type
clone(T& original) {
// 实现
}

// 第一步:创建concept
template<typename T>
concept CopyConstructible = std::is_copy_constructible_v<T>;

// 第二步:使用concept约束
template<CopyConstructible T>
void clone(T& original) {
// 实现
}

然后,逐步将更复杂的SFINAE模式迁移到concept。对于需要多个条件组合的情况,可以定义组合concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 复杂的SFINAE条件
template<typename T>
typename std::enable_if<
std::is_default_constructible_v<T> &&
std::is_move_constructible_v<T>
>::type
initialize();

// 迁移到concept
template<typename T>
concept MovableAndDefaultConstructible =
std::is_default_constructible_v<T> &&
std::is_move_constructible_v<T>;

template<MovableAndDefaultConstructible T>
void initialize() {
// 实现
}

六、如何避免滥用concept

虽然concept是一个强大的工具,但滥用concept会导致代码复杂度增加、维护困难等问题。了解常见的滥用模式以及如何避免它们,对于写出高质量的C++代码至关重要。

第一个常见的滥用是将concept应用于所有模板参数。实际上,并非所有模板参数都需要concept约束。对于内部实现不需要了解类型细节的通用容器,可以使用无约束的模板参数,concept约束应该只在需要表达接口要求时使用:

1
2
3
4
5
6
7
8
9
10
11
// 过度约束的例子
template<DefaultConstructible T, Destructible T, Copyable T>
class OverConstrainedContainer {
// 可能过度约束
};

// 更合理的约束方式
template<Regular T> // Regular已经包含了基本要求
class BetterContainer {
// Regular = DefaultConstructible + Copyable + Moveable + EqualityComparable
};

第二个常见的滥用是定义过于复杂的concept。concept应该保持简单和可读,过于复杂的concept难以理解和维护。如果一个concept需要表达大量的约束条件,可能意味着应该将其拆分成多个更小的concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 过于复杂的concept
template<typename T>
concept ComplexConcept = requires(T t) {
typename T::value_type;
typename T::reference;
typename T::const_reference;
typename T::iterator;
typename T::const_iterator;
{ t.begin() };
{ t.end() };
{ t.size() };
{ t.empty() };
{ t[0] };
{ t.at(0) };
};

// 拆分成更小的concept
template<typename T>
concept HasValueType = requires { typename T::value_type; };

template<typename T>
concept Iterable = requires(T t) {
{ t.begin() };
{ t.end() };
};

template<typename T>
concept Sized = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.empty() } -> std::convertible_to<bool>;
};

template<typename T>
concept RandomAccess = requires(T t) {
{ t[0] };
{ t.at(0) };
};

// 组合使用
template<typename T>
concept Container = HasValueType<T> && Iterable<T> && Sized<T>;

第三个常见的滥用是在不需要的地方使用concept。concept主要用于模板代码的编译期约束,对于非模板代码或运行时多态,使用concept是不合适的:

1
2
3
4
5
6
7
8
9
10
11
// 不需要concept的场景
// 普通函数不需要concept约束
int add(int a, int b) {
return a + b;
}

// 类成员函数如果不需要模板化,也不应该使用concept
class SimpleClass {
public:
void process() { /* 实现 */ }
};

第四个常见的滥用是忽略concept的命名规范。良好的命名可以使代码更加自文档化,concept名称应该清晰地表达它们检查的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不好的命名
template<typename T>
concept C1 = std::is_integral_v<T>;

template<typename T>
concept C2 = requires(T t) { { t.value() }; };

// 好的命名
template<typename T>
concept IntegralNumber = std::is_integral_v<T>;

template<typename T>
concept HasValueMethod = requires(T t) { { t.value() }; };

第五个常见的滥用是过度依赖concept而忽略运行时检查。concept只提供编译期检查,对于某些需要在运行时验证的条件,仍然需要使用断言或其他运行时检查机制:

1
2
3
4
5
6
7
8
9
10
// 只依赖concept是不够的
template<PositiveNumber T>
void process(T value) {
// concept只保证T是数值类型,不保证value是正数
// 需要添加运行时检查
if (value <= 0) {
throw std::invalid_argument("Value must be positive");
}
// ...
}

第六个常见的滥用是创建重复的标准concept。C++标准库在<concepts>头文件中提供了大量预定义的concept,在定义自己的concept之前,应该先检查标准库是否已经提供了相同或相似的concept:

1
2
3
4
5
6
7
8
// 重复造轮子
template<typename T>
concept MyIntegral = std::is_integral_v<T>;

// 应该直接使用标准concept
template<Integral T>
// 或
template<std::integral<T>

为了避免滥用concept,遵循以下原则会有所帮助:

保持concept简单:一个concept应该只检查一个特定的属性或能力。如果需要检查多个属性,使用组合。

只在需要的地方使用concept:只有当模板代码需要表达对类型的要求时,才使用concept约束。不要为了使用concept而使用它。

优先使用标准concept:在编写自己的concept之前,先查看标准库是否提供了类似的concept。标准concept经过充分的测试和优化。

使concept的名称具有描述性:concept的名称应该清楚地表达它检查的属性,例如使用Addable而不是C1来表示支持加法的类型。

区分编译期和运行期约束:concept提供编译期检查,某些运行时验证仍然需要使用断言或其他机制。

七、concept相关标准库

C++20标准库在<concepts>头文件中定义了一系列预定义的concept,这些concept涵盖了从基本类型属性到复杂关系的各种场景。熟悉这些标准concept对于高效使用concept至关重要。

7.1 基础类型concept

基础类型concept用于检查类型的基本属性,这些concept是最常用也是最基础的:

1
2
3
4
5
6
7
8
9
10
11
// 检查类型是否相同
template<typename T, typename U>
concept std::same_as = /* ... */;

// 检查类型是否可转换为另一种类型
template<typename From, typename To>
concept std::convertible_to = /* ... */;

// 检查是否是派生关系
template<typename Derived, typename Base>
concept std::derived_from = /* ... */;

7.2 类型分类concept

类型分类concept用于检查类型的分类属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 检查是否是整数类型
template<typename T>
concept std::integral = /* ... */;

// 检查是否是浮点类型
template<typename T>
concept std::floating_point = /* ... */;

// 检查是否是算术类型(整数或浮点)
template<typename T>
concept std::arithmetic = /* ... */;

// 检查是否是对象类型
template<typename T>
concept std::object = /* ... */;

// 检查是否是函数类型
template<typename T>
concept std::function = /* ... */;

// 检查是否是引用类型
template<typename T>
concept std::reference = /* ... */;

// 检查是否是成员指针类型
template<typename T>
concept std::member_pointer = /* ... */;

// 检查是否是枚举类型
template<typename T>
concept std::enum = /* ... */;

// 检查是否是联合类型
template<typename T>
concept std::union_type = /* ... */;

7.3 类型特性concept

类型特性concept用于检查类型的更复杂特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 检查是否是移动构造的类型
template<typename T>
concept std::move_constructible = /* ... */;

// 检查是否是拷贝构造的类型
template<typename T>
concept std::copy_constructible = /* ... */;

// 检查是否是默认构造的类型
template<typename T>
concept std::default_initializable = /* ... */;

// 检查是否是分配的类型
template<typename T>
concept std::allocatable = /* ... */;

7.4 可调用概念

可调用concept用于检查类型是否可以被调用:

1
2
3
4
5
6
7
// 检查是否是可调用类型
template<typename F, typename... Args>
concept std::invocable = /* ... */;

// 检查调用结果是否可以转换为指定类型
template<typename F, typename... Args, typename R>
concept std::predicate = /* ... */;

7.5 比较概念

比较concept用于检查类型的比较操作:

1
2
3
4
5
6
7
// 检查是否可以比较相等
template<typename T>
concept std::equality_comparable = /* ... */;

// 检查是否可以严格弱序比较
template<typename T>
concept std::strict_weak_order = /* ... */;

7.6 对象概念

对象concept用于检查对象的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 检查是否是可移动的类型
template<typename T>
concept std::movable = /* ... */;

// 检查是否是可拷贝的类型
template<typename T>
concept std::copyable = /* ... */;

// 检查是否是半常规类型(可移动、可拷贝、可默认构造)
template<typename T>
concept std::semiregular = /* ... */;

// 检查是否是常规类型(半常规 + 可相等比较)
template<typename T>
concept std::regular = /* ... */;

7.7 自定义标准concept

除了使用标准concept外,我们还可以基于标准concept创建自己的自定义concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 基于标准concept创建自定义concept
template<typename T>
concept MyPointer = std::is_pointer_v<T>;

template<typename T>
concept MyContainer = requires(T t) {
typename T::value_type;
{ t.begin() };
{ t.end() };
{ t.size() } -> std::convertible_to<std::size_t>;
};

// 组合使用多个标准concept
template<typename T>
concept MyNumericContainer =
std::regular<T> &&
requires(T t) {
typename T::value_type;
std::is_arithmetic_v<typename T::value_type>;
};

7.8 使用标准库concept的完整示例

下面是一个使用标准库concept的完整示例,展示了如何在实际编程中组合使用这些concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <concepts>
#include <vector>
#include <list>
#include <iostream>

// 使用标准concept定义函数模板
template<std::regular T>
class Container {
std::vector<T> data_;

public:
void add(const T& value) {
data_.push_back(value);
}

template<std::strict_weak_order<T> Comp>
void sort(Comp comp = std::less<T>{}) {
std::sort(data_.begin(), data_.end(), comp);
}

void print() const {
for (const auto& elem : data_) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
};

int main() {
Container<int> intContainer;
intContainer.add(5);
intContainer.add(2);
intContainer.add(8);
intContainer.add(1);
intContainer.sort();
intContainer.print(); // 输出: 1 2 5 8

return 0;
}

八、concept相关的设计模式

concept在设计模式中的应用为传统的面向对象设计模式注入了新的活力。通过concept,我们可以在编译期实现更加灵活和高效的变体,这些变体往往比传统的运行时多态具有更好的性能。下面介绍几种与concept密切相关的设计模式及其实现方式。

8.1 策略模式的概念化实现

传统的策略模式通过接口和多态实现不同算法的切换,而使用concept可以在编译期实现类似的分派功能,同时保持零运行时开销:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 定义不同的策略concept
template<typename Strategy>
concept AdditionStrategy = requires(Strategy s, int a, int b) {
{ s.compute(a, b) } -> std::same_as<int>;
};

template<typename Strategy>
concept MultiplicationStrategy = requires(Strategy s, int a, int b) {
{ s.compute(a, b) } -> std::same_as<int>;
};

// 策略类
struct AddStrategy {
int compute(int a, int b) { return a + b; }
};

struct MultiplyStrategy {
int compute(int a, int b) { return a * b; }
};

struct SubtractStrategy {
int compute(int a, int b) { return a - b; }
};

// 使用concept约束的策略计算器
template<AdditionStrategy Strategy>
int calculate(Strategy strategy, int a, int b) {
return strategy.compute(a, b);
}

template<MultiplicationStrategy Strategy>
int calculate(Strategy strategy, int a, int b) {
return strategy.compute(a, b);
}

// 通用策略计算器
template<typename Strategy>
requires (!AdditionStrategy<Strategy> && !MultiplicationStrategy<Strategy>)
int calculate(Strategy strategy, int a, int b) {
return strategy.compute(a, b);
}

8.2 模板方法模式的概念化实现

concept使得模板方法模式可以更加灵活地定义算法的各个步骤,而不需要使用虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template<typename T>
concept Drawable = requires(T t) {
{ t.draw() };
{ t.getColor() } -> std::convertible_to<std::string>;
};

template<typename T>
concept Resizable = requires(T t, double factor) {
{ t.resize(factor) };
};

template<Drawable T>
class Shape {
public:
void render() {
// 模板方法:draw()是虚函数般的存在
onBeforeDraw();
T& self = static_cast<T&>(*this);
self.draw();
onAfterDraw();
}

std::string getDescription() {
T& self = static_cast<T&>(*this);
return "Shape with color: " + self.getColor();
}

protected:
void onBeforeDraw() { /* 通用前置处理 */ }
void onAfterDraw() { /* 通用后置处理 */ }
};

// 具体形状类
class Circle : public Shape<Circle> {
public:
void draw() { /* 绘制圆形 */ }
std::string getColor() const { return "red"; }
};

class Rectangle : public Shape<Rectangle> {
public:
void draw() { /* 绘制矩形 */ }
std::string getColor() const { return "blue"; }
};

8.3 访问者模式的概念化实现

使用concept可以创建类型安全的访问者模式,避免传统访问者模式中的类型安全问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 前向声明
template<typename Derived>
class Visitable;

template<typename T>
concept Acceptable = requires(T& visitor) {
{ visitor.visit(std::declval<Visitable<typename T::ElementType>>()) };
};

template<typename Derived>
class Visitable {
public:
template<Acceptable Visitor>
void accept(Visitor& visitor) {
visitor.visit(static_cast<Derived&>(*this));
}
};

class Circle : public Visitable<Circle> {
public:
double radius = 1.0;
};

class Rectangle : public Visitable<Rectangle> {
public:
double width = 1.0;
double height = 1.0;
};

// 访问者类
class ShapeVisitor {
public:
void visit(Circle& circle) {
std::cout << "Circle with radius: " << circle.radius << std::endl;
}

void visit(Rectangle& rectangle) {
std::cout << "Rectangle: " << rectangle.width << "x" << rectangle.height << std::endl;
}
};

8.4 工厂模式的概念化实现

concept使得工厂模式可以更加灵活地处理不同类型的产品,同时保持类型安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 产品concept
template<typename T>
concept Product = requires {
typename T::id;
{ T::create() };
};

// 产品注册表
template<Product T>
class ProductRegistry {
using Id = typename T::id;

public:
static Id id() { return T::id; }

static std::unique_ptr<T> create() {
return std::make_unique<T>();
}
};

// 产品基类和产品类
struct ProductBase {
virtual ~ProductBase() = default;
virtual void operation() = 0;
};

struct ProductA : ProductBase {
static constexpr auto id = "ProductA";

static std::unique_ptr<ProductA> create() {
return std::make_unique<ProductA>();
}

void operation() override {
std::cout << "ProductA operation" << std::endl;
}
};

struct ProductB : ProductBase {
static constexpr auto id = "ProductB";

static std::unique_ptr<ProductB> create() {
return std::make_unique<ProductB>();
}

void operation() override {
std::cout << "ProductB operation" << std::endl;
}
};

8.5 装饰器模式的概念化实现

使用concept可以创建更加灵活的装饰器模式,支持动态组合不同的装饰功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
template<typename T>
concept Decoratable = requires(T t, int v) {
{ t.getValue() } -> std::convertible_to<int>;
{ t.setValue(v) };
};

template<Decoratable T>
class Decorator {
protected:
std::unique_ptr<T> wrapped_;

public:
explicit Decorator(std::unique_ptr<T> wrapped)
: wrapped_(std::move(wrapped)) {}

int getValue() {
return wrapped_->getValue();
}

void setValue(int v) {
wrapped_->setValue(v);
}
};

class TimestampingDecorator : public Decorator<TimestampingDecorator> {
using Decorator::Decorator;

public:
int getValue() {
auto val = Decorator::getValue();
std::cout << "Timestamp: " << std::chrono::system_clock::now() << std::endl;
return val;
}
};

class ValidationDecorator : public Decorator<ValidationDecorator> {
using Decorator::Decorator;

public:
void setValue(int v) {
if (v < 0) {
throw std::invalid_argument("Value must be non-negative");
}
Decorator::setValue(v);
}
};

通过这些设计模式的概念化实现,我们可以看到concept不仅是一种类型约束工具,更是一种设计思维方式的转变。它允许我们在编译期实现原本需要在运行时才能实现的多态和灵活性,同时带来更好的类型安全性和性能。这些模式在现代C++库(如Ranges库)中得到了广泛的应用,掌握它们对于编写高质量的泛型代码具有重要意义。

concept作为C++20最重要的特性之一,它为泛型编程带来了革命性的变化。通过提供声明式的类型约束机制,concept使得代码更加清晰、错误信息更加友好、设计更加灵活。从基本类型检查到复杂的设计模式实现,concept都有其独特的价值。在实际开发中,我们应该充分利用concept的优势,同时注意避免滥用,以写出高质量、可维护的C++代码。

感谢您对本站的支持.