有一些类型虽然不是基本类型,但是和基本类型一样常用,都是用来替代相同功能的C版本特性的,比如std::unique_ptr<>, std::shared_ptr<>, std::string, std::array<>, std::vector<>,分别用来替代raw pointers, const char* strings, low-level dynamic memory
本章讲的主要也是一些小特性,不过不是用来替代什么类型,是为了增加代码可读性、提高编码效率设计的
其实还有两个,variant用来替代联合体union,any用来替代void*指针,但是这两者使用频率较低,用到的时候再学也可以
Working with Optional Values
看这个函数原型,我们会猜测这个函数是用来在字符串中寻找某字符出现的最后位置的函数,但是如果我们没有在s中找到需要的字符呢?或者我们不需要起始位置想要搜索整个字符串s呢?
有一种方案是我们可以规定一些特殊数字用来返回没有结果的情况,比如数组和字符串经常用-1表示没有找到某元素,-1用来表示搜索整个字符串或范围等等;还有的只要是负数都可以
实际上string也正是这么做的,标准库的find方法返回的就是std::string::npos这个特殊数字
但是我们不可能去记住所有用来代表特殊参数或特殊输出的标记,有的用0有的用负数或者其他,所以一般像start_index这种可选参数,我们都会给一个默认值
回顾之前看到的find方法,起始位置都是有一个默认参数0表示搜索的范围是整个字符串
但是这个方法不适用于返回值,而且有的类型也不像int这种比较好默认处理
像这个想要读取某些配置文件的函数
我们只看原型是无法假设当配置没有读取到的返回值是0?-1?还是其他数字,所以这种情况下一般处理方法有两种
第一种如果没有找到就返回default;第二种没有找到就返回false,找到了通过output输出结果
虽然这些方法也可行,但是C++提供了一个新的类型来处理类似情景:optional<>
虽然这里也可以使用,但是在类里面定义相关类型应该更能体现这个类型的作用
std::optional
C++17引入了这个类型,用来表示可选的输入输出值,使用optional之后函数原型就变成这样了:
这样更简洁,一目了然
optional<>是个类模板,和vector、array一样,下面通过一个例子看看这个类型如何使用:
# include <optional>; // std::optional<> is defined in the <optional> module
# include <iostream>;
# include <string>;std::optional<size_t> find_last(const std::string& string, char to_find,std::optional<size_t> start_index = std::nullopt); // or: ... start_index = {});
int main() {const auto string = "Growing old is mandatory; growing up is optional.";const std::optional<size_t> found_a{find_last(string, 'a')};if (found_a)std::cout << "Found the last a at index " << *found_a << std::endl;const auto found_b{find_last(string, 'b')};if (found_b.has_value())std::cout << "Found the last b at index " << found_b.value() << std::endl;// following line gives an error (cannot convert std::optional<size_t> to size_t)// const size_t found_c{ find_last(string, 'c') };const auto found_early_i{find_last(string, 'i', 10)};if (found_early_i != std::nullopt)std::cout << "Found an early i at index " << *found_early_i << std::endl;
}std::optional<size_t> find_last(const std::string& string, char to_find,std::optional<size_t> start_index) {// code below will not work for empty stringsif (string.empty())return std::nullopt; // or: 'return std::optional<size_t>{};'// or: 'return {};'// determine the starting index for the loop that follows:size_t index = start_index.value_or(string.size() - 1);while (true) // never use while (index >= 0) here, as size_t is always >= 0!{if (string[index] == to_find)return index;if (index == 0)return std::nullopt;--index;}
}
因为我们的函数使用了无符号数作为索引,所以第三个参数的默认值我们不能假设是-1或者其他数了,那么标准库定义了std::nullopt这个值来作为optional的默认值,表示没有初始化的状态
注意代码中函数的第三次调用,我们传入了一个整数10,那么函数会将这个参数转换为optional类型
使用这个类型的两个关键问题是,如何确定里面是否有值而不是nullopt;如何获得其中的有效值
对于第一个问题我们可以在代码中看到对应的解决方法:
- 如果有值optional对象可以转换为true
- has_value()返回值为bool,如果为true说明有值
- 和nullopt相比,不相等说明有值
第二个问题我们也已经看到输出,要么解引用获取值,要么调用value()成员函数
我们再来细想一下这个类的使用逻辑,如果有值就可以取用,如果没值就和nullopt相等,那这样我们可以使用另外一个非常好用的函数value_or,可以看函数内部的下标赋值就是这么做的,这个函数可以合二为一,如果没有值我们就从字符串最后位置开始查找,如果有值就用opt里的值给start_index赋值
同样,本书也只是介绍一部分,剩下的可以去看ref学习,值得一提的是,opt不仅支持解引用还支持箭头运算符->,所以如果是复杂类型我们也可以这样写:
虽然这样让opt看着像指针,但是opt不是指针,这个类会把给的值复制一份存在对象里而不是堆上,总体行为是值传递而不是指针的间接寻址
String Views: The New Reference-to-const-string
我们学引用的时候就知道,复制一个大对象成本很高最好使用引用;const引用看起来很完美但还是有不足,并不能完全避免复制行为,来看下面的代码:
我们传入一个C串但是参数类型是const string&的时候,其实还是发生了复制行为,因为我们需要用一个C串构造出临时string,然后将引用绑定到这个临时string上
有没有什么办法能够不复制作为输入参数的字符串,然后还能使用string提供的这么多好用的方法呢?
std::string_view
provides read-only access to an existing string (a C-style string, astd::string
, or anotherstd::string_view
) without making a copy.
5.10 — Introduction to std::string_view – Learn C++
顾名思义,string_view是字符串的视图,对字符串只读不能进行修改,因此维护成本也很低,内部只存储一个指针和长度
也就是说string_view相当于const string
Using String View Function Parameters
把之前的const string&改成string_view是更好的做法,尤其是在输入参数这里,string_view可以接收三类字符串参数,只要我们不修改字符串
注意两点,string_view不提供c_str()函数但是仍提供data();此外string_view由于不可修改因此不支持连接操作
后面一节没怎么看懂。。。
Spans: The New Reference-to-vector or -array
考虑下面的情景:
之前说过这种逻辑类似的函数我们可以使用下一章介绍的函数模板,但是目前这两个函数都是针对一个数组,一个顺序容器进行的操作,C++20引入了一个新的类型叫span<>,在<span>头文件里
使用span之后我们不用再重载了,而且可以使用下标[]以及range-for,而且span传参是非常高效的,和string_view类似,一般内部也就只存储一个指针和长度
std::span - cppreference.com
除了vector、string、array都支持的一些操作外,span还有一些自己的方法
这样写的话处理函数基本没有需要更改的了,我们已经实现了对相同类型的元素集合这种参数的重载,只是调用的时候需要我们注意一下:
考虑一般的数组,我们可以使用第四个版本的构造函数,直接使用数组引用构造span;如果是一个临时list,那么第八个版本会适用,array有第五和六个版本,其他类型也可以很方便的构造span:
可以看到array直接传进去,span就可以构造出来
如果我们传入的类型长度不能推导出来比如是个起始位置的指针,那么我们是需要第二个参数构造span的:
Spans vs. Views
span比view功能多的地方在于,span和view虽然都不直接存储序列的元素,但是span允许对序列进行修改,而view不允许
front表示首元素,可以直接赋值修改;也支持下标修改元素
这两个操作加上back三个方法都返回的是元素引用,所以可以修改元素;而string_view的下标返回的是const引用所以对元素是只读的
但是span和view一样,不允许我们添加或删除序列元素,也就是不支持push_back等操作
Spans of const Elements
span允许我们修改元素,而const对象不允许修改,因此任何const序列都不能构造span,否则逻辑矛盾,比如下面的代码就不能通过编译:
这也导致一个问题,那些使用span参数的函数无法传入一个const序列
std::string_view is thus most similar to a std::span<const char>.
所以我们需要修改当时的那个查找序列最大数字的函数原型:
把span的元素类型改为const即可解决这个问题
Use span<const T> instead of const vector<T>&. Similarly, use span<T> instead of vector<T>&, unless you need to insert or remove elements.
Fixed-Size Spans
span也可以指定第二个参数表示定长序列
Use span<T,N> instead of array<T,N>& or T(&)[N], and span<const T,N> instead of
const array<T,N>& or const T(&)[N].
但是我没搞懂对于标准库array来说为什么要指定长度,可能有些函数就是处理定长序列才设计的,上面这个求所有数平均值的好像没有必要写这个长度
写成这样照样可以出结果