当前位置: 首页 > news >正文

阅读翻译Discovering Modern C++之5.2.3 A `const`-Clean View Example

阅读翻译Discovering Modern C++之5.2.3 A const-Clean View Example

关于

  • 首次发表日期:2024-07-14
  • 书籍介绍请看亚马逊链接:https://www.amazon.com/Discovering-Modern-C-Peter-Gottschling-ebook/dp/B09HTJRJ3V
  • 使用KIMI和ChatGPT机翻,然后人工润色
  • 如有错误,请不吝指出

在这一节中,我们将使用type traits来解决视图(Views)的技术问题。视图是提供对另一个对象不同视角的小对象。一个典型的应用案例是矩阵的转置。提供转置矩阵的一种方式当然是创建一个新的矩阵对象,将相应位置的值交换。这是一个非常昂贵的操作:它需要内存分配和释放,并复制所有交换值后的矩阵数据。视图将更加高效,正如我们将看到的。

5.2.3.1 Writing a Simple View Class(编写一个简单的视图类)

与构建具有新数据的对象不同,视图仅引用现有对象并调整其接口。这对于矩阵转置工作得非常好,因为我们只需要在接口中交换行和列的角色:

在这里插入图片描述

在这个例子中,我们假设 Matrix 类提供了一个接受两个参数(行索引和列索引)的 operator(),并返回对应项 aija_{ij}aij 的引用。我们还假设已经定义了type traits value_typesize_type。这是在这个小例子中我们需要了解的关于所引用矩阵的所有信息(理想情况下,我们会为一个简单的矩阵指定一个concept)。当然,像 MTL4 这样的真实模板库提供了更大的接口。然而,这个小例子足以演示在某些视图中使用元编程的方法。

transposed_view 类的对象可以像常规矩阵一样被对待;例如,它可以被传递给所有期望一个matrix的函数模板。转置是通过调用被引用对象的 operator() 并交换索引时即时实现的。对于每个矩阵对象,我们都可以定义一个像矩阵一样表现的转置视图:
在这里插入图片描述

当我们访问 At(i, j) 时,我们将得到 A(j, i)。我们还定义了一个非 const 访问,这样我们甚至可以更改条目:

At(2, 0)= 4.5;

此操作将 A(0, 2) 设置为 4.5。定义一个转置视图对象并不会导致特别简洁的程序。为了方便起见,我们添加一个返回转置视图的函数:

在这里插入图片描述
现在我们可以在我们的科学软件中优雅地使用 trans,例如,在矩阵-向量乘积中:

v= trans(A) * q;

在这种情况下,会创建一个临时视图并用于乘积计算。由于大多数编译器会内联视图的 operator(),使用 trans(A) 进行的计算将和使用 A 一样快,但访问内存的顺序可能会影响性能。

5.2.3.2 Dealing with const-ness(处理常量性)

到目前为止,我们的视图工作得很好。问题出现在常量矩阵的转置视图中:

const mtl::dense2D <float > B{A};

我们仍然可以创建 B 的转置视图,但无法访问其元素:

cout ≪ "trans(B)(2, 0) = "trans(B)(2, 0) ≪ '
'; // Error

编译器会告诉我们,它无法从一个 const float 初始化一个 float&。当我们查看错误的位置时,会发现这是在操作符的非 const 重载中发生的。这引发了一个问题:为什么没有使用 const 重载,因为它返回的是一个常量引用,完全符合我们的需求。

首先,我们想检查 ref 成员是否确实是常量。我们在类定义或函数 trans 中从未使用 const 声明符。运行时类型识别 (RTTI) 提供了帮助。我们添加了头文件 <typeinfo> 并打印类型信息:
在这里插入图片描述

这将在使用 g++ 编译器时产生以下输出:

在这里插入图片描述

输出在这里不是特别易读。当你使用 Visual Studio 时,使用 typeid 可以看到原始类型名称。不幸的是,在我们看到的所有其他编译器上,RTTI 打印的类型都是名字混淆的。仔细观察时,我们可以看到第二行额外的 K,这告诉我们视图是使用常量矩阵类型实例化的。尽管如此,我们建议不要浪费时间在混淆的名称上。一个简单(且可移植的!)技巧是通过引发类似这样的错误消息来获得可读的类型名称:

int ta= trans(A);
int tb= trans(B);

更好的方法是使用一个名称解码器。例如,GNU 编译器提供了一个名为 c++filt 的工具,它也适用于 clang++。默认情况下,它只解码函数名称,我们需要使用 -t 选项,如下所示:trans const | c++filt -t。然后我们会看到:

在这里插入图片描述

