WEBKT

C++20 Concepts:让你的模板代码更上一层楼

51 0 0 0

C++20 引入的 Concepts 特性,堪称现代 C++ 模板编程的利器。它就像是模板的“类型约束”,让你的代码更具表达力、更安全,错误信息也更友好。对于已经对 C++ 模板编程有一定了解,并渴望掌握 Concepts 的开发者来说,本文将带你深入理解并灵活运用这一强大特性。

一、告别“SFINAE 魔法”,拥抱 Concepts 的清晰

在 Concepts 出现之前,我们常常使用 SFINAE (Substitution Failure Is Not An Error,替换失败不是错误) 这种“黑魔法”来实现模板的条件编译和类型约束。SFINAE 虽然强大,但其代码往往晦涩难懂,错误信息也让人摸不着头脑。Concepts 的出现,正是为了解决这些问题。

想象一下,你想要编写一个模板函数,它接受一个参数,这个参数必须支持 operator+ 操作。使用 SFINAE,你可能会写出类似这样的代码:

template <typename T, typename = decltype(std::declval<T>() + std::declval<T>())>
auto add(T a, T b) -> T {
return a + b;
}

这段代码使用了 decltypestd::declval 这样的“黑话”,让人难以理解其真实意图。如果类型 T 不支持 operator+,编译器会报出非常长的错误信息,难以定位问题。

而使用 Concepts,你可以这样写:

template <typename T>
requires std::plus<T>()
auto add(T a, T b) -> T {
return a + b;
}

或者更简洁的方式:

template <std::plus T>
auto add(T a, T b) -> T {
return a + b;
}

这段代码清晰地表达了你的意图:add 函数接受的类型 T 必须满足 std::plus<T>() 这个 Concept,也就是必须支持 operator+ 操作。如果类型 T 不满足这个 Concept,编译器会报出更清晰、更易于理解的错误信息,例如:“类型 T 不满足 Concept std::plus”。

二、Concepts 的基本语法:定义与使用

Concepts 的核心在于定义 Concept 和使用 Concept。

  • 定义 Concept

你可以使用 concept 关键字来定义一个 Concept。一个 Concept 本质上是一个返回 bool 类型的表达式,它接受一个或多个类型作为参数。例如,我们可以定义一个 Addable Concept,用于判断一个类型是否支持 operator+ 操作:

template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // 表达式 a + b 必须合法,并且结果可以转换为 T
};

这个 Concept 使用了 requires 表达式,它用于检查一个或多个表达式是否合法。requires 表达式可以包含:

* **简单要求 (Simple requirement)**:例如 `{ a + b }`,表示表达式 `a + b` 必须合法。
* **类型要求 (Type requirement)**:例如 `typename T::value_type`,表示类型 `T` 必须包含一个名为 `value_type` 的成员类型。
* **复合要求 (Compound requirement)**:例如 `{ a + b } -> std::convertible_to<T>`,表示表达式 `a + b` 必须合法,并且其结果可以转换为类型 `T``->` 后面可以跟一个 Concept 或者一个类型约束。
* **嵌套要求 (Nested requirement)**:允许在 Concept 内部使用 `requires` 关键字嵌套定义更复杂的约束。
  • 使用 Concept

定义好 Concept 之后,你可以在以下几个地方使用它:

* **作为模板参数的约束 (Constrained template parameter)**:例如 `template <Addable T>`
* **在 `requires` 子句中 (Requires clause)**:例如 `template <typename T> requires Addable<T>`
* **作为 `auto` 占位符类型的约束 (Constrained auto placeholder type)**:例如 `auto add(Addable auto a, Addable auto b)`

三、标准库中的 Concepts:站在巨人的肩膀上

C++20 标准库已经定义了大量的 Concepts,涵盖了各种常见的类型约束。例如:

  • std::integral:表示一个整数类型。
  • std::floating_point:表示一个浮点数类型。
  • std::copyable:表示一个可以复制的类型。
  • std::moveable:表示一个可以移动的类型。
  • std::default_initializable:表示一个可以默认初始化的类型。
  • std::equality_comparable:表示一个可以使用 ==!= 进行比较的类型。
  • std::totally_ordered:表示一个可以使用 <><=>= 进行比较的类型。
  • std::invocable:表示一个可以调用的类型(例如函数、函数对象、Lambda 表达式)。
  • std::same_as:判断两个类型是否相同。

利用这些标准库提供的 Concepts,你可以大大简化你的代码,避免重复造轮子。

四、自定义 Concepts:打造专属的类型约束

除了使用标准库提供的 Concepts,你还可以根据自己的需求定义自己的 Concepts。这可以让你更好地表达你的代码意图,并提供更精确的类型约束。

例如,假设你正在开发一个图形库,你需要定义一个 Concept,用于表示一个可以绘制的图形:

