网站首页 > 基础教程 正文
本章涵盖
- 练习算法和执行策略
- 模板参数包
- std::visit 方法和 Overload 模式
- 可变的 lambda 表达式
- 额外练习变体、 std::format 和范围
我们已经多次使用参数包(模板中的三个点),但我们还没有停下来理解它们是如何工作的。在最后一章中,我们将填补这些空白,并练习我们迄今为止学到的许多内容。我们将生成三角数,并简要考虑它们的一些属性。三角数在各种地方出现(例如,计算一群人中如果每个人都握手会发生多少次握手)。因为我们从帕斯卡三角形开始,回到数字序列似乎是一个很好的总结方式。
我们将发现可以使用数值算法在几行代码中创建三角形数字,然后我们将使用前几个三角形数字构建一个老虎机。我们将首先构建一个简单的机器,仅旋转转轴。然后我们将改进游戏,允许保持、轻推或旋转。为了实现这些选项,我们将学习关于 std::visit 和 Overload 模式的知识。我们将练习在前几章中学到的内容,这将帮助我们使用新特性编写更多的 C++,并有信心跟上未来的任何变化。
9.1 三角数
三角数是 1、3、6、10,依此类推,通过求和 1、1 + 2、1 + 2 + 3、1 + 2 + 3 + 4 等形成。如果我们积累了那么多的台球,就可以形成一个三角形。因此得名。要在图 9.1 中显示的五行上再添加一行,我们需要六个额外的台球。再添加一行将增加七个,以此类推。
我们将在本章中使用前几个三角形数字,因此让我们创建一个名为 make_ triangle_numbers 的函数。我们将接受一个 count 并返回一个 vector 的 int 。 std::vector 和 std::string 自 C++20 以来支持 constexpr (请参见 http://mng.bz/wjDP),因此我们可以将该函数标记为 constexpr ,我们在第 3 章首次看到它,当时我们学习了如何使用 static_assert 进行测试。我们也将能够在这里执行类似的检查。我们的新函数以以下签名开始:
constexpr std::vector<int> make_triangle_numbers(int count)
让我们添加细节。如果我们从数字 1、2、3 等开始,我们可以将这些数字相加以获得三角数。C++11 在 numeric 头文件中引入了 iota 函数,该函数用从选定值开始的顺序递增的值填充容器。如果我们创建一个可以容纳 20 个数字的向量
std::vector<int> numbers(20);
我们可以然后调用 iota ,从值 1 开始,创建数字 1、2、3,依此类推:
std::iota(numbers.begin(),numbers.end(), 1);
或者,我们可以使用在 C++23 中引入的范围版本:
std::ranges::iota(numbers, 1);
C++23 还没有得到广泛支持,因此您可能需要等到您的编译器提供范围版本。在任何情况下,这将用从 1 开始并每次增加 1 的数字填充 vector 。这给我们 1, 2, 3,...20。 iota 函数来自 APL 编程语言,并在 C++11 之前提出,但直到后来才被包含。这是一个小而有用的函数。
如果我们找到这些数字的部分或累积和(1,1 + 2,等等),我们就得到了三角数。为此,我们可以使用 std::partial_sum 函数来自 numeric 头文件:
std::partial_sum(numbers.begin(),numbers.end(),numbers.begin());
我们得到了我们想要的三角数(1, 3, 6, 10, 15,...210)。
清单 9.1 生成前几个三角形数字
#include <numeric>
#include <vector>
constexpr std::vector<int> make_triangle_numbers(int count)
{
std::vector<int> numbers(count); ?
std::iota(numbers.begin(), numbers.end(), 1); ?
std::partial_sum(numbers.begin(), numbers.end(), numbers.begin()); ?
return numbers;
}
? 默认初始化整数的容器
? 填充 1, 2,...
? 和 1, 1 + 2, 1 + 2 + 3,...
我们使用了一个较旧的 C++ 函数 std::partial_sum ,以及来自 numeric 头文件的较新 std::iota 函数。还有许多其他算法我们在本书中没有机会使用。请查看 algorithm 和 numeric 头文件,尝试一个你以前没有使用过的,或者自己实现一个。这是一个很好的练习方式。
9.1.1 使用算法测试我们的三角形数字
我们应该测试我们的三角数,并可以使用更多的算法来做到这一点。我们可以使用 adjacent_difference 来撤销 partial_sum ,这给出了容器中相邻元素之间的差异。如果我们为差异创建一个 vector ,我们可以将这些与 iota 创建的从 1 到 20 的整数进行比较,并且我们可以 assert 它们匹配。
清单 9.2 测试我们的三角数
#include <cassert>
void check_properties()
{
const int count = 20;
const auto triangle_numbers = make_triangle_numbers(count);
std::vector<int> diffs(count);
std::adjacent_difference(triangle_numbers.begin(),
triangle_numbers.end(),
diffs.begin()); ?
std::vector<int> numbers(count);
std::iota(numbers.begin(), numbers.end(), 1); ?
assert(numbers == diffs); ?
}
? 找出差异
? 与 1, 2,... 进行比较,
让我们花一点时间为我们的测试函数添加更多的 assert 。如果我们第二次找到 adjacent_difference ,我们应该得到一个 vector 的 1 。我们可以使用带有 lambda 的 all_of 算法来检查这一点:
#include <algorithm>
std::adjacent_difference(diffs.begin(), diffs.end(), diffs.begin());
assert(std::all_of(diffs.begin(), diffs.end(),
[](int x) { return x == 1; }));
我们可以计算 1 来检查我们是否有我们开始时的数字,使用 std::coun :
assert(std::count(diffs.begin(), diffs.end(), 1) == count);
我们有少量测试,并将很快添加另一个。在此之前,值得补充一些细节。大多数算法有多种重载。例如, std::count 有三个版本(请参见 https://en.cppreference.com/w/cpp/algorithm/count)。我们使用了第一个版本。第二个版本标记为 constexpr ,因此可以在编译时使用,第三个版本使用执行策略,允许算法的并行执行。
9.1.2 算法的执行策略
C++17 引入了几种执行类型策略,这些策略位于 execution 头文件中。默认情况下,使用 sequenced_policy , std::execution::seq ,这会导致算法按顺序操作,一次处理一个项目。我们还可以使用 std::execution:: par 或 std::execution::par_unseq 以及 C++20 的 std::execution::unseq 。后面三种允许并行执行,而无序策略可能导致执行以任何顺序发生。它们表明算法可以并行化,因此这是一种许可而不是要求。如果实现无法并行化,这些策略会回退到顺序策略,即使可以并行化,代码也可能变得更慢(请参见 Bartlomiej Filipek 的博客 http://mng.bz/JdGV)。并行版本为我们提供了一种简单的方法来指示工作可以分配给不同的线程,但并不能保证加速我们的代码。它们可能会加速,但在新线程上设置工作可能会有开销。
如果我们将 std::execution::par 添加为第一个参数,我们将使用并行执行的重载:
#include <execution>
assert(std::count(std::execution::par, diffs.begin(), diffs.end(), 1)
== count);
请求并行执行是简单明了的,可能会加速您的代码。进行实验并测量以查看会发生什么。线程和并行执行是一个大话题。安东尼·威廉姆斯的书《C++并发实战》(Manning Publications,2019;请参见 http://mng.bz/PR5n)是一个优秀的资源,您可以在互联网上找到他的许多演讲。
9.1.3 可变的 lambda 表达式
到目前为止,我们的测试是必要的,但不充分。三角数有一个封闭形式的公式,可以直接计算第 n 个数为
我们可以利用这个关系使我们的测试足够充分,至少对于前几个数字,通过检查每个值是否与方程的值匹配。
清单 9.3 检查每个值
for (size_t i=0; i< triangle_numbers.size(); ++i)
{
const int n = i + 1;
assert(triangle_numbers[i] == n*(n+1)/2);
}
我们已经看到,我们通常可以使用算法来代替 for 循环,并且因为我们想检查关系对所有数字都成立, std::all_of 将有效。然而,当我们切换到算法时,我们不再有变量 i 可以在计算中使用。我们可以在 lambda 的方括号中声明一个变量 [] 并将 lambda 标记为 mutable ,这允许我们递增该变量。没有 mutable 关键字,我们会得到一个编译器错误,告诉我们在不可变的 lambda 中无法修改按复制捕获的变量。
此外, mutable 允许 lambda 修改通过复制捕获的对象,并调用通过复制捕获的对象的非常量成员函数。使用 std::all_of 而不是列表 9.3 中的 for 循环与可变 lambda 给我们以下代码。
清单 9.4 使用可变 lambda 检查每个值
assert(std::all_of(triangle_numbers.begin(), triangle_numbers.end(),
[n = 0](int x) mutable ?
{ ?
++n; ?
return x == n * (n + 1) / 2;
}
));
? n 设置为 0 并且可变,因为 n 被递增
我们有三角数和一些测试。如果我们暂停一下,看看更多的属性,我们可以多练习一些算法。我们还将发现一个有用的属性,使三角数适合用于我们的老虎机。
9.1.4 三角数的更多属性
首先,让我们考虑三角数是奇数还是偶数。然后,我们将找到另一个可以用于我们的老虎机的模式。在调查过程中,我们还将多练习一些算法和 std::map 。前两个三角数,1 和 3,是奇数,然后我们得到两个偶数,6 和 10。这个模式会继续吗?如果我们转换我们的 vector ,用点( '.' )标记奇数,用星号( '*' )标记偶数,我们将会发现答案。
我们可以声明另一个 vector 来保存转换。我们在第 7 章中使用了 std::transform 算法,该算法来自 algorithm 头文件,将 std::string 中的字符转换为小写。虽然有多种重载,但每种都将一个函数应用于输入范围,并将结果存储在输出中。原始版本接受一对输入迭代器,起始和结束,一个输出迭代器,以及一个一元函数:一个接受一个输入的函数,比如我们的 lambda。C++20 引入了一个范围版本,它接受一个输入源,而不是一对迭代器,以及输出迭代器和一元函数。还有一个版本接受两个输入范围和一个二元函数来创建输出,以及一个接受执行策略的版本。
让我们写一个名为 demo_further_properties 的函数。我们将为每个数字使用一个字符,因此我们可以使用一个 vector 来存储结果:
std::vector<char> odd_or_even
我们可以为转换函数编写一个 lambda,接受一个 int 并返回适当的字符以指示一个数字的奇偶性:
[](int i) { return i%2? '.':'*'; }
如果 i%2 非零,我们得到一个奇数,因此我们返回 '.' ;否则,我们返回 '*' 。我们在转换中使用这个,使用 back_inserter 来根据需要扩展输出:
std::vector<char> odd_or_even;
std::ranges::transform(triangle_numbers,
std::back_inserter(odd_or_even),
[](int i) { return i%2? '.':'*'; });
我们可以使用基于范围的 for 循环来显示数字的奇偶性,但在第二章中,我们提到过可以使用 std::copy 或范围版本将容器的内容插入到流中。第一个参数是容器,或其 begin 和 end ,第二个是用流(在我们的例子中是 std::cout )和分隔符(比如空格)构造的 std::ostream_iterator 。一旦我们包含了 iostream 头文件,就可以在一行代码中输出奇数或偶数标记:
std::ranges::copy(odd_or_even, std::ostream_iterator<char>(std::cout, " "));
我们的进一步属性功能如下。
清单 9.5 检查数字是奇数还是偶数
#include <algorithm>
#include <iostream>
#include <iterator>
void demo_further_properties()
{
const int count = 20;
const auto triangle_numbers = make_triangle_numbers(count);
std::vector<char> odd_or_even; ?
std::ranges::transform(triangle_numbers,
std::back_inserter(odd_or_even),
[](int i) { return i % 2 ? '.' : '*'; }); ?
std::ranges::copy(odd_or_even,
std::ostream_iterator<char>(std::cout, " ")); ?
std::cout << '\n';
}
结果的向量
? Lambda 检查奇偶性
? 复制到 cout
如果我们从 main 调用这个并查看输出,我们会看到
. . * * . . * * . . * * . . * * . .
看起来我们确实会不断出现两个奇数后跟两个偶数。Stack Exchange 的数学网站解释了为什么会发生这种情况(请参见 http://mng.bz/1JBj)。
我们发现了一个有趣的模式。要构建一个老虎机,我们需要选择一些物品在某些转轴上显示。如果某些物品匹配,老虎机将支付奖金。三角数的最后数字有另一个模式。一些数字出现的频率比其他数字更高,因此我们可以使用三角数的最后数字作为我们的老虎机。出现频率较低的数字将提供更高的支付。通过在一个 std::map 中保持记录并计算 % 10 而不是 % 2 ,我们将看到每个数字出现的频率。我们需要将最后一个数字,即 int ,映射到一个计数,因此在包含 map 标题后,我们可以使用
std::map<int, size_t> last_digits;
在我们的 demo_further_properties 函数中。我们可以根据数字的可能性来确定老虎机的支付。我们将使用原始循环来找到每个三角形数的最后一个数字。我们需要使用 operator[] 查找数字 % 10 并递增我们获得的值。我们了解到 operator[] 将在第 7 章中插入一个键值对到映射中,如果键不存在,当我们构建答案击碎游戏时。对应的值是该值类型的默认值,在我们的例子中,是一个值为 0 的 size_t 。这就是我们需要的。我们按如下方式创建最后数字的统计:
for (int number: triangle_numbers)
{
++last_digits[number % 10];
}
我们可以流出计数,这样我们就知道哪些数字出现得最频繁:
for (const auto& [key, value] : last_digits)
{
std::cout << key << " : " << value << '\n';
}
将其引入函数中,我们得到以下内容。
清单 9.6 将数字的计数添加到其他属性
#include <map>
void demo_further_properties()
{
const int count = 20;
const auto triangle_numbers = make_triangle_numbers(count);
// ... as before
std::map<int, size_t> last_digits; ?
for (int number: triangle_numbers)
{
++last_digits[number % 10]; ?
}
std::cout <<
"Tallies of the final digits of the first 20 triangle numbers\n";
for (const auto& [key, value] : last_digits) ?
{ ?
std::cout << key << " : " << value << '\n'; ?
}
}
? 使用地图存储计数
? 计算最终数字
? 输出结果
从 main 调用此内容,我们看到
0 : 4
1 : 4
3 : 2
5 : 4
6 : 4
8 : 2
8 s 和 3 s 不太可能; 0 、 1 、 5 和 6 的可能性是两倍。实际上,最后的数字重复了这个模式。
13605186556815063100
一遍又一遍。如果我们选择任意三个三角形数字,我们不太可能得到三个 3 或 8 作为最后的数字,因此这样的结果可能在游戏中是一个大奖。
让我们用三个三角数构建一个老虎机。我们需要制作三个转轮,将数字随机排列。我们还希望显示转轮并让它们在每次旋转时旋转,以决定是否支付。
9.2 一个简单的老虎机
我们需要三卷数字来旋转。我们将显示当前行的数字,以及上面和下面行的数字。我们可以用 '-' 符号来指示当前行,如下所示:
28 91 153
- 45 120 45-
36 1 3
我们将从每次旋转转盘开始。如果最后两个数字匹配,我们将支付,如果三个都匹配,我们将支付更多。一旦我们有了一个可工作的游戏,我们将在第 9.3 节中扩展它,如果我们得到三个 3 或 8 ,将颁发一个大奖。
9.2.1 constexpr 和 std::format 的修订
清单 9.1 生成三角形数字作为一个 std::vector<int> 。如果我们利用在上一章中遇到的 using 语句,我们就不需要每次提到卷轴时都拼写 std:: vector<int> 。
using Reel = std::vector<int>;
这可以位于 main.cpp 文件的顶部。我们现在可以为我们的老虎机制作三个卷轴,每个卷轴包含 20 个数字,使用一个名为 make_reels 的新函数:
constexpr int numbers = 20;
constexpr size_t number_of_reels = 3u;
std::vector<Reel> reels(number_of_reels, make_triangle_numbers(numbers));
数字应该为游戏进行洗牌。我们可以直接在卷轴上使用 std::shuffle :
std::shuffle(reel.begin(),
reel.end(),std::mt19937(std::random_device{}()));
然而,我们知道测试具有随机行为的代码可能很困难。如果我们使用一个可调用函数的模板,而不是随机数生成器,我们可以在测试中替换掉生成器。可调用函数接受两个迭代器作为 vector 的 Reel ,因此我们使用
std::invocable<std::vector<Reel>::iterator,
std::vector<Reel>::iterator>
在模板头中使用关键字 typename 代替:
template<std::invocable<std::vector<Reel>::iterator,
std::vector<Reel>::iterator> T>
我们会逃脱惩罚
template<typename T>
但使用概念而不是原始类型名称意味着如果我们没有为 T 提供合适的类型,我们可能会获得更清晰的诊断。
我们需要包含 concepts 头文件,并且我们可以将该函数标记为 constexpr 。我们的 make_reels 函数如下所示。
清单 9.7 设置 reels
#include <concepts>
template<std::invocable<std::vector<Reel>::iterator,
std::vector<Reel>::iterator> T> ?
constexpr std::vector<Reel> make_reels(int numbers, ?
int number_of_reels, ?
T shuffle) ?
{
std::vector<Reel> reels(number_of_reels,
make_triangle_numbers(numbers)); ?
for (auto& reel : reels)
{
shuffle(reel.begin(), reel.end()); ?
}
return reels;
}
? 通过洗牌传递以允许测试
制作卷轴
? 旋转转轴
我们可以通过两种方式调用这段代码。为了在我们即将创建的游戏中使用这个函数,我们需要一个种子生成器。
std::random_device rd;
std::mt19937 gen{ rd() };
并在 lambda 中通过引用捕获此生成器:
auto shuffle = [&gen](auto begin, auto end)
{ std::shuffle(begin, end, gen); };
我们可以使用我们的 lambda 调用 make_reels :
std::vector<Reel> reels = make_reels(numbers, number_of_reels, shuffle);
此外,由于函数是 constexpr ,我们可以在我们在列表 9.2 中开始的 check_ properties 函数中使用 static_assert ,用一个无操作的 lambda 模拟随机行为:
constexpr auto no_op = [](auto begin, auto end) { };
static_assert(make_reels(1, 1, no_op).size() == 1);
这并没有测试太多,但表明了可能性。
配备三卷洗牌的卷轴,我们需要显示每个卷轴上的数字。我们将显示上一行、当前行和下一行,用 '-' 指示当前行。我们在第二章中使用了 std::format ,所以让我们再用一次进行练习。如果您的编译器不支持 std::format ,请回顾第二章以获取使用 fmt 库的说明。数字最多为三位数,因此我们将其右对齐,宽度为三个字符,并用空格填充。我们在冒号后放置格式说明符,使用 > 进行右对齐,使用 3 指定空格数,得到 {:>3} 。我们传入卷轴和流,以便测试我们的代码。
清单 9.8 显示 reels
#include <format>
void show_reels(std::ostream& s,
const std::vector<int>& left,
const std::vector<int>& middle,
const std::vector<int>& right)
{
s << std::format(" {:>3} {:>3} {:>3}\n",
left.back(), middle.back(), right.back()); ?
s << std::format("-{:>3} {:>3} {:>3}-\n",
left[0], middle[0], right[0]); ?
s << std::format(" {:>3} {:>3} {:>3}\n",
left[1], middle[1], right[1]); ?
}
? 上一行
? 当前行用 - 表示
? 下一行
我们已经设置好了转盘,现在可以显示它们。为了制作一个游戏,我们需要决定当前的行是否值得某种支付,然后我们需要旋转转盘。我们还想要一种停止游戏的方法。我们可以像之前一样使用 getline :
std::string response;
std::getline(std::cin, response);
如果 response 不是按下回车键,我们将退出。让我们先旋转转盘,然后再构建游戏。
9.2.2 使用 std::rotate 旋转转盘
algorithm 头文件提供了一个 std::rotate 函数,我们可以用来进行旋转。该函数对元素执行左旋转。给定一些元素
std::vector v{1, 2, 3, 4, 5}
我们可以将它们可视化为一个卷轴,如图 9.2 所示。
我们可以通过指定开始、中间(例如,数字 4,即从开始数起的第三个)和结束来对元素进行左旋转:
std::rotate(v.begin(), v.begin() + 3, v.end());
使用 v.begin() + 3 作为中间,将数字 4 移动到开头,之前的元素移动到末尾,因此我们得到
4, 5, 1, 2, 3
就像数字的卷轴已经旋转一样。按照卷轴的方式排列,这些数字将向左旋转,如图 9.3 所示。
最初,我们有 1、2、3、4 和 5。我们选择了 begin + 3 的中间,将 4 移到前面。1 现在位于 begin + 2 ,所以我们可以再次旋转,使用 1 的位置。
std::rotate(v.begin(), v.begin() + 2, v.end());
元素最终回到它们开始的地方。
我们想要随机旋转老虎机的转盘,改变所用的中间值。参数是迭代器,因此我们可以在转盘的开头添加一个随机数,以选择使用哪个中间值。我们有一个随机数生成器,用于初始洗牌。我们现在也需要一个分布。我们希望转盘上的每个数字都有可能出现,但也希望转盘能够移动,因此我们需要从第二个元素运行到最后一个元素。我们可以使用从 1 到包括转盘大小 ? 1 的分布来生成一个偏移量,添加到 begin :
std::uniform_int_distribution dist(1, numbers - 1);
如果我们允许 0 ,卷轴将不会移动。然后我们可以旋转所有三个卷轴:
for (auto& reel : reels)
{
std::rotate(reel.begin(), reel.begin() + dist(gen), reel.end());
}
我们将在下一节的简单老虎机功能中直接使用这个。
rotate 函数在 C++ 中已经存在很长时间了。如果我们查看 CppReference ( http://mng.bz/27E0),我们会注意到一个接受执行策略的版本,这是在 C++17 中引入的,还有一个 constexpr 版本,这是在 C++20 中引入的,以及一个与范围版本的链接。我们现在已经习惯了这些新特性,并且在查找算法时会经常看到它们。我们还需要一个函数来计算支付。然后我们就可以创建我们的游戏。
9.2.3 简单老虎机
为了决定支付金额,我们需要检查最后的数字是否有匹配。三个匹配的比两个匹配的更值得奖励,而没有匹配的则一无所获,因此目前我们将为三个匹配奖励 2,为两个匹配奖励 1。
清单 9.9 计算支付金额
int calculate_payout(int left, int middle, int right)
{
int payout = 0;
if (left == middle && middle == right) ?
{
payout = 2;
}
else if (left == middle
|| middle == right
|| left == right) ?
{
payout = 1;
}
return payout;
}
? 三场比赛
? 两场比赛
现在,如果我们想为 3 或 8 提供更高的支付,这些情况发生的可能性较小,我们就有可能陷入 if 和 else 的混乱中。我们稍后会在为我们的游戏添加更多功能时重新审视这个问题。现在,我们已经拥有制作简单老虎机所需的所有部分。
我们设置转轴,显示数字,并在一条线获胜时发放奖金。玩家可以按回车键继续,或按其他任意键退出。如果他们继续,我们将旋转转轴并再次显示数字。
清单 9.10 一个简单的老虎机
#include <iostream>
#include <random>
#include <string>
#include <vector>
void triangle_machine_spins_only()
{
constexpr int numbers = 20;
constexpr size_t number_of_reels = 3u;
std::random_device rd;
std::mt19937 gen{ rd() };
auto shuffle = [&gen](auto begin, auto end)
{ std::shuffle(begin, end, gen); };
std::vector<Reel> reels = make_reels(numbers,
number_of_reels,
shuffle); ?
std::uniform_int_distribution dist(1, numbers - 1); ?
int credit = 1; ?
while (true)
{
show_reels(std::cout, reels[0], reels[1], reels[2]);
const int payout = calculate_payout(reels[0][0]%10,
reels[1][0]%10,
reels[2][0]%10);
--credit;
credit += payout;
std::cout << "won " << payout
<< " credit = " << credit << '\n';
std::string response; ?
std::getline(std::cin, response); ?
if (response != "") ?
{ ?
break; ?
}
for (auto& reel : reels) ?
{
std::rotate(reel.begin(),
reel.begin() + dist(gen), ?
reel.end());
}
}
}
? 设置
随机整数以旋转卷轴
? 跟踪信用
? 允许玩家退出
? 旋转卷轴
随机整数以旋转卷轴
如果我们从 main 调用这个,我们就可以玩我们的游戏。我们可能不会经常获胜,所以请注意我们的信用在流失。一个典型的输出可能看起来像这样:
15 1 36
-136 78 91-
6 3 15
won 0 credit = 0
210 3 45
- 45 6 66-
10 153 105
won 1 credit = 0
36 210 171
- 1 171 153-
15 190 28
won 1 credit = 0
3 1 190
-210 78 6-
45 3 171
won 0 credit = -1
78 78 171
- 66 3 153-
21 6 28
won 1 credit = -1
支付并不是很公平,因为两个或三个匹配的最后数字并不太可能。我们可以提供更公平的支付。如果我们还允许一个转轴被保持或轻推,我们就有更大的获胜机会。我们可以使用更多新的 C++特性,包括 std::visit ,来实现这一点。让我们打造一个更好的老虎机。
9.3 更好的老虎机
我们将进行两项更改。首先,我们将改善支付,然后我们将允许保持或轻推。让我们先处理支付。支付基于左、中、右数字的最后一位数字。我们知道在前 20 个三角形数字中, 3 或 8 只出现两次,因此每个出现的概率为 1/10。因此,获得三个 3 的概率为 1/10×1/10×1/10 = 1/1000,获得三个 8 的概率也是如此。其他数字的出现概率更高。这次我们还将每局收费两个积分。在没有进行全面分析的情况下,我们将为三个 3 或 8 提供 250 积分,为其他任何三个匹配数字提供 15 积分。两个匹配数字的出现概率更高,因此我们将为两个 3 或两个 8 提供 15 积分,而其他的仅提供 1 积分。
9.3.1 参数包和折叠表达式
当我们之前计算支付时,我们没有使用加权,并注意到如果我们添加更多条件,我们可能需要几个 if 和 else 。让我们换个方法。如果我们找到最终数字的频率,我们可以选择最频繁的数字来计算支付。我们使用三个转轴,因此我们希望有一个函数接受三个数字并返回一个从数字到频率的 map :
#include <map>
std::map<int, size_t> counter = frequencies(left, middle, right);
与其编写一个接受三个数字的函数,我们可以做一些更通用的事情。我们使用了 STL 中的几个类,接受不同数量的参数,包括一个 variant 。在第五章中,我们提到了它的定义:
template <class... Types>
class variant;
对于一个 variant ,我们使用一个类型。我们也可以使用非类型模板参数。例如,我们在第 4 章中遇到了 std::ratio ,使用 int 来形成诸如 std::ratio<3, 6> 的分数。我们接受 variant 中的三个点或省略号表示一个参数包,允许我们声明任意数量的类型。我们可以在函数模板中使用参数包,也可以在类中使用,并且也可以使用非类型模板参数包。我们可以使用带有非类型模板参数包的函数来查找频率。我们需要解包参数以找到频率。
一般来说,变参模板是具有至少一个参数包的模板。这些在 C++11 中引入,但随着语言的发展,使用起来变得更加简单。在 C++11 中,我们需要使用递归来展开参数,使用一个项,然后再次调用函数,传入剩余的项。C++17 引入了折叠表达式(参见 https://en.cppreference.com/w/cpp/language/fold),避免了递归的需要。
让我们尝试一个例子。我们可以写一个折叠表达式来对一个或多个项目求和。之后,我们将能够使用可变参数模板来找到我们想要的改进老虎机支付的频率。我们需要在三个地方注意参数包。首先,我们说 typename... Ts 来表示零个或多个参数:
template <typename... Ts>
在这里使用 Ts 而不是 T 来引起对可能存在多个 Ts 的注意是很常见的。我们可以自由使用任何我们想要的名称。我们可以使用 class 或 typename ,后面跟着省略号,然后是我们的名称 Ts 。接下来,函数的参数是类型为 Ts... 的 tail 。注意,省略号现在已切换到出现在 Ts 之后。最后,在实现中,我们再次使用三个点与 operator+ 结合来求和。返回类型取决于参数,因此我们可以使用 auto ,编译器会为我们处理。
清单 9.11 折叠示例
template<typename... Ts> ?
auto add(const Ts&... tail) ?
{
return (... + tail); ?
}
模板头中的点
? 函数签名中的点
? 解包函数中的点
... + 解包 tail ,称为折叠表达式。这样的表达式告诉编译器对可变参数包中的每个元素重复运算符。我们也可以使用 operator- ,或者任何适用于参数的其他运算符。我们还可以使用解包。
return (tail + ...);
我们可以检查几个数字的值:
assert(6==add(1, 2, 3));
参数 1 、 2 和 3 通过 ... + tail 解包为左结合表达式:
((1 + 2) + 3)
如果我们将点放在右侧,我们将得到右结合的表达式:
(1 + (2 + 3))
对于数字的加法,侧面没有区别。减法则很重要,因为
((1 - 2) - 3) = -1 -3 = -4
而在
(1 - (2 - 3)) = 1 - (-1) = 2
我们还可以使用一个 single 号码:
assert(1 == add(1));
我们的函数在没有数字的情况下无法编译。如果我们尝试
assert(0 == add());
我们被告知,关于 + 的一元折叠表达式必须具有非空扩展。一元折叠具有包和运算符,或者是右折叠。
tail operator ...
或左折:
... operator tail
一元折叠不适用于空包。我们可以改用二元折叠,提供一个初始值 init ,作为右折叠,初始值在右侧。
tail operator ... operator init
或左折叠,初始值在左侧:
init operator ... operator tail
我们可以将返回语句改为使用二进制折叠,提供初始值 0 :
return (0 + ... + tail);
我们需要能够将尾部的值添加到 0 。
坚持使用一元折叠,我们还可以添加其他支持 operator+ 的类型;例如,一些字符串:
using namespace std::literals;
assert(add("Hello"s, "again"s, "world"s)=="Helloagainworld"s);
请注意,如果不使用概念来约束模板,如果类型没有适当的 operator+ ,我们将会得到很多编译器错误。此外,我们现在有三个 add 的实例化,因为我们有三次调用,一次使用一个 int ,一次使用三个 int :
auto add<int>(int)
auto add<int, int, int>(int, int, int)
和一个使用三个字符串作为参数包:
auto add<std::string, std::string, std::string>(std::string, std:: string, std:: string)
折叠表达式非常强大,我们仅仅触及了表面。有关更多示例,请参见 https://www.foonathan.net/2020/05/fold-tricks/。
9.3.2 使用参数包查找频率
回到我们的游戏。让我们编写一个函数来查找支付的数字频率,以便找出当前行中出现最频繁的数字。我们在第 9.1.2 节中使用了一个 std::map<int, size_t> 来查找每个最后数字在三角数中出现的频率。我们现在可以使用另一个可变参数模板做类似的事情。我们将不再在新函数中计算最后数字,而是编写一个通用的频率函数。我们的游戏将传入最后数字,就像我们在列表 9.9 中调用之前的 calculate_payout 函数时所做的那样。
我们想要一个可以接受可变数量数字的函数。我们只会用三个数字来调用它,但可以为了练习编写一个通用函数。对于可变参数模板,我们注意到在 typename 后面放了三个点,然后在函数签名中的参数前面放了点:
template<typename... Ts>
std::map<int, size_t> frequencies(Ts... numbers)
我们可以根据需要调用该函数,传入任意数量的数字,这意味着如果我们愿意,可以将我们的机器推广到超过三个卷轴。请记住,我们也可以使用 auto 而不是模板头:
std::map<int, size_t> frequencies(auto... numbers)
在我们实现这个功能之前,我们应该确保数字实际上是数字,使用一个概念。我们没有对 add 这样做,因此我们可以集中在点上,但注意到如果没有概念,我们可能会遇到很多编译错误。我们正在进行计数,所以我们希望有一个整数或可以转换为整数的东西来计数,而 std::convertible_to<int> 正好满足我们的需求。我们在 auto 之前添加了以下要求:
#include <concepts>
std::map<int, size_t> frequencies(std::convertible_to<int> auto... numbers)
现在我们可以实现这个函数。我们有一些数字,或者至少是可以使用 static_cast<int> 转换为 int 的元素。我们在上一节中使用了带点的运算符来解包参数。我们还可以将参数解包到初始化列表中:
{ static_cast<int>(numbers)... }
我们可以在基于范围的 for 循环中使用初始化列表来填充 map 的频率。
清单 9.12 使用参数包查找频率
#include <map>
std::map<int, size_t> frequencies(std::convertible_to<int> auto... numbers)
{
std::map<int, size_t> counter{};
for (int i : { static_cast<int>(numbers)... }) ?
{
counter[i]++; ?
}
return counter;
}
? 将参数解包为初始化列表
? 记录计数
我们可以使用频率函数来处理不同数量的数字:
auto tally_of_3 = frequencies(1, 3, 5);
auto tally_of_4 = frequencies(1, 3, 5, 999);
我们获得了一张地图,显示每个数字出现的频率。
我们的老虎机将发送左、中、右三个数字,就像我们在列表 9.9 中计算支付时所做的那样。我们将编写一个新函数来计算更公平的支付,该函数将像以前一样从每个转盘中获取最后一个数字:
int calculate_payout(int left, int middle, int right)
我们可以计算每个数字在当前行中出现的频率
std::map<int, size_t> counter = frequencies(left, middle, right);
并使用这些计数来决定支付金额。我们可以根据每个结果的可能性给出更公平的支付,而不是我们之前对三场比赛的 2 和对两场比赛的 1 的做法。
9.3.3 更公平的支付
我们有三个卷轴,因此最终数字出现一次、两次或三次。如果我们找到出现频率最高的数字,就可以用它来决定支付。算法头定义了 std::max_element ,它使用 operator< 按默认顺序查找范围内的最大元素。我们的频率包含键值对,我们想要值最大的元素。键是对的第一个元素,值是第二个元素,因此我们在 lambda 中使用第二个元素进行比较:
auto it = std::max_element(counter.begin(), counter.end(),
[](auto it1, auto it2) { return it1.second < it2.second; });
只要计数器不为空,我们就可以获取一个元素的迭代器并给予适当的奖励。我们现在将每次收费 2 个积分。正如我们所指出的, 3 和 8 的出现概率较低。头奖是三个匹配的 3 或 8 的最后数字,因此我们给予 250 个积分。三个其他匹配的最后数字获得 15 个积分。两个 3 或 8 可以获得 10 个积分,任何其他匹配的对获得 1 个积分。如果最后一个数字是 3 或 8 ,我们可以使用一个 std::array ,并在与频率相对应的索引处给予正确的奖励:
constexpr std::array value = {0, 0, 10, 250};
零或一给出 0 ,而二给出 10 的积分,三给出 250 的大奖。同样,对于更可能的数字,我们可以使用
constexpr std::array value = {0, 0, 1, 15};
支付 1 或 15 的赔付。
清单 9.13 更公平的支付
#include <array>
int calculate_payout(int left, int middle, int right)
{
std::map<int, size_t> counter = frequencies(left,
middle,
right);
auto it = std::max_element(counter.begin(),
counter.end(),
[](auto it1, auto it2) {
return it1.second < it2.second;
});
if (it != counter.end())
{
int digit = it->first;
size_t count = it->second;
if (digit == 8 || digit == 3)
{
constexpr std::array value = { 0, 0, 10, 250 };
return value[count];
}
else
{
constexpr std::array value = { 0, 0, 1, 15 };
return value[count];
}
}
return 0;
}
我们现在有了更好的支付功能,并且学到了更多的 C++。如果我们在旋转中添加保持和轻推功能,我们将拥有一个更好的游戏,并可以使用另一个新的 C++特性。
9.3.4 允许保持、轻推或旋转
我们最初的游戏只提供旋转。在我们改进的游戏中,我们将做两件事。如果玩家获胜,他们可以选择退出或让转盘在下一轮继续旋转。否则,他们每个转盘有三个选项。在简单老虎机的输出中,第一次旋转给出了
210 3 45
- 45 6 66-
10 153 105
won 1 credit = 0
如果我们被允许进行 45 ,旋转中间的转盘,并轻推右侧的转盘使 105 上升,我们将会有两个数字以 5 结尾,因此我们将赢得一些积分。例如,我们可能会得到
210 210 66
- 45 171 105-
10 190 15
中间的卷轴旋转,所以它可以是任何东西,但我们必须在左侧有 45 ,在右侧有 105 ,至少给出两个最后匹配的数字。
我们可以使用空的 struct 来指示如何移动每个卷轴,并将其中一个保留在 variant 中。我们之前使用过 variant ,因此一些额外的练习是有用的。我们包含 variant 头部,并使用 using 指令命名我们的 variant 。它可以是三个空结构之一。
清单 9.14 允许更多选项
#include <variant>
struct Hold {};
struct Nudge {};
struct Spin {};
using options = std::variant<Hold, Nudge, Spin>;
如果玩家上次获胜,他们可以选择退出或按回车键旋转所有三个转盘。我们可以用 vector 的 options 来表示这一点:
std::vector<options>{Spin{}, Spin{}, Spin{}}
我们可以像在清单 9.10 中的简单老虎机中那样使用 std::getline 来填充 std::string :
std::string response;
std::getline(std::cin, response);
如果响应是 Enter,我们将得到一个空字符串,然后游戏应该旋转所有三个转盘。我们可以将解析放在一个函数中。一个 optional 是合适的返回类型。我们还可以将函数标记为 constexpr ,允许我们在 static_assert 中使用它。
清单 9.15 三次旋转以进入
#include <optional>
#include <string>
#include <vector>
constexpr std::optional<std::vector<options>>
parse_enter(const std::string& response)
{
if(response.empty()) ?
{ ?
return std::vector<options>{ ?
Spin{}, ?
Spin{}, ?
Spin{}}; ?
}
else
{
return {}; ?
}
}
? 按下进入键,因此返回三次旋转
? 其他东西被按下,因此返回空的可选值
我们应该检查玩家是否真的想要退出,如果他们输入了什么。我们会询问,给他们一个按回车键继续游戏的机会。
清单 9.16 检查是否按下 Enter 键
std::optional<std::vector<options>> get_enter()
{
std::cout << "Enter to play\n";
std::string response;
std::getline(std::cin, response);
auto got = parse_enter(response); ?
if (!got) ?
{ ?
std::cout << "Are you sure you want to quit? " ?
"Press Enter to keep playing\n"; ?
std::getline(std::cin, response); ?
got = parse_enter(response); ?
}
return got;
}
? 三次旋转以进入
? 检查玩家是否真的想要退出
如果玩家没有获胜,他们可以保持、轻推或旋转每个转盘。我们可以像之前一样获取响应,并逐个检查字符,以查看玩家想对每个转盘做什么。我们可以分别使用 'h' 、 'n' 或 's' 来表示保持、轻推或旋转。按下回车键可以表示旋转所有三个,就像在获胜后那样。其他任何操作都表示玩家希望停止。首先,我们想将一个字符映射到我们的结构体之一,因此我们使用 constexpr 函数并返回 optional 。
清单 9.17 将字符映射到动作
#include <optional>
constexpr std::optional<options> map_input(char c)
{
switch (c)
{
case 'h':
return Hold{};
break;
case 'n':
return Nudge{};
break;
case 's':
return Spin{};
break;
}
return {};
}
我们决定接受 Enter 键进行三次旋转,以节省玩家的一些按键。我们映射每个字母,将相应的选项放入 vector 中。再次,我们使用 constexpr 并返回 optional 。
清单 9.18 检查保持、推动或旋转
constexpr std::optional<std::vector<options>>
parse_input(const std::string & response)
{
std::vector<options> choice;
for (char c : response)
{
auto first = map_input(c);
if (first)
{
choice.push_back(first.value());
}
else
{
return {};
}
}
return choice.empty() ?
std::vector<options>{Spin{}, Spin{}, Spin{}} : choice;
}
我们现在可以使用我们的解析函数检查玩家的选项,如果他们在最后一次没有获胜。如果输入无效,或者为空或过长,我们将检查他们是否想要退出。
清单 9.19 检查选项
std::optional<std::vector<options>> get_input(size_t expected_length)
{
std::cout << "Hold (h), spin(s), nudge(n) or Enter for spins\n";
std::string response;
std::getline(std::cin, response;
auto got = parse_input(response); ?
if (!got || response.length()>expected_length) ?
{ ?
std::cout << "Are you sure you want to quit?\n"; ?
std::getline(std::cin, response); ?
got = parse_input(response); ?
}
return got;
}
解析输入
? 检查他们是否想要退出
在我们列表 9.10 中的原始游戏中,我们检查了主游戏中的响应,以决定是旋转还是退出。这一次,我们将根据玩家是否获胜来调用相应的函数:
std::optional<std::vector<options>> choice = won ?
get_enter() : get_input();
我们现在需要适当地移动转盘。之前,我们使用 std::rotate 来旋转所有三个转盘。我们现在需要根据玩家的选择采取适当的行动。使用 variant 来处理 options 使我们能够使用另一个有用的 C++特性,这是一种幸运的选择。
9.3.5 使用 std::visit 和 std::views::zip 的旋转卷轴
我们在第 5 章中使用了一个 std::variant ,当我们想要向我们的牌组中添加小丑时,我们还使用了 std::holds_alternative 来检测小丑。我们现在有三种可能的类型之一。 variant 头文件包含一个名为 std::visit 的方法,允许我们提供一个可调用对象,该对象接受变体中的每种可能类型(请参见 http://mng.bz/RmoK)。我们可以基于 std::holds_alternative 自己构建一些东西,使用大量的 if 和 else ,但很容易忘记为变体中的某种类型添加分支。使用 std::visit 意味着如果我们遗漏了一个替代项,就会出现编译错误。该函数将一个可调用对象应用于一个或多个变体:
template <class R, class Visitor, class... Variants>
constexpr R visit( Visitor&& vis, Variants&&... vars );
返回值 R 可以是 void 。变体 vars 是参数包中的一个或多个变体。访问者 vis 是可以使用变体中的类型调用的任何可调用对象。可调用对象可以是 struct ,每种类型都有一个重载的 operator() 。
清单 9.20 提供 std::visit 的可调用对象的一种方法
struct RollMethod
{
void operator()(Hold)
void operator()(Nudge)
void operator()(Spin)
};
考虑到玩家的选项 opt ,我们可以调用
std::visit(RollMethod{}, opt);
并且适当的 operator() 将被调用。这比构建一个长函数来检查 std::holds_alternative 更简洁,如果我们忘记某个类型的重载,编译器会报错。
我们还可以结合另一个可变参数模板使用 lambda 以获得更多练习。Lambda 是可调用的,因此具有一个 operator() 。通过创建一个派生自 lambda 的类模板,我们可以通过 using 语句暴露该 lambda 的 operator() 。
清单 9.21 在类中引入 operator() 的作用域
template <typename T>
struct Overload : T { ?
using T::operator(); ?
};
? 源自 T 并将 operator() 引入作用域
在 C++17 及之前的 Clang v17 版本中,我们需要提供一个模板推导指南,告诉编译器如何推导模板参数。该指南展示了如何将一组构造函数参数解释为类的模板参数,因此对于我们的类型 T ,我们希望一个 Overload(T) 来推导 Overload<T> 。因此,我们写道
template<typename T>
Overload(T) -> Overload<T>;
自 C++20 起,我们不再需要额外的推导指南。 struct 允许我们使用 lambda 创建一个 Overload 并调用该 lambda。例如,我们可以向 check_properties 函数添加一个 assert :
auto overload = Overload{ []() { return 0; } };
assert(overload() == 0);
单一类型的重载本身并没有太大用处,因为我们只有一个函数。直接使用 lambda 更简单,但我们可以使用参数包将多个 lambda 组合在一起。这将使每个 lambda 的 operator() 进入作用域。同样,我们可能需要一个推导指南,正如我们之前提到的,我们必须在三个地方考虑参数包的省略号。
清单 9.22 Overload 模式
template <typename... Ts> ?
struct Overload : Ts... { ?
using Ts::operator()...; ?
};
template<typename... Ts>
Overload(Ts...) -> Overload<Ts...>; ?
模板头中的点
? 结构体基中的点
? 解包点以使用每个运算符()
? C++17(以及 v17 之前的 Clang)的推导指南
我们可以创建一个滚动方法,为每个卷轴执行正确的操作,使用列表 9.22 中的 Overload 和三个 lambda。保持不变,轻推将卷轴移动一个位置。旋转,如之前一样,按随机量旋转,由函数 random_fn 提供。轻推和旋转都需要通过引用捕获使用的卷轴。
清单 9.23 一个保持、轻推或旋转 Overload
auto RollMethod = Overload{
[](Hold) {
},
[&reel](Nudge) {
std::rotate(reel.begin(),
reel.begin() + 1,
reel.end());
},
[&reel, &random_fn](Spin) {
std::rotate(reel.begin(),
reel.begin() + random_fn(),
reel.end());
},
};
现在 std::visit 可以使用 RollMethod. 中的适当功能
清单 9.24 移动卷轴
template<typename T>
void move_reel(std::vector<int>& reel, options opt, T random_fn)
{
auto RollMethod = Overload{
[](Hold) {
},
[&reel](Nudge) {
std::rotate(reel.begin(),
reel.begin() + 1,
reel.end());
},
[&reel, &random_fn](Spin) {
std::rotate(reel.begin(),
reel.begin() + random_fn(),
reel.end());
},
};
std::visit(RollMethod, opt);
}
我们现在可以使用玩家的选项移动特定的卷轴。我们有三个卷轴,因此我们想将玩家的选择与卷轴配对。我们有一个 vector 的卷轴和另一个 vector 的 options 。我们可以在 for 循环中使用索引,但我们可以使用最后一个新特性,范围的 zip 视图。 std::views::zip 是在 C++23 中引入的,因此某些编译器尚不支持,但您可以使用 Range-v3 库(请参见 https://ericniebler.github.io/range-v3/)或 for 循环:
for (size_t i = 0; i < reels.size(); ++i)
{
move_reel(reels[i], choice.value()[i], random_fn);
}
我们在第二章首次使用范围时遇到了范围的视图 std::view 。我们使用 drop_while 和 filter 来查看单个集合。在包含 ranges 头部后,我们可以使用 zip 来汇总这两个 vectors 。
std::views::zip(reels, choice.value())
zip 视图为我们提供了来自每个 vector 的项元组,而无需进行复制。如果我们将两个容器进行 zip 并迭代,元组会在两个向量之间移动,为我们提供来自每个向量的一个项。这些向量并没有连接,而是迭代器在每个输入集合上移动,如图 9.4 所示。
我们可以压缩两个以上的集合,如果我们想的话。迭代卷轴和选择的压缩视图会给我们一个包含两个引用的元组,我们可以在循环中使用它来移动卷轴。我们可以使用结构化绑定来命名元组中的两个项目,并适当地移动卷轴,使用列表 9.24 中的 move_reel 方法:
for (auto [reel, option] : std::views::zip(reels, choice.value()))
{
move_reel(reel, option, random_fn);
}
将这一切整合在一起就是我们的最终游戏。
清单 9.25 改进的三角形数字机器
void triangle_machine()
{
constexpr int numbers = 20; ?
constexpr size_t number_of_reels = 3u; ?
std::random_device rd; ?
std::mt19937 gen{ rd() }; ?
auto shuffle = [&gen](auto begin, auto end) { ?
std::shuffle(begin, end, gen); ?
}; ?
std::vector<Reel> reels = make_reels(numbers, ?
number_of_reels, ?
shuffle); ?
std::uniform_int_distribution dist(1, numbers - 1);
auto random_fn = [&gen, &dist]() { return dist(gen); };
int credit = 2;
while (true)
{
show_reels(std::cout, reels[0], reels[1], reels[2]); ?
const int won = calculate_payout(reels[0][0] % 10,
reels[1][0] % 10,
reels[2][0] % 10); ?
credit -= 2; ?
credit += won;
std::cout << "won " << won << " credit = " << credit << '\n';
std::optional<std::vector<options>> choice = won ?
get_enter() : get_input(number_of_reels); ?
if (!choice) ?
{ ?
break; ?
} ?
for (auto [reel, option] : ?
std::views::zip(reels, choice.value())) ?
{
move_reel(reel, option, random_fn); ?
}
}
}
? 如前所述设置
? 显示卷轴如之前所示
改进的支付方式
? 对于这款游戏收费更高
? 进入以赢得胜利;否则保持、轻推或旋转
? 适当地移动卷轴
我们称我们的新游戏为 main ,并且有更大的机会获得一些积分。一个示例游戏可能以没有匹配行开始:
28 21 171
-105 3 36-
153 136 45
won 0 credit = 0
Hold (h), spin(s), nudge(n) or Enter for spins
如果我们保持 105 ,旋转中间的卷轴,并轻推最后的卷轴以移动 45 ,我们将至少有两个匹配的最后数字,因此我们至少赢得 1 个积分,尽管我们必须为这一轮支付 2 个积分:
hsn
28 36 36
-105 120 45-
153 45 1
won 1 credit = -1
Enter to play
我们接下来必须让所有卷轴旋转,因为我们刚刚赢得了一些东西:
66 136 153
- 1 66 10-
6 105 21
won 0 credit = -3
Hold (h), spin(s), nudge(n) or Enter for spins
我们的信用下降,但我们仍然可以保持、旋转和推动:
hsn
66 21 10
- 1 3 21-
6 136 91
won 1 credit = -4
Enter to play
我们赢的机会更大,因此游戏更具吸引力。我们还学到了更多的 C++。
我们没有涵盖 C++ 中的每一个新特性,随着语言的不断发展,总会有更多的内容需要学习。从一个 vector 开始,找到一些小型游戏和项目给了我们很多练习。我们现在处于一个良好的状态,可以保持我们的技能更新。使用 CppReference,并通过添加你发现的缺失示例来帮助他人。尝试使用 Compiler Explorer 和 C++ Insights。关注 ISOCpp 网站以获取最新的新闻、文章和播客。继续学习和练习,最重要的是,享受乐趣!
摘要
- 使用 std::iota 填充一个容器,填入从选择的值开始的递增值。
- 许多算法支持执行策略,提供了一种简单的方法来请求并行化。这是一个请求,可能无法实现,在这种情况下,执行将回退到顺序策略。
- 我们可以将一个 lambda 标记为 mutable ,以允许它修改由 copy 捕获的对象并调用它们的非 const 成员函数。
- 使用概念而不是模板中的原始类型名称意味着如果在使用模板时未提供合适的类型,我们可能会获得更清晰的诊断信息。
- 我们可以将 constexpr 用于几乎任何事情,包括 std::vector 和 std::string ,自 C++20 起。评估可能在编译时发生,但不需要。我们可以使用 static_assert 测试 constexpr 代码。
- 变参模板是一个至少包含一个参数包的模板,用省略号表示。
- 我们再次使用省略号来展开参数包。我们在清单 9.11 中使用了 (... + tail) 来展开尾部,我们还可以将参数包放入初始化列表中,这在清单 9.12 中使用了 { static_cast<int>(numbers)... } 。
- 我们可以使用 std::visit 来调用 std::variant 的一个函数,这确保我们对任何可能持有的类型都有一个适当的重载。
- 使用 std::visit 的一种方法是 Overload 模式,它使用参数包将 operator() 引入作用域,使我们能够为 std::variant 中的每种类型打包 lambda。
- 最后,我们使用 std::views::zip 来自 ranges 库来配对两个集合。我们可以压缩多个集合,然后可以遍历视图中的元素元组。
- 继续学习和练习,最重要的是,享受乐趣!
- 上一篇: c++信奥算法学习-二叉树中序+后序推出层序
- 下一篇: c++编程实战入门:新鸡兔同笼
猜你喜欢
- 2025-01-15 「C语言编程」如何整蛊你的损友,让他的电脑一直关机?
- 2025-01-15 C++中int型和char型一起运算结果是什么?在编程中有何用处?
- 2025-01-15 知识分享:C语言语法总结,初学者可收藏
- 2025-01-15 「初识C语言」C语言保留字(关键字)详解
- 2025-01-15 C++代码解析8
- 2025-01-15 C++网络编程:TCP并发通信、I/O多路复用(转接)技术
- 2025-01-15 数学表达式计算器
- 2025-01-15 C/C++程序的断点调试
- 2025-01-15 C++ 编程入门指南:开启代码世界的奇妙之旅?
- 2025-01-15 c++编程实战入门:新鸡兔同笼
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)