现在我们可以清楚地看到,trans(B) 返回一个模板参数为 const dense2D<...>transposed_view(而不是 dense2D<...>)。因此,成员 ref 的类型是 const dense2D<...>&。当我们回过头来看,这现在变得合理。我们将类型为 const dense2D<...> 的对象传递给了函数 trans,该函数的模板参数类型为 Matrix&。因此,Matrix 被替换为 const dense2D<...>,因此返回类型相应地是 transposed_view<const dense2D<...>>。经过这段短暂的类型内省之旅,我们确定了成员 ref 是一个常量引用。接下来会发生以下情况:

  • 当我们调用 trans(B) 时,函数的模板参数被实例化为 const dense2D<float>
  • 因此,返回类型是 transposed_view<const dense2D<float>>
  • 构造函数参数的类型为 const dense2D<float>&
  • 同样地,成员 refconst dense2D<float>&

仍然存在一个问题,即为什么尽管我们引用了一个常量矩阵,却调用了操作符的非 const 版本。答案是,ref 的常量性对选择并不重要,重要的是视图对象本身是否是常量。为了确保视图也是常量,我们可以这样写:

const transposed_view <const mtl::dense2D <float > > Bt{B};
cout ≪ "Bt(2, 0) = "Bt(2, 0) ≪ '
';

这种方法可行,但相当笨拙。一个粗暴的可能性是去除常量性以使视图能够编译用于常量矩阵。不期望的结果是,常量矩阵的可变视图会允许修改声明为常量的矩阵。这严重违反了我们的原则,以至于我们甚至不会展示这种代码会是什么样子。


**Rule** 将常量性转换视为最后的无奈之举。

以下是处理常量性正确的非常有效的方法。每个 const_cast 都表明设计上存在严重错误。正如Herb Sutter和Andrei Alexandrescu所说,“一旦使用了 const,就不应再回头。” 我们唯一需要使用 const_cast 的情况是处理const不正确的第三方软件,比如将只读参数作为可变指针或引用传递的情况。这不是我们的错,我们别无选择。不幸的是,仍然有许多软件包完全忽视了 const 限定符。其中一些软件包太大了,无法快速重写。我们唯一能做的就是在其上增加适当的API,并避免使用原始API。这样可以避免在我们的应用程序中使用 const_cast,并将难以言说的 const_cast 限制在接口中。Boost::Bindings 就是这样一个很好的例子,它为BLAS、LAPACK和其他类似老式接口的库提供了一个高质量的 const 正确接口(委婉地说)。相反地,只要我们仅使用自己的函数和类,我们就能够通过或多或少的额外工作避免使用任何 const_cast

为了正确处理常量矩阵,我们可以为它们实现第二个视图类,并相应地重载 trans

在这里插入图片描述

使用这个额外的类,我们解决了我们的问题。但我们为此添加了相当多的代码。而且比代码长度更糟糕的是冗余:我们的新类 const_transposed_view 几乎与 transposed_view 相同,只是不包含非 constoperator()

让我们寻找一个更有成效且不那么冗余的解决方案。为此,在接下来的内容中,我们引入了两个新的元函数。

5.2.3.3 Check for Constancy(检查常量性)

在列表5-1中,我们的视图问题在于无法正确处理所有方法中作为模板参数的常量类型。为了修改常量参数的行为,我们首先需要确定参数是否是常量。为此,标准库提供了类型特性 std::is_const。这个元函数可以通过部分模板特化非常简单地实现:

在这里插入图片描述

常量类型匹配两个定义,但第二个更为具体,因此编译器会选择它。非常量类型只匹配第一个。请注意,我们只考虑最外层的类型:模板参数的常量性不被考虑。例如,view<const matrix> 不被视为常量,因为视图本身不是 const

5.2.3.4 Variable Templates(可变模板)

变量模板在元编程中非常有用。除了我们的 is_const type trait,我们可以定义:

template <typename T>
constexpr bool is_const_v = is_const<T>::value;

这使我们在使用值时不必每次都附加 ::value

C++17 为所有基于值的type traits添加了相应的变量模板,后缀为 _v,正如我们在这里所做的,例如,is_pointer 现在伴随着 is_pointer_v。当然,is_const_v 在标准库中也是可用的。

5.2.3.5 Compile-Time Branching (编译时分支)

我们视图所需的另一个工具是根据逻辑条件进行类型选择。这项技术由 Krzysztof Czarnecki 和 Ulrich Eisenecker 引入。标准库中的 编译时条件语句 被命名为 conditional。它可以通过一个相当简单的实现来实现:

在这里插入图片描述

当这个模板与一个逻辑表达式和两种类型一起实例化时,只有主模板(在上面)在第一个参数评估为 true 时匹配,并且在类型定义中使用 ThenType。如果第一个参数评估为 false,那么特殊化(在下面)更具体,因此使用 ElseType。像许多巧妙的发明一样,一旦找到它就非常简单。这个元函数是 C++11 的一部分,在头文件 <type_traits> 中。

这个元函数允许我们定义有趣的事情,比如在我们的最大迭代次数大于100时,使用 double 作为临时类型,否则使用 float

在这里插入图片描述

不用说,max_iter 必须在编译时已知。诚然,这个例子看起来并不特别有用,而且元-if 在小而孤立的代码片段中并不那么重要。相比之下,对于大型通用软件包的开发,它变得非常重要。请注意,比较操作被括号括起来;否则大于号 > 将被解释为模板参数的结束。同样地,在 C++11 或更高版本中,包含右移操作符 >> 的表达式也必须由于同样的原因用括号括起来。

