网站首页 > 博客文章 正文
添加到C++ 20中的concept形式被称为lite。这是因为它们没有提供一个非常重要的功能:让编译器检查有约束模板的作者是否只使用约束concept允许的操作和类型。换句话说,我们可以说我们的模板只要求操作A和B是有效的,但是我们仍然可以在内部使用一些其他操作,这对于编译器来说是很好的。在这篇文章中,我们将展示这是如何存在问题的,甚至对于意识到这个问题的程序员,以及如何用concept原型来解决它。
这个例子有点做作,但我们需要把它弄的小一点。我们将使用sum()函数作为范型算法的示例。它实现两个值的相加,前提是它们是可添加的:
template <Addable T>
T sum(T a, T b);
我们要求我们的类型T是regular的,并提供运算符+和+=:
template <typename T>
concept Addable =
std::regular<T> &&
requires(T x, T y) {
{ x += y } -> std::same_as<T&>;
{ x + y } -> std::convertible_to<T>;
};
在满足这些约束的情况下,我们可以提供加法的各种实现。例如这个:
template <typename T>
T sum_impl(T a, T b)
{
assert (a >= b);
a += b;
return a;
}
template <Addable T>
T sum(T a, T b)
{
if (b > a)
return sum_impl(b, a);
else
return sum_impl(a, b);
}
让我们假设我们已经发现,当大的数字在左边时最有效的实现是使用+=而不是+。关键是,当我们以concept的形式提供了接口时,如何实现功能就是我们的工作,只要我们满足合同约定。另外一个小观察是,我们可以在有约束模板中调用无约束模板,这是可以的。
现在假设我们正在实现一个其他人将要使用的范型库。函数模板不是一段可以执行的代码。只有在使用用户提供的类型实例化时,它才会变成一段可以执行的代码。模板定义可以编译并不意味着它的函数体是正确的:模板中的大多数错误在我们尝试实例化它们时会显示出来。因此,为了提供一个像样的库,我们必须测试我们的模板是否可以实例化为addable类型:
int main()
{
int i = 1, j = 2;
std::cout << sum(i, j) << std::endl;
double x = 2.25, y = 5.25;
std::cout << sum(x, y) << std::endl;
}
可以编译、运行,输出:
3
7.5
所以,这个实现可以工作。但这些都是非常基本的类型。如果用户将在我们的函数中使用他们的自定义可添加类型呢?这也需要测试。让我们用Boost.Rational库:
#include <boost/rational.hpp>
// verify if the type can be used with the library
static_assert(Addable<boost::rational<int>>);
int main()
{
boost::rational<int> q1{1, 2}; // 1/2
boost::rational<int> q2{1, 3}; // 1/3
std::cout << sum(q1, q2) << std::endl;
std::cout << sum(q2, q1) << std::endl;
}
输出:
5/6
5/6
可以工作!我们还可以查看Boost的big int库:
#include <boost/multiprecision/cpp_int.hpp>
static_assert(Addable<boost::multiprecision::cpp_int>);
int main()
{
boost::multiprecision::cpp_int i{"770000"},
j{"119999"};
std::cout << sum(i, j);
}
可以工作,输出:
889999
现在我们可以放心地将我们的库发送给客户。
我们的客户打算使用标准库类型:std::complex的库。第一步,她验证她的类型是否符合我们的concept:
#include <complex>
#include "addable_library.hpp"
static_assert(Addable<std::complex<double>>);
编译正常,因此std::complex符合Addable。现在,她可以正确使用库:
int main()
{
std::complex<double> z1{1, 0}, z2{0, 1};
std::cout << sum(z1, z2);
}
这个程序无法编译。我们得到一条老式的模板错误消息,它公开了库函数的内部实现,该函数使用运算符>。并且std::complex<double>不提供运算符>或>=。
看来我们的实现使用了我们没有在concept中列出的操作。但是,由于addable但不提供关系操作的类型非常少见,所以这个bug可能会在很长一段时间内不被注意到。即使在一个简短的函数中也不容易发现这一点。编译器也不想帮我们。(作为补充说明,编译器最好不要这样做是有充分理由的。)
为了避免这种情况,我们必须检查在实现模板时我们是否只使用了concept中需要的接口。为了做到这一点,我们可以为我们的concept定义一个原型;也就是说,一个只提供在concept中指明的接口的类型,其他一个操作也不多。
我们来定义一个:
template <typename T>
concept Addable =
std::regular<T> &&
requires(T x, T y) {
{ x += y } -> std::same_as<T&>;
{ x + y } -> std::convertible_to<T>;
};
从一个空的类开始:
class A {};
但它并不是真的是空的:它的接口已经提供了构造函数(default,copy,move)、赋值(copy,move)、析构函数和其他一些操作符,比如operator&and operator,。为了使我们的类型真正为空,我们必须将这些操作声明为已删除。但由于我们的concept已经要求存在上述构造函数、赋值和析构函数,所以我们不会删除它们。此外,为了明确我们的意图,我们将其标记为违约:
class A
{
public:
A() = default;
A(A&&) = default;
A(A const&) = default;
A& operator=(A &&) = default;
A& operator=(A const&) = default;
~A() = default;
void operator&() const = delete;
friend void operator,(A const&, A const&) = delete;
};
这种类型已经满足std::semiruler concept,它是std::regular的一部分,而std::regular又是Addable的一部分。它没有做任何有用的事情,但它很好:它的目标只是满足concept。我们可以测试一下:
static_assert(std::semiregular<A>);
现在,为了满足std::regular,我们的类型需要提供相等和不等运算符。在C++ 20中,我们可以用一个声明来完成:
class A
{
public:
// ...
friend bool operator==(A const&, A const&) = default;
};
// test it:
static_assert(std::regular<A>);
接下来是两个相加运算符,我们可以这样做:
class A
{
public:
A& operator+=(A const&) { return *this; }
friend A operator+(A const&, A const&) { return {}; }
// ...
};
但这并不是我们concept的最小满足。回看:
template <typename T>
concept Addable =
std::regular<T> &&
requires(T x, T y) {
{ x += y } -> std::same_as<T&>;
{ x + y } -> std::convertible_to<T>;
};
运算符+的要求是它的结果可以转换为T,但不一定是T。如果我们忽略了这一点,我们将面临与使用运算符>观察到的相同的问题:由Addable约束的算法可能依赖于加法的结果正好是T。大多数类型都满足它,所以它不会被注意到,但是,库用户以后可能会使用一个类型,其中加法的结果不是T,而是可以转换为T的东西。所以,我们必须考虑到这一点。最小的实现可以如下所示:
class A
{
struct Rslt {
operator A() { return {}; }
};
public:
A& operator+=(A const&) { return *this; }
friend Rslt operator+(A const&, A const&) { return {}; }
// ...
};
完整的类定义:
class A
{
struct Rslt {
operator A() { return {}; }
};
public:
A() = default;
A(A&&) = default;
A(A const&) = default;
A& operator=(A &&) = default;
A& operator=(A const&) = default;
~A() = default;
void operator&() const = delete;
friend void operator,(A const&, A const&) = delete;
A& operator+=(A const&) { return *this; }
friend Rslt operator+(A const&, A const&) { return {}; }
friend bool operator==(A const&, A const&) = default;
};
我们现在可以给它一个更好的名字,并测试原型是否真的满足这个concept:
using AddableArchetype = A;
static_assert(Addable<AddableArchetype>);
现在我们已经证明了我们的原型对concept进行了建模,并且我们确信(尽管这不能静态地证明),它是对concept建模的最小接口。换句话说,现在我们确信我们有一个原型。
下一步,我们可以用它来测试我们算法的实现。我们只需定义一个非模板函数,用我们的原型实例化算法sum:
inline void test_concept_usage(AddableArchetype a,
AddableArchetype b)
{
sum(a, b);
}
此函数不用被调用;参数a和b也不用初始化。只要函数体的存在就足以保证sum的整个实现被实例化,并测试是否仅使用声明的操作。
可以测试。程序无法编译。错误消息是老式消息,公开了实现细节:类型AddableArchetype不提供operator>或operator>=。这是我们以前见过的错误。但这一次是库作者在库发布之前看到错误,而不是库用户在太晚的时候看到错误。我们的测试发现了一个错误。这个bug要么是在sum()函数的实现中,要么是在可添加的concept中——目前还不清楚。但因为我们提前得到警告,我们可以做出有意识的选择。
我们有几个选择。一,我们可以判断我们的算法的实现走得太远了。这种实现是非法的,必须进行更改,以便只使用concept规定的操作。二,我们可以得出一个相反的结论:按照我们的方式实现算法的经验表明,我们最初关于concept应该要求什么的想法是不正确的,现在必须加以改进。修改一个concept的决定并不容易,但是当我们在实现算法的过程中了解了一些关于我们领域的新知识时,可能是必要的。最后,可以提供sum()的两个重载:一个需要较少的参数,另一个提供优化:
emplate <Addable T>
T sum(T a, T b);
template <Addable T>
requires std::totally_ordered<T>
T sum(T a, T b);
假设我们最终决定:concept Addable还需要类型提供关系运算符;这将使它接近代数结构有序组。(这不一定是最好的决定,但我想说明改变concept的过程是如何运作的。)我们必须改变concept的定义:
template <typename T>
concept Addable =
std::regular<T> &&
std::totally_ordered<T> &&
requires(T x, T y) {
{ x += y } -> std::same_as<T&>;
{ x + y } -> std::convertible_to<T>;
};
现在我们的原型不再满足这个concept了。我们添加的静态检查将提醒我们:
static_assert(Addable<AddableArchetype>);
// ^--- this now fails
我们可以通过将默认的相等运算符替换为默认的spaceship操作符来修复我们的原型:
class A
{
struct Rslt {
operator A() { return {}; }
};
public:
A() = default;
A(A&&) = default;
A(A const&) = default;
A& operator=(A &&) = default;
A& operator=(A const&) = default;
~A() = default;
void operator&() const = delete;
friend void operator,(A const&, A const&) = delete;
A& operator+=(A const&) { return *this; }
friend Rslt operator+(A const&, A const&) { return {}; }
friend auto operator<=>(A const&, A const&) = default;
};
现在,原型再次满足更新后的concept,静态测试test_concept_usage使用情况通过,这使我们确信我们的算法只使用concept中定义的操作。这个库的用户仍然不能在std::complex中使用它,但这一次,他们将得到一个明确的消息:您的类型不满足concept Addable。
就这样。最后,在这个例子中,我们看到了一个实现简单的原型。可以让原型实际做一些有用的事情,比如计算接口中单个函数的调用次数,或者为了测试异常安全性而从函数中抛出异常。
猜你喜欢
- 2024-10-12 C++核心准则T.24:用标签类或特征区分只有语义不同的概念
- 2024-10-12 用苹果发布会方式打开C++20(苹果在哪开发布会)
- 2024-10-12 C++核心准则T.25:避免互补性约束(规矩是一种约束,一种准则)
- 2024-10-12 C++核心准则T.21:为概念定义一套完整的操作
- 2024-10-12 C++核心准则T.5:结合使用泛型和面向对象技术应该增强效果
- 2024-10-12 C++经典书籍(c++相关书籍)
- 2024-10-12 C++一行代码实现任意系统函数Hook
- 2024-10-12 C++核心准则T.11:只要可能就使用标准概念
- 2024-10-12 C++核心准则T.48:如果不能用概念,用enable_if
- 2024-10-12 C++核心准则T.13:简单、单类型参数概念使用缩略记法更好
你 发表评论:
欢迎- 最近发表
-
- 给3D Slicer添加Python第三方插件库
- Python自动化——pytest常用插件详解
- Pycharm下安装MicroPython Tools插件(ESP32开发板)
- IntelliJ IDEA 2025.1.3 发布(idea 2020)
- IDEA+Continue插件+DeepSeek:开发者效率飙升的「三体组合」!
- Cursor:提升Python开发效率的必备IDE及插件安装指南
- 日本旅行时想借厕所、买香烟怎么办?便利商店里能解决大问题!
- 11天!日本史上最长黄金周来了!旅游万金句总结!
- 北川景子&DAIGO缘定1.11 召开记者会宣布结婚
- PIKO‘PPAP’ 洗脑歌登上美国告示牌
- 标签列表
-
- ifneq (61)
- messagesource (56)
- aspose.pdf破解版 (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- vue数组concat (56)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)