class Point { public: double x, y; };
concept Drawable {
typename T::draw_result; // 必须有一个名为 draw_result 的成员类型
requires(T& shape, Point p) {
shape.draw(p) -> std::same_as<typename T::draw_result>; // 必须有一个名为 draw 的成员函数,接受一个 Point 参数,并返回 draw_result 类型
};
};
class Circle {
public:
using draw_result = void;
void draw(Point p) { std::cout << "drawing at" << p.x << " " << p.y << std::endl; }
};
static_assert(Drawable<Circle>);
template <Drawable D>
void render(D shape, Point p) {
shape.draw(p);
}
int main() {
Circle c;
render(c, Point{ 1.0, 1.0 });
}

这个 Drawable Concept 要求类型 T 必须包含一个名为 draw_result 的成员类型,并且必须有一个名为 draw 的成员函数,接受一个 Point 参数,并返回 draw_result 类型。

五、Concepts 与 SFINAE 的对比:优劣一目了然

特性 Concepts SFINAE
代码可读性 高,易于理解代码意图 低,代码晦涩难懂
错误信息 清晰,易于定位问题 冗长,难以定位问题
编译速度 快,编译器可以更早地进行类型检查 慢,编译器需要进行更多的模板替换
功能 主要用于类型约束 可以用于更广泛的模板元编程
学习曲线 相对平缓 陡峭

总的来说,Concepts 在代码可读性、错误信息和编译速度方面都优于 SFINAE。然而,SFINAE 在模板元编程方面仍然具有一定的优势,例如可以实现更复杂的类型计算和条件编译。

六、Concepts 的进阶用法:组合与继承

你可以使用逻辑运算符(&&||!)来组合多个 Concepts,创建更复杂的类型约束。

例如,你可以定义一个 Sortable Concept,它要求类型 T 必须是可复制的,并且可以使用 < 运算符进行比较:

template <typename T>
concept Sortable = std::copyable<T> && requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};

你也可以让一个 Concept 继承另一个 Concept,从而复用已有的类型约束。

例如,你可以定义一个 Ordered Concept,它继承自 std::totally_ordered,并添加额外的约束:

template <typename T>
concept Ordered = std::totally_ordered<T> && requires(T a) {
{ a.compare(a) } -> std::convertible_to<int>; // 必须有一个名为 compare 的成员函数,返回 int 类型
};

七、Concepts 的实战应用:案例分析

  • 约束模板容器的元素类型

你可以使用 Concepts 来约束模板容器的元素类型,例如:

template <typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template <Number T>
class MyVector {
// ...
};

这个 MyVector 类只能存储整数或浮点数类型的元素。

  • 约束算法的参数类型

你可以使用 Concepts 来约束算法的参数类型,例如:

template <typename T, typename U>
concept Multipliable = requires(T a, U b) {
{ a * b } -> std::convertible_to<T>;
};
template <Multipliable T, typename U>
T multiply(T a, U b) {
return a * b;
}

这个 multiply 函数只能接受两个可以进行乘法运算的参数,并且结果可以转换为第一个参数的类型。

  • 改进函数重载

Concepts 可以更精确地选择重载函数,避免二义性。

void print(int i) {
std::cout << "int: " << i << std::endl;
}
void print(double d) {
std::cout << "double: " << d << std::endl;
}
template <typename T>
requires std::integral<T>
void print(T i) {
std::cout << "integral: " << i << std::endl;
}
template <typename T>
requires std::floating_point<T>
void print(T d) {
std::cout << "floating_point: " << d << std::endl;
}
int main() {
print(1); // 输出 integral: 1
print(1.0); // 输出 floating_point: 1
}

八、Concepts 的注意事项:避免过度约束

虽然 Concepts 可以提供更精确的类型约束,但也需要避免过度约束。过度约束会导致你的代码难以复用,并限制其适用范围。

在定义 Concepts 时,应该尽量使用最小化的约束集合,只包含必要的类型要求。避免添加不必要的约束,例如要求类型必须包含某个特定的成员变量或成员函数。

九、总结:Concepts 是现代 C++ 模板编程的基石

C++20 Concepts 是一个强大的特性,它可以让你编写更具表达力、更安全、更易于维护的模板代码。通过定义和使用 Concepts,你可以清晰地表达你的代码意图,提供更精确的类型约束,并获得更友好的错误信息。

掌握 Concepts 是成为一名优秀的 C++ 模板程序员的必备技能。希望本文能够帮助你更好地理解和运用 Concepts,让你的模板代码更上一层楼。

十、更进一步:未来展望

随着 C++ 语言的不断发展,Concepts 将会在更多的场景中得到应用。例如,未来的 C++ 标准可能会提供更多的标准 Concepts,以及更强大的 Concept 组合和继承机制。学习和掌握 Concepts,将让你在未来的 C++ 开发中更具竞争力。 拥抱 Concepts,让你的 C++ 代码更加现代化!

模板老司机 C++20Concepts模板编程

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9346