C++初学者指南-5.标准库(第一部分)–顺序视图
文章目录
- C++初学者指南-5.标准库(第一部分)--顺序视图
- std::string_view (C++17)
- 避免不必要的内存分配
- 类似字符串的函数参数
- 创建string_views
- string_view接口
- std::span (C++20)
- 作为参数(主要用例)
- 明确地划分跨度
- 大小和数据访问
- 比较Span
- 从Span创建Span
- 使用指南
- 函数参数中的视图
- 当心返回视图
- 避免使用局部视图变量
- 备忘录
- 相关内容
视图不拥有资源
一个对象被称为资源(内存、文件句柄、连接、线程、锁等)的所有者,如果它对其生命周期(初始化/创建、结束/销毁)负责。
std::string_view (C++17)
#include <string_view>
- 轻量级(= 复制成本低,可以按值传递)
- 非拥有(= 不负责分配或删除内存)
- 只读视图(= 不允许修改目标字符串)
- 字符范围或字符串(类字符串)对象
- 主要用例:只读函数参数(避免临时复制)
避免不必要的内存分配
动机:只读字符串参数
我们不希望为只读参数进行额外的复制或内存分配!
传统的选择 std::string const& 存在问题:
- std::string可以从字符串字面值或char序列的迭代器范围构造。
- 如果我们将一个对象作为函数参数传递,该对象本身不是字符串,但可以用于构造字符串,比如字符串文字或迭代器范围,那么将会分配一个新的临时字符串对象并绑定到常量引用上。
string_view避免了临时副本:
类似字符串的函数参数
如果你… | 使用参数类型 |
---|---|
始终需要在函数内部保留输入字符串的副本 | std::string 传值 |
想要只读访问不一定需要一个副本 正在使用 C++17/20 | #include <string_view> std::string_view |
想要只读访问不一定需要一个副本 被困在C++98/C++11/C++14标准 | std::string const& 传递常量引用 |
希望可以直接在原字符串上修改(尽量避免使用输出参数) | std::string & 传递非常量引用 |
看这里的更多解释
创建string_views
使用构造函数调用
std::string s = "Some Text";
// view whole string
std::string_view sv1 { s };
// view subrange
std::string_view sv2 {begin(s)+2, begin(s)+5};
std::string_view sv3 {begin(s)+2, end(s)};
运行示例代码
带有特殊标记的字面值 "…"sv
using namespace std::string_view_literals;
auto literal_view = "C-String Literal"sv;
cout << literal_view;
运行示例代码
注意:视图可能会比字符串存在的时间更长!
std::string_view sv1 {std::string{"Text"}};
cout << sv1; // 字符串对象已经被释放!
using namespace std::string_literals;
std::string_view sv2 {"std::string Literal"s};
cout << sv2; // 字符串对象已经被释放!
主要应该把 string_view 作为函数参数使用!
string_view接口
std::string_view指南图表
std::span (C++20)
#include < span >
- 轻量级(= 复制成本低,可以按值传递)
- 非拥有视图(= 不负责分配或删除内存)
- 一个连续的内存块(如std::vector,std::array等)
主要用途:作为函数参数(与容器无关的值访问)
span< int > | 可更改其值的整数序列 |
span sequence of integers whose values can be changed | |
可更改其值的整数序列 | |
span< int const > | 无法更改其值的整数序列 |
span<int,5> | 由5个整数组成的序列(编译时固定的值的数量) |
作为参数(主要用例)
void print_ints (std::span<int const> s);
void print_chars (std::span<char const> s);
void modify_ints (std::span<int> s);
用容器/范围进行调用:
std::vector<int> v {1,2,3,4};
print_ints( v );
std::array<int,3> a {1,2,3};
print_ints( a );
std::string s = "Some Text";
print_chars( s );
std::string_view sv = s;
print_chars( sv );
运行示例代码
使用迭代器范围进行调用:
std::vector<int> v {1,2,3,4,5,6,7,8};
// iterator range:
print_ints( {begin(v), end(v)} );
print_ints( {begin(v)+2, end(v)} );
print_ints( {begin(v)+2, begin(v)+5} );
// iterator + length:
print_ints( {begin(v)+2, 3} );
span将序列数据的存储策略与只访问序列中的元素的代码解耦但不改变其结构。
明确地划分跨度
整个容器/范围的视图:
std::vector<int> w {0, 1, 2, 3, 4, 5, 6};
std::array<int,4> a {0, 1, 2, 3};
// 自动推断类型/长度:
std::span sw1 { w }; // span<int>
std::span sa1 { a }; // span<int,4>
// 明确的只读视图:
std::span sw2 { std::as_const(w) };
// 带有明确类型参数:
std::span<int> sw3 { w };
std::span<int> sa2 { a };
std::span<int const> sw4 { w };
// 使用显式类型参数和长度:
std::span<int,4> sa3{ a };
运行示例代码
容器子序列的视图
vector<int> w {0, 1, 2, 3, 4, 5, 6};
// |----.---'
std::span s1 {begin(w)+2, 4};
std::span s2 {begin(w)+2, end(w)};
运行示例代码
大小和数据访问
std::span<int> s = …;
if (s.empty()) return;
if (s.size() < 1024) { … }
// spans in range-based for loops
for (auto x : s) { … }
// indexed access
s[0] = 8;
if (s[2] > 0) { … }
// iterator access
auto m1 = std::min_element(s.begin(), s.end());
auto m2 = std::min_element(begin(s), end(s));
运行示例代码
比较Span
#include <algorithm> // std::ranges::equal
std::vector<int> v {1,2,3,4};
std::vector<int> w {1,2,3,4};
std::span sv {v};
std::span sw {w};
bool memory_same = sv.data() == sw.data(); // false
bool values_same = std::ranges::equal(sv,sw); // true
运行示例代码
从Span创建Span
std::vector<int> v {0,1,2,3,4,5,6,7,8};
std::span s = v;
auto first3elements = s.first(3);
auto last3elements = s.last(3);
size_t offset = 2;
size_t count = 4;
auto subs = s.subspan(offset, count);
运行示例代码
使用指南
函数参数中的视图
- 将函数实现与数据表示/容器类型分离
- 清楚地传达只读取/修改序列中的元素,而不修改底层的内存/数据结构的意图
- 可以轻松地将函数应用于序列子范围
- 几乎永远不会悬空,即指向已被销毁的内存(因为参数的生存周期超过所有函数局部变量)
int foo (std::span<int const> in) { … }
std::vector<int> v {…};
// v永远比参数 'in' 存活得长久!
foo(v);
foo({begin(v), 5});
- 视图的目标不能让函数执行期间视图引用的内存失效(除非它是在另一个线程中执行的)。
- 视图可以通过避免一级间接访问加快访问速度:
当心返回视图
- 视图指向的对象或内存不是总是清晰的
- 返回的视图可能会(无意中)失效
// 哪个参数是span返回值的目标?
std::span<int const>
foo (std::vector<int> const& x, std::vector<int> const& y);
// 我们可以假设返回的span
// 指向vector的元素
std::span<int const> random_subrange (std::vector<int> const& v);
// 然而,这仍然存在问题:
auto s = random_subrange(std::vector<int>{1,2,3,4});
// 's' 悬空了 - 向量对象已经销毁了!
class Payments { …
public:
std::span<Money const> of (Customer const&) const;
…
};
Customer const& john = …;
Payments pms = read_payments(file1);
auto m = pms.of(john);
pms = read_payments(file2);
// 根据支付的完成情况
// 在重新赋值之后,可能 m 的目标内存不再有效
避免使用局部视图变量
- 易产生悬空视图,因为我们必须手动跟踪生命周期,确保没有视图超过其目标。
- 即使内存所有者仍然存活,它可能会使视图引用的内存无效。
std::string str1 = "Text";
std::string_view sv {str1};
if ( … ) {
std::string str2 = "Text";
sv = str2;
}
cout << sv; // str2 已经被释放!
std::string_view sv1 {"C-String Literal"};
cout << sv1; // 正确
std::string_view sv2 {std::string{"Text"}};
cout << sv2; // 错误 string对象已经被释放!
using namespace std::string_literals;
std::string_view sv3 {"std::string Literal"s};
cout << sv3; // 错误 string对象已经被释放!
所有者的内存失效
像 vector 这样的容器可能会分配新的内存 使它的所有视图无效:
std::vector<int> w {1,2,3,4,5};
std::span s {w};
w.push_back({6,7,8,9});
cout << s[0]; // w 可能重新分配了内存
备忘录
相关内容
std::span
使用哪种字符串函数参数类型?
cppreference的std:span参考
cppreference的std::string_view参考
striing_view的视频教程
string_view是一种借用类型(by Arthur O’Dwyer)
用值传递 std::string_view 的三个原因(by Arthur O’Dwyer)
关于通过值传递 std::string_view 的三个理由的补充(by Arthur O’Dwyer)
附上原文链接
如果文章对您有用,请随手点个赞,谢谢!^_^