大多数浏览器和
Developer App 均支持流媒体播放。
-
理解概念以简化 C++ 模板
了解如何借助 C++20 的功能,将您的 C++ 代码提升到全新境界。我们将介绍相关概念,并探索它们如何能帮助您更迅速地发现泛型 C++ 代码中的错误。我们还将讨论最新增强的 constexpr 功能,了解它如何帮助在编译时评估代码,从而改进 App 的性能。
资源
相关视频
WWDC22
-
下载
♪ ♪
Alex: 大家好 我是Alex 从事开发工具相关的工作 今天我想和大家谈谈 Xcode 14 支持的 C++ 20 的新功能 我将重点介绍 C++ 20 概念如何简化 以及提高泛型 C++ 代码的 类型安全性 我将演示如何使用概念 并解释如何创建自己的概念 最后 我将列出 Xcode 支持的 C++ 20 的其他几个新功能 并将介绍其中一些功能 如何通过编译时代码求值 来提高 C++ 项目的性能
在深入研究 C++ 概念之前 让我们先快速浏览一下 如何用 C++ 编写泛型代码 假设我想写一个函数来检查 一个数是否是奇数 我可以编写一个接受 int 形参的函数 它可以处理任何可以用 int 类型表示的值 如果我传入一个 64 位无符号整数值会发生什么 像这样的具体函数 在 64 位值的情况下不能正确运行 因为它们会被截断以适应 int 类型 为了解决这个问题 我可以将 isOdd 作为函数模板 现在我有了一个函数模板 可以将 64 位无符号整数值传递给它 编译器现在将自动生成 isOdd 的特化(Specialization) 该特化可以正确使用 uint64_t 类型 这真的很有用 因为这意味着我不必编写 两个版本的 isOdd 来操作两种不同的类型 您可以使用 C++ 模板编写泛型函数 如 isOdd 以及泛型容器类 让我们看看如何使用 isOdd 我在测试文件中添加了几个测试用例 来测试这个函数 不幸的是 我在测试中犯了一个错误 编译器发现了这个错误 但没有指出我在哪里犯了错误 而是在 isOdd 模板中显示了一个错误 看起来好像我打错了字 我在测试中写了 1.1 而不是 11 因此 编译器会生成 接受 double 类型的 isOdd 功能 不幸的是 我花了一段时间 才找到这个错误 因为 Xcode 没有指出 isOdd 被错误类型调用的具体位置
语言和编译器能帮我更快地 找到这样的错误吗 在当前的示例中 没有明确指定 允许进入 isOdd 的类型的要求 只有文档注释说我必须使用 整数类型调用 isOdd 时 才可以 在 C++ 20 之前 C++ 程序员 在编写泛型 C++ 代码时 没有很好的方法来指定模板要求 在指定模板要求时 他们通常需要 求助于 文档注释 特定的参数名称 或复杂的 enable_if 检查 您可能听说过 C++ 20 引入了一个新的 C++ 功能 叫做 概念(Concepts) 您可以使用概念来验证 泛型 C++ 代码中的模板要求 让我们来看看概念是如何帮助我验证 可以传入 isOdd 的类型的 首先 让我们回到 isOdd 的声明中 目前 我使用 class 关键字 来指定这个模板使用的类型 T 可以是任何类型 C++ 20 允许我使用一个概念 而不是 class 关键字 来限制这个模板可以使用的类型集 我可以使用标准库提供的 整数(Integral)概念 将此 isOdd 函数模板 限制在内置的整数类型 当 T 不满足这个概念时 编译器甚至不会尝试 特化这个函数模板 整数概念是在 C++ 标准库中声明的 因此 我需要包含概念头(Concepts Header) 才能在代码中使用它 现在 我在“isOdd”函数模板中的 type _T_ 中 添加了一个 integral 要求 编译器能够提供一个 更清晰的诊断 直接指出 我在测试中哪里出错了 结果表明 1.1 是一个双精度实数 因此 不满足 integral 概念 编译器能够用一条清晰的 错误消息向我解释这一点 帮助我比以前更快地 找到并修复这个拼写错误 除了帮助我修复这个错误之外 将传递的类型约束为 isOdd 使我放心 因为我所有的 isOdd 测试用例只适用于整数类型 而且它们实际上是在测试 算法的预期行为 您可以使用概念来声明 您的模板打算用于哪些类型 然后 编译器会在模板特化之前 验证类型要求 让我们仔细看看如何使用概念 以及 C++ 标准库提供了哪些核心概念 C++ 标准库提供了一个概念库 它实现了一组核心语言概念 您可以使用这些概念 来验证类型的核心行为 您可以通过在代码中包含 概念头来访问这个库
我已经在前面的例子中展示了 如何使用 integral 的概念 现在 让我们看看 这个库提供的其他概念 这个库提供了许多有用的 核心语言概念 比如测试一个类型 是否是内置类型的概念 例如 floating_point 的概念 被 float 和 double 这样的 内置类型所满足 这里显示的 static_assert 验证了情况确实如此 它还提供了许多其他有用的核心概念 用于检查类型是可构造的 可破坏的 可转换的 还是与其他类型相同 例如 convertible_to 概念测试 一种类型是否可以转换为另一种类型 而 move_constructible 概念是由 可以直接从同一类型的另一个值 构造的类型来满足的 该库还提供了几个比较概念 用于测试类型是否可以 与其他类型进行比较 例如 如果类型具有有效的 == 运算符 并且该运算符 与相同类型的值一起使用 则满足 equality_comparable 概念
除了本页面中提到的概念外 该库还提供了许多其他核心语言概念 它还提供了测试类型是否 可以移动或复制的概念 除此之外 它还提供了 检查类型是否是 某个可调用对象的概念
既然我们已经了解了 C++ 标准库 提供的概念 那么让我们来看看 如何使用概念来约束模板 如前所述 您可以在模板中 使用概念而不是 class 关键字 来限制该模板允许哪些类型 除此之外 如果需要将一个类型 约束到多个概念 则可以在 模板声明中使用 requires 子句 让我们看一个稍微不同的例子 看看如何实现它
这里我有一个 isDefaultValue 函数模板 如果给定值等于其类型的默认值 则返回 true 在特化该模板之前 我可以使用标准库中的 两个概念来测试该类型 是否支持这些操作 我将添加 requires 子句 来限制此函数模板 所允许的类型集 让我们看看概念库中的哪些概念 可以帮助我验证 这里的类型 首先 equality_comparable 概念 测试是否可以与相同类型的 另一个值进行比较 然后 default_constructible 概念 测试 if _T_ 是否是 具有默认构造函数的类型 它们之间的逻辑和运算符指示编译器 验证这两个概念 这确保此函数模板将仅特化 受支持的类型 让我们回顾一下到目前为止 我们所学到的概念 您应该使用概念来限制 允许在模板中使用的类型 然后 编译器将能够显示 更清晰的诊断信息 因为如果发生类型不匹配 则不必对模板进行特化 如果需要验证某个类型的 一些核心行为 则应该重用概念库中的概念
当需要测试类型是否符合多个要求时 应该将 requires 子句添加到模板中 现在我们已经了解了如何在 C++ 程序中使用概念 C++ 允许我们声明自定义的概念 来验证一个类型的特定行为 让我们看看如何创建我们自己的概念 来验证特定类型的行为 不过 在此之前 我们需要了解一下 如何确定必须由我们想要声明的概念 来验证的行为要求 我将使用一个新示例来说明 如何使用概念来验证特定类型的行为 假设我正在构建一个 C++ 库 它可以将各种二维形状渲染为图像
我想在我的库中支持各种形状 我从一个圆形开始 因为它是最简单的渲染 我将使用一个 C++ 类 来存储它的属性 比如位置和半径 为了渲染这个圆形 我将使用基于距离函数的渲染算法 该算法在渲染图像的每个像素上运行 该算法需要计算到形状表面的距离 以便对其进行渲染 圆形类中的 getDistanceFrom 方法 可以计算它 它在圆内返回负距离 在圆外返回正距离 除了这个圆 我还想渲染其他形状 例如 通过从一个圆形中 减去另一个圆形 我也可以绘制一个月牙形(Crescent) 我打算用类来表示像月牙这样的形状 同时也想用类来渲染 每个新的形状类都包含 getDistanceFrom 方法 在创建了几个形状类之后 我现在想尝试渲染这些形状 来验证它们的实现 对于如何创建 可用于任何形状的渲染函数 我有几个选项 我可以为形状创建一个类层次结构 并使用虚方法 来计算到形状表面的距离 然而 出于性能原因 我将使用函数模板来代替 因为我想避免虚调用的开销 因为该函数在呈现期间 将被调用数百万次 这就是我创建这个 渲染函数模板的原因 computePixelColor 函数 接受一个形状值 并检查给定的像素是否在形状内 如果它在内部 则返回纯白色 现在 我可以验证 形状是否可以正确填充
这个函数是一个模板 这使得它可以处理任何形状类型 无论是圆形 月牙形 还是任何其他匹配类型 尽管模板在这里运行良好 我还是想用概念来约束 可以传递给这个函数的类型 约束传递给此函数的类型 将允许编译器在发生 类型不匹配时产生更清晰的诊断 除此之外 约束传递给此函数的类型 还允许我添加此函数的其他重载
为了约束类型 我将创建一个形状(Shape)概念 这个概念将验证类型的行为 并将接受像圆形 月牙形 以及我将来 可能想要添加的任何其他形状类 为了创建一个像 Shape 这样的概念 我首先需要确定 必须由该概念验证的要求 让我们看看如何能做到这一点 此函数模板使用类型 T 作为泛型类型 然后 将一个名为 shape 的 类型为 T 的参数传递给此函数 然后 当我对它调用 getDistanceFrom 方法时 在函数内部使用 shape 参数 如您所见 这是我想在概念中验证的 唯一要求 因为此函数中没有 对形状执行其他操作 您可以使用 requires 表达式 来测试类型是否 以特定方式运行 让我们来看看如何使用 requires 来创建形状概念 我需要提供一组表达式 来测试 requires 中类型的行为 我已经将对 getDistanceFrom 的调用 确定为我需要测试的一个要求 所以现在我可以继续 创建 Shape 概念了 我使用 concept 关键字声明了 shape 概念 然后 我在这个概念中添加了 requires 表达式来验证类型 我在 requires 表达式中 添加了一个参数列表 此参数列表允许我声明 类型为 T 的值 shape 然后我将在 requires 中测试该值 您可以在 requires 表达式中 使用参数列表来声明任何类型的值 然后 您将能够在 requires 中 使用这些值 requires 表达式的主体包含一组 为了满足这个概念而必须通过的要求 shape 概念只有一个 简单的表达式要求 即检查对 getDistanceFrom 的 方法调用是否有效 这个表达式实际上不会在程序中执行 只有在编译时才需要它 来验证类型的行为 在验证之后就会丢弃它 通过测试特定的表达式是否可编译 您可以使用表达式要求 来验证类型的行为 这个特殊的表达式还不完整 因为我们缺少 getDistanceFrom 方法调用的参数 我知道我想让这个方法接受 两个浮点类型的值 所以我可以使用两个浮点文字 来完成这个表达式 我将添加一个额外的检查来测试 getDistanceFrom 方法 是否返回一个浮点值 因为这是我的泛型代码所假设的 我目前正在使用一个 简单的表达式要求 来测试该类型是否具有 getDistanceFrom 方法 但我可以使用复合要求 而不是表达式要求 来测试它是否返回一个浮点值 箭头运算符可以遵循复合要求 箭头运算符的右边需要一个约束 所以这里我可以使用一个 标准的库概念 比如 same_as 来验证对 getDistanceFrom 方法的调用 是否返回一个浮点值 现在这个概念在我看来已经准备好了
我可以继续使用它来约束可以传递给 computePixelColor 函数的类型 现在 我的泛型 ComputePixelColor 函数 将只处理满足 shape 概念的类型 这意味着像圆形和月牙形这样的类 将使用这个特殊的泛型 computePixelColor 函数来呈现 因为这两种类型都满足 shape 概念
在看到普通形状的渲染后 我想创建一个不同版本的 computePixelColor 为我的一些形状添加颜色 假设我想向我的形状库添加一个 彩色 GradientCircle 类 我现在需要一个新函数 来计算图像中的像素颜色 C++20 允许我创建 computePixelColor 函数模板的 多个变体 每个变体都必须使用 不同的概念进行约束 我将创建一个新的 GradientShape 概念 它将被 GradientCircle 这样的类所满足 然后 这个概念将约束一个新的 computePixelColor 变体 该变体仅适用于具有渐变的形状 这个概念是使用 requires 表达式实现的 就像形状概念一样 但是 由于我想让 GradientShape 也满足原来的形状概念 所以我把它作为新概念的第一个要求 这确保了满足 GradientShape 概念的类 也满足形状概念 这意味着我仍然可以为此类的值 调用 getDistanceFrom 方法 然后我使用逻辑和操作符 以及 requires 表达式 来确保只有具有 getGradientColor 方法的类 才能满足 GradientShape 概念 现在我已经创建了 GradientShape 概念 我可以继续创建一个新的变体 computePixelColor 这个函数模板只适用于 带有渐变的形状类 比如 GradientCircle 类 因为它受 GradientShape 概念的约束 现在我已经准备好了所有的部分 我可以继续尝试用渐变渲染一个圆形 这里我正在渲染 GradientCircle 让我们看看编译器 会在 render 函数中 选择哪个 computePixelColor 的重载
尽管 GradientCircle 可以安全地与 computePixelColor 的两种变体 一起使用 但编译器会选择 受 GradientShape 概念 约束的重载 因为它比第一个重载更具体 因为编译器选择了 computePixelColor 最匹配的重载 所以当我测试库时 可以看到这个美丽的渐变圆 太不可思议了
现在让我们回顾一下我们所学到的 关于创建概念的内容
您可以通过识别现有泛型代码中的 行为要求来创建概念 您应该使用 requires 表达式 创建概念来验证类型的行为 您还可以使用概念来创建 泛型函数和类的 更具体的变体
我们现在已经了解了 如何用概念来增强泛型 C++ 代码 除了支持概念 Xcode 14 还改进了 对其他 C++ 20 功能的支持 更具体地说 我想强调 在 Xcode 14 中对编译时 C++ 代码评估的改进支持 编译时代码评估非常有用 因为它可以降低 C++ 代码中 变量的初始化成本 如果您的 App 有大量 依赖于复杂初始化序列的 C++ 代码 这可以帮助减少 App 的启动时间 除此之外 编译时代码求值 可以帮助您验证需要在编译时 进行验证的常量 这可以帮助您在 代码运行之前就发现错误 我们看一个例子 看看如何在 C++ 中 使用编译时代码求值
这里有一段代码 用于初始化形状渲染库中的调色板 然后在 iOS App 中使用该库 将形状渲染到显示器上 调色板中的每种颜色都是通过解析 一个带有颜色的 HTML 十六进制代码的 字符串字面来初始化的 目前 fromHexCode 函数 需要在数组初始化期间 解析三个字符串文字 如果我有很多这样复杂的 常量初始化操作 它们会对我的 App 的启动时间 产生相当大的影响 我可以使用编译时代码求值 来确保该数组 使用常量颜色值进行初始化 我向您演示一下如何完成这个操作 constexpr 关键字在 C++ 中 支持编译时代码求值 我必须在示例中的几个地方添加它 以确保调色板是一个恒定的颜色数组 首先 我需要将 constexpr 关键字 添加到 fromHexCode 函数中 当在编译时初始化序列中 使用该函数时 编译器现在可以在编译时 执行该函数中的代码 如果您希望 C++ 函数 在编译时可求值 那么应该将其设置为 constexpr 当您在 constexpr 初始化序列中 使用该函数时 编译器会显示错误让您知道 这种函数中的代码 在编译时不能被求值 不过 您也可以在 添加 constexpr 之前检查函数 以查看是否可以在 编译时对其进行求值 让我们来看看 fromHexCode 看看如何检查这样的函数 是否适合进行编译时代码求值 该函数使用许多语言结构 如 if 语句 以及基本运算 如比较运算符和算术运算符 所有这些操作都可以在 编译时进行求值 此外 该函数还多次调用 另一个函数 hexToInt 我已经用 constexpr 注释了 hexToInt 函数 因此对这个函数的调用 可以在编译时进行求值 总的来说 看起来 fromHexCode 包含了编译器应该能够在 编译时求值的代码 所以我认为在编译时 初始化序列中使用它是安全的 在确保在编译时可以 计算 fromHexCode 之后 我需要将 constexpr 关键字 添加到 colorPalette 变量声明中 编译器现在保证在编译时 计算该数组的整个初始化序列 更具体地说 编译器将计算 对 fromHexCode 函数的每次调用 求值将生成一个恒定的颜色值 该值将替换调色板初始值 设定项中对函数的原始调用 由于对 fromHexCode 的所有调用 现在都被常量颜色值所取代 colorPalette 变量现在保证 由包含常量颜色值的 数组文本初始化 这意味着当这个调色板初始化时 我的 App 不需要为解析颜色值 支付额外的成本 这大大缩短了我的 App 启动时间 因为它减少了 App 内部的 C++ 库 在启动时必须完成的工作量 当您想要确保 C++ 变量使用常量值 进行初始化时 您应该将它们设置为 constexpr Xcode14 实际上极大地改进了 它对编译时求值的 标准库支持 今年 我们为几个不同的标准库类型 和算法添加了 constexpr 支持 现在可以在编译时 代码求值期间使用它们
除此之外 Xcode14 还极大地改进了 对 C++ 20 标准的支持 这里显示的所有功能现在都可以 在 C++ 20 模式下使用
如果您还没有切换到 C++ 20 模式 那么今天就应该尝试下 您可以在 Xcode 项目中使用 C++ Language Dialect 来升级到 C++ 20 切换到 C++ 20 可以使您在 代码中使用概念等功能 C++ 20 不需要最低的部署目标 因此您仍然可以为当前目标的 相同 OS 版本发布代码 今天就试试 C++ 20 吧 谢谢 请享受余下的开发者大会之旅吧
-
-
0:02 - snippet1
int main() { }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。