专业的编程技术博客社区

网站首页 > 博客文章 正文

创建更安全的C++模板,使用概念和约束

baijin 2024-10-12 02:09:24 博客文章 14 ℃ 0 评论


模板非常适合编写与不同类型一起工作的代码。例如,这个函数可以与任何数值类型一起工作:

template <typename T>

T arg42(const T & arg) {

return arg + 42;

}


但是,当你尝试用非数值类型调用它时会发生什么呢?

const char * n = "7";

cout << "result is " << arg42(n) << "\n";


输出:

Result is ion


这可以编译并运行,但结果是不可预测的。实际上,这个调用是危险的,它很容易崩溃或变成一个漏洞。我更希望编译器生成一个错误信息,这样我可以修复代码。

现在,有了概念,我可以这样写:

template <typename T>

requires Numeric<T>

T arg42(const T & arg) {

return arg + 42;

}


`requires` 关键字是 C++20 的新特性。它对模板应用约束。Numeric 是一个概念,它只接受整数和浮点类型。现在,当我用非数值参数编译这段代码时,我会得到一个合理的编译器错误:

error: 'arg42': no matching overloaded function found

error: 'arg42': the associated constraints are not satisfied


这样的错误信息比大多数编译器错误更有用。

让我们更仔细地看看如何在代码中使用概念和约束。

如何做到这一点…

概念其实就是一个命名的约束。上面的 Numeric 概念看起来像这样:

#include <concepts>

template <typename T>

concept Numeric = integral<T> || floating_point<T>;


这个概念要求类型 T 满足 std::integral 或 std::floating_point 预定义概念中的任何一个。这些概念包含在 <concepts> 头文件中。

概念和约束可以用在类模板、函数模板或变量模板中。我们已经看到了一个受约束的函数模板,现在这里有一个简单的受约束的类模板示例:

template<typename T>

requires Numeric<T>

struct Num {

T n;

Num(T n) : n{n} {}

};


这里有一个简单的变量模板示例:

template<typename T>

requires floating_point<T>

T pi{3.1415926535897932385L};


你可以在任何模板上使用概念和约束。让我们考虑一些更进一步的例子。为了简单起见,我们将在这些例子中使用函数模板。

约束可以使用概念或类型特征来评估一个类型的特性。你可以使用 <type_traits> 头文件中的任何类型特征,只要它返回一个 bool。

例如:

template<typename T>

requires is_integral<T>::value // value 是 bool

constexpr double avg(vector<T> const& vec) {

	double sum{ accumulate(vec.begin(), vec.end(), 0.0) };

	return sum / vec.size();

}


`requires` 关键字是 C++20 的新特性。它为模板参数引入了一个约束。在这个例子中,约束表达式测试模板参数是否符合类型特征 is_integral。

你可以使用 <type_traits> 头文件中的预定义特性,或者你可以像定义模板变量一样定义你自己的,只要它返回 constexpr bool。例如:

template<typename T>
constexpr bool is_gt_byte{ sizeof(T) > 1 };


这定义了一个类型特征,称为 is_gt_byte。这个特性使用 sizeof 运算符来测试类型 T 是否大于 1 字节。

概念其实就是一组命名的约束。例如:

template<typename T>

concept Numeric = is_gt_byte<T> &&

(integral<T> || floating_point<T>);


这定义了一个名为 Numeric 的概念。它使用我们的 is_gt_byte 约束,以及来自 <concepts> 头文件的 floating_point 和 integral 概念。我们可以使用它来限制模板只接受大小大于 1 字节的数值类型。

template<Numeric T>

T arg42(const T & arg) {

return arg + 42;

}


你会注意到,我在模板声明中应用了约束,而不是在 requires 表达式中的单独一行。有几种方法可以应用概念。让我们看看这是如何工作的。

它是如何工作的…

有几种不同的方法可以应用概念或约束:

你可以使用 requires 关键字应用概念或约束:

template<typename T>

requires Numeric<T>

T arg42(const T & arg) {

return arg + 42;

}


你可以在模板声明中应用概念:

template<Numeric T>

T arg42(const T & arg) {

return arg + 42;

}


你可以在函数签名中使用 requires 关键字:

template<typename T>

T arg42(const T & arg) requires Numeric<T> {

return arg + 42;

}


或者你可以在简写函数模板的参数列表中使用概念:

auto arg42(Numeric auto & arg) {

return arg + 42;

}


对于许多目的来说,选择这些策略中的一种可能是风格问题。在某些情况下,一个可能比另一个更好。

还有更多…

标准使用术语 conjunction(合取)、disjunction(析取)和 atomic(原子)来描述可以用来构建约束的表达式类型。让我们定义这些术语。

你可以使用 && 和 || 运算符组合概念和约束。这些组合分别称为合取和析取。你可以将它们视为逻辑 AND 和 OR。

通过使用 && 运算符与两个约束结合,形成约束合取:

Template <typename T>

concept Integral_s = Integral<T> && is_signed<T>::value;


合取只有在 && 运算符的两边都满足时才满足。它从左到右进行评估。合取的操作数是短路的,也就是说,如果左边的约束不满足,右边的将不会被评估。

通过使用 || 运算符与两个约束结合,形成约束析取:

Template <typename T>

concept Numeric = integral<T> || floating_point<T>;


析取如果 || 运算符的任一边满足就满足。它从左到右进行评估。合取的操作数是短路的,也就是说,如果左边的约束满足,右边的将不会被评估。

原子约束是一个返回 bool 类型的表达式,不能再进一步分解。换句话说,它不是合取也不是析取。

template<typename T>

concept is_gt_byte = sizeof(T) > 1;


你也可以在原子约束中使用逻辑 !(NOT)运算符:

template<typename T>

concept is_byte = !is_gt_byte<T>;


正如预期的那样,! 运算符将 ! 右边的 bool 表达式的值取反。

当然,我们可以将所有这些表达式类型组合成一个更大的表达式。我们在以下示例中看到了这些约束表达式的例子:

template<typename T>

concept Numeric = is_gt_byte<T> &&

(integral<T> || floating_point<T>);


让我们分解一下。子表达式 (integral<T> || floating_point<T>) 是一个析取。子表达式 is_gt_byte<T> && (…) 是一个合取。而每个子表达式 integral<T>、floating_point<T> 和 is_gt_byte<T> 都是原子的。

这些区分主要是为了描述目的。虽然理解细节是好的,但在你编写代码时,将它们视为简单的逻辑 ||、&& 和 ! 运算符是安全的。

概念和约束是 C++ 标准中受欢迎的补充,我期待着在未来的项目中使用它们。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表