C++14 引入了模板别名,以便我们在引用结果类型时不必键入 typename::type

在这里插入图片描述

5.2.3.6 The Final View (最终视图)

现在我们已经拥有了区分所引用的 Matrix 类型常量性的工具。我们可以尝试让mutable的[]运算符消失,正如我们将在第5.2.6节中对其他函数所做的那样。不幸的是,这种技术只适用于函数本身的模板参数,而不适用于enclosing class的模板参数。

因此,我们同时保留可变的和常量的访问运算符,但根据模板参数的类型选择前者的返回类型:

在这里插入图片描述

这种实现根据可变视图与所引用的矩阵的可变性区分了可变视图的返回类型。这确立了所需的行为,如下案例将展示。当引用的矩阵是可变的时,operator() 的返回类型取决于视图对象的常量性:

  • 如果视图对象是可变的,则第14行的 operator() 返回一个可变引用(第10行);
  • 如果视图对象是常量的,则第17行的 operator() 返回一个常量引用。

这与列表5-1中之前的行为相同。如果矩阵引用是常量的,那么总是返回一个常量引用:

  • 如果视图对象是可变的,则第14行的 operator() 返回一个常量引用(第9行);
  • 如果视图对象是常量的,则第17行的 operator() 返回一个常量引用。

总之,我们实现了一个视图类,仅当视图对象和引用的矩阵都是可变时提供写入访问。


文章转载自:

http://mA269kOG.crsqs.cn
http://ejztHKqh.crsqs.cn
http://zOepLi8l.crsqs.cn
http://r2nmaHEc.crsqs.cn
http://jvqBRYZ6.crsqs.cn
http://tz8IYuqr.crsqs.cn
http://TPUvzXcT.crsqs.cn
http://UvHLj7Ay.crsqs.cn
http://oWZPbOAW.crsqs.cn
http://ExgL9Y0W.crsqs.cn
http://rhh86rGF.crsqs.cn
http://mDLQCxeP.crsqs.cn
http://YtAL7X1X.crsqs.cn
http://aGNcBA23.crsqs.cn
http://3mfiS4mP.crsqs.cn
http://8Ffb6YJY.crsqs.cn
http://30U8bfmf.crsqs.cn
http://n15loFnf.crsqs.cn
http://HCsg5pId.crsqs.cn
http://jVBDCE2x.crsqs.cn
http://oCFYjA17.crsqs.cn
http://JHnb5QMC.crsqs.cn
http://QbUE0iBj.crsqs.cn
http://Z7LrwRkX.crsqs.cn
http://wGYpVUql.crsqs.cn
http://OxrNm8Q8.crsqs.cn
http://RYxyyAME.crsqs.cn
http://5Ra2CjSy.crsqs.cn
http://LNkhyTIN.crsqs.cn
http://eA2C6Arr.crsqs.cn
http://www.dtcms.com/a/380216.html

相关文章:

  • MUSIC, Maximum Likelihood, and Cramer-Rao Bound
  • APT32F0042F6P6 32位微控制器(MCU)单片机 APT爱普特微电子 芯片核心解析
  • react3面试题
  • LeetCode 344.反转字符串
  • 【C++】list模拟实现全解析
  • C++动态规划算法:斐波那契数列模型
  • 第六章:AI进阶之------python的变量与赋值语句(二)
  • 传统项目管理流程有哪些?深度分析
  • 导购电商平台的服务治理体系构建:熔断、限流与降级机制实现
  • Axios 中设置请求头
  • 十四十五. 图论
  • Transporter App 使用全流程详解:iOS 应用 ipa 上传工具、 uni-app 应用发布指南
  • 缺失数据处理全指南:方法、案例与最佳实践
  • 【后端】Java封装一个多线程处理任务,可以设置任务优先级优先插队处理,并且提供根据任务ID取消任务
  • 数据通信学习
  • Coze源码分析-资源库-创建知识库-前端源码-核心组件
  • GEO 优化工具:让品牌被 AI 主动推荐的关键!
  • 调用京东商品详情API接口时,如何进行性能优化?
  • 鸿蒙审核问题——折叠屏展开态切换时,输入框内容丢失
  • JAiRouter GitHub Actions 自动打包发布镜像到 Docker Hub 技术揭秘
  • 破壁者指南:内网穿透技术的深度解构与实战方法
  • TOGAF——ArchiMate
  • 吃透 Vue 样式穿透:从 scoped 原理到组件库样式修改实战
  • Linux网络:初识网络
  • 【Docker-Nginx】通过Docker部署Nginx容器
  • 测试es向量检索
  • 统计与大数据分析专业核心工具指南
  • Qtday2作业
  • LazyForEach性能优化:解决长列表卡顿问题
  • 封装从url 拉取 HTML 并加载到 WebView 的完整流程