现代 C++【3】返回类对象

前提

多现代?C++ 11 就有了。

问题

我想返回一个对象,但我受到惊吓……

是不是应该从指针型参数返回对象?

结论

已经 C++20 了,请放心,直接,返回对象!

概念

  • RVO:Return Value Optimization,返回值优化。

  • NRVO:Named RVO,具名的返回值优化。

返回的对象会 move 给接收的变量,并且,最多可能优化成直接对接收变量进行构造(NRVO)。

如果明确没有 move 构造函数,则会调用 copy 构造函数,当对象构造代价高时,应该尽量保证有 move 构造函数。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 传统,不建议,可读性差,使用也不方便
void GetName(std::string& name) noexcept {
name = "UMU";
}

// RVO,优化,但不够
std::string GetName() noexcept {
return "UMU618";
}

// NRVO, 最优化,推荐这样写!
std::string GetName() noexcept {
std::string name("UMU618");
return name;
}

避坑

没有必要对返回值再加一次 std::move,因为返回本身就已经是 move,再加一次就是多一次没必要的 move。

现代 C++【2】std::span

前提

多现代?C++ 20。

C++ 17 才有 std::string_view,而相似的 std::span 居然到 C++ 20 才有。

问题

如何解决 C-Style 数组(包含动态分配的连续内存)的退化(array decay)和越界访问(range errors)两大问题?

解决

C 语言解决这两个问题,主要是增加一个长度参数。很多 Win32 API 这样做,例如:

1
2
3
4
5
6
7
8
9
10
11
12
PCSTR WSAAPI inet_ntop(
INT Family,
const VOID *pAddr,
PSTR pStringBuf,
size_t StringBufSize
);

int GetWindowTextA(
HWND hWnd,
LPSTR lpString,
int nMaxCount
);

但它会带来新问题:不小心传错!另外也有一些地方并没有提供长度参数,比如下面 Linux 内核代码里的函数:

1
static inline int ip_decrease_ttl(struct iphdr *iph);

当我们打算把 uint8_t 数组转成 struct iphdr * 时,必须在调用前保证数组长度大于等于最小 IP 头长度。

C++ 的解决方案是:std::span,它是一个连续对象存储的观察者。类似 std::string_viewstd::string 的观察者。它可以同时管理数组的地址和大小,并且它没有数据所有权,仅占用最多两个指针的空间,可以像 std::string_view 一样在绝大多数时候直接按值传递。

例子

以下函数用于获取 IP 头的长度:

1
2
3
4
5
6
std::uint8_t GetHeaderLength(const void* ip_header, size_t size) noexcept;

std::uint8_t ip[] = {0x45, 0x00, 0x00, 0x54, 0xfa, 0xa6, 0x40, 0x00,
0x40, 0x01, 0xb3, 0x9a, 0xc0, 0xa8, 0x0b, 0x02,
0xc0, 0xa8, 0x00, 0x15};
std::cout << "HeaderLength: " << (int)GetHeaderLength(ip, sizeof(ip)) << '\n';

它可以用 std::span 包装成:

1
2
3
4
5
6
template <typename T, size_t N>
inline std::uint8_t GetHeaderLength(std::span<T, N> ip_header) noexcept {
return GetHeaderLength(ip_header.data(), sizeof(T) * ip_header.size());
}

std::cout << "HeaderLength: " << (int)GetHeaderLength(std::span{ip}) << '\n';

另一个便利是,使用 subspan 成员函数可以对其内部指针和长度成对操作,以避免单独处理时可能不小心少处理一个的问题。

避坑

std::spanstd::string_view 一样,没有数据所有权,所以要担心数据失效问题,不要在数据被释放后使用。

下面是个错误示范,来自:std::string_view encourages use-after-free; the Core Guidelines Checker doesn’t complain #1038

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>
#include <string_view>

int main() {
std::string s = "Hellooooooooooooooo ";
std::string_view sv = s + "World\n";
std::cout << sv;
}

现代 C++【1】类对象作为函数参数

前提

多现代?C++ 17,因为本文内含 std::string_view

目前 C++ 20 还未普及,CLang 和 GCC 对 C++ 20 不是很上心,【直到今天 2020-08-18】连 std::format 都没有,被 MSVC 甩开。

问题

类对象作为参数究竟应该怎么传?

Effective C++》的条款 20 说:

  1. 宁以 pass-by-reference-to-const 替换 pass-by-value

为什么新规范又建议构造函数 pass-by-value?

原则

  1. 只读访问并且不复制时,使用 pass-by-reference-to-const。

  2. 需要保存对象副本时,并且对象可移动,使用 pass-by-value。

  3. 对象很小时,使用 pass-by-value。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 只读访问,不需要将 text 保存起来。
void Print(const std::string& text) {
std::cout << text;
}

class Foo {
public:
// 需要保存 message 到类成员变量
Foo(std::string message) : message_(std::move(message)) {}

private:
std::string message_;
};

// std::string_view 很小,连 const 都没必要加,就好像 int 型参数不会加 const
void CallCStyleApi(std::string_view dir) {
if (0 < dir.size()) {
chdir(dir.data());
}
}

说明

Foo 的构造函数使用 pass-by-value,这使得它变成“两用”的,相当于针对这个类对象参数同时实现复制构造函数(ctor)、 移动构造函数(mtor)。

  • 当传一个左值给它时,参数 message 是复制的,但它立刻移动给了成员变量 message_,整个过程发生一次复制和一次移动;

  • 当传一个右值给它时,参数 message 是这个右值移动而来的,然后又立刻移动给了成员变量 message_,整个过程发生两次移动;

1
2
3
std::string name("UMU618");
Foo f1(name); // copy + move
Foo f2(std::move(name)); // move 2 times

如果类实现得妥当,移动两次对象实际上最多可以被编译器优化成零次。

八哥之神【番外篇11】

听说鲁豫要来采访稣
请戴口罩采访

由于没有经费写《后传》就把后传的内容写到《番外篇》了。

  1. 为什么最厉害的是周易?

周易是老钧的化身之一。他看到任何结局,但都不去干预。莫名其妙?没错,这正是他的特征。

圣仙山就会去干预世事演化,而成为天道的执行者之一,本质是个工具人。

大部分英雄电影都有一些角色会因为法律、信仰、道义、情感等原因去干预世事,尤其一些穿越剧,妄想改变历史。这些都是不符合天道的。

天地不仁,以万物为刍狗。所以周易最厉害。

  1. 识界是现实中存在的吗?

当然是。

首先要认识到人类集体显意识是确实存在的,比如中药学就是靠着集体显意识保存下来的。如果某种技能只有一个人会,那么它失传是必然的,这个人一死就失传了。存在集体显意识里就不容易失传,但集体显意识也不一定能够很客观地保存信息,有些人出于某种目的,会篡改信息。并且不是什么信息都可以靠集体显意识保存,比如有些东西无法达成共识。

举个例子,噬菌体,一种可以编程控制的半生命体,可以编程它来定向消灭细菌,比如治疗青春痘……安全有效不含抗生素。它在古代治疗过很多奇怪的疾病,尤其是被下蛊之类的,那时候医学理论不完善,人们便以为是草药起了作用,其实是自然界中的噬菌体治好的。这个事情在集体显意识里是没有记录的,它只在集体潜意识里保存。

PS:1990 年以来,人类几乎没有发现新的抗生素种类了。世界卫生组织 2017 年 9 月份宣布,“确认世界的抗生素频临枯竭”。参考:https://www.who.int/zh/news/item/20-09-2017-the-world-is-running-out-of-antibiotics-who-report-confirms

总之抗生素要完,以后由细菌导致的疾病只能靠噬菌体了。

其次,如果承认个人是有潜意识的,那么集体也应该有集体潜意识。

  1. 稣到底怎么来的?

稣的由来可以有很多解释,但根源来自圣仙山参与写作的《山海经》。

我们注意到现实中的颛顼在八哥宇宙里名字是:姬稣。

作者为什么选择和颛顼攀关系呢?其实颛顼和《八哥之神》的主题十分契合。不信您翻开圣仙山参与写作的《山海经》,里面有一句话特地被重复两次。

有鱼偏枯,名曰鱼妇。颛顼死即复苏。风道北来,天及大水泉,蛇乃化为鱼,是为鱼妇。颛顼死即复苏

古人惜墨如金,为什么这句话非要写两遍?这里面藏着惊天大秘密!——意识是可以重复的,唯一可以穿越时间的,也只有意识。比如现代人死后可以回到古代,但由于记忆无法穿越,所以不会存在任何时间悖论。

“稣”字和“苏”同音,并且就是“鱼”字旁,对应文中多次提到的“鱼”。当圣小开要自称稣时,显然“稣”更合适,因为“苏”是个姓,容易被人误会姓苏。

  1. 《山海经》居然是圣仙山写的?

确切地说,圣仙山以巫咸的身份写了《山海经》的预言部分,而这部分正好因为威力太大,若为坏人得之,天下危矣,故被大禹删除。

也有另一种说法是,巫咸给《山海经》作序用的是大禹看不懂的文字(Bân-lâm-gú),所以到这部分预言内容就慢慢失传了。

幸运的是,识界里还保留着完整的《山海经》,刚好作者就懂 Bân-lâm-gú,所以才能得知这些故事,写出《八哥之神》。不过由于《八哥之神创世手稿》失传,导致并非人人可以通过修炼它而进入识界,天下得以承平日久。

PS:圣仙山和妹妹李灵海,名字合起来就是“山海”,这也是个暗示,历史上的巫阳就是李灵海的前世,秦阳也和巫阳相对应,他们都是同一个意识在不同时期或者境界的具化体。

  1. 《八哥之神》真的是无神论???

是的!姬稣是坚定的无神论,所以有“绝地天通”,破除迷信,不让觋巫扰乱民间。在怪力乱神的时代,姬稣实乃是无神论里少数善终之人,活了 97 岁。

另一个无神论者就很惨,他就是齐凤卿前世帝辛,实则是位明君,但由于是无神论者,得罪势力集团,被污蔑造谣为暴君,至今难以平反。em……不能因为妲己长得漂亮,就说她是狐狸精吧!这太不科学哩!

暂时扯到这里。

Boost【6】函数式编程

本文可简单了……结论先行:尽量别用 Boost 进行函数式编程。

原因

  1. 它们大部分已经被加入标准库,应该直接使用 STL。
Boost STL Header
Boost.Function std::function <functional>
Boost.Bind std::bind <functional>
Boost.Ref std::ref, std::cref <functional>
Boost.Lambda lambda part of C++11
  1. Boost.Phoenix 是个例外,目前并没有 STL 可以完全替代,但它可读性不好,尽量不要用。

使用时机

有时候用 Boost.Phoenix 省事,因为它就像 lambda 表达式的模板,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <algorithm>
#include <iostream>
#include <vector>

#include <boost/phoenix/phoenix.hpp>

bool is_odd(int i) {
return i % 2 == 1;
}

int main() {
std::vector<int> v{1, 2, 3, 4, 5};

std::cout << std::count_if(v.begin(), v.end(), is_odd) << '\n';

auto lambda = [](int i) { return i % 2 == 1; };
std::cout << std::count_if(v.begin(), v.end(), lambda) << '\n';

using namespace boost::phoenix::placeholders;
auto phoenix = arg1 % 2 == 1;
std::cout << std::count_if(v.begin(), v.end(), phoenix) << '\n';

std::vector<long long> v2;
v2.insert(v2.begin(), v.begin(), v.end());
// warning
// std::cout << std::count_if(v2.begin(), v2.end(), lambda) << '\n';

std::cout << std::count_if(v.begin(), v.end(), phoenix) << '\n';
std::cout << std::count_if(v2.begin(), v2.end(), phoenix) << '\n';

std::cout << "arg1(): " << arg1(1, 2, 3, 4, 5) << '\n';

auto value = boost::phoenix::val(2);
std::cout << value() << '\n';
}

本例中采用 Boost.Phoenix 可以自动适配 int 和 long long,而 lambda 表达式是确定的 int 参数,传入 long long 会 warning。

C++14 支持基于类型推断的泛型 lambda 表达式,将上面代码改进一下,说明没必要使用 Boost.Phoenix:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <algorithm>
#include <iostream>
#include <vector>

bool is_odd(int i) {
return i % 2 == 1;
}

int main() {
std::vector<int> v{1, 2, 3, 4, 5};

std::cout << std::count_if(v.begin(), v.end(), is_odd) << '\n';

// C++14
auto lambda = [](auto i) { return i % 2 == 1; };
std::cout << std::count_if(v.begin(), v.end(), lambda) << '\n';

std::vector<long long> v2;
v2.insert(v2.begin(), v.begin(), v.end());
std::cout << std::count_if(v2.begin(), v2.end(), lambda) << '\n';
}

延申讨论

STL 和 Boost 都有的类应该用哪个?

  • 如果是标准库原本没有,Boost 先有,然后 Boost 的实现被加入标准库,那么应该使用标准库。

  • 如果 Boost 加强了标准库的实现,那么就看标准库能不能满足您的需求,如果不能再采用 Boost 的。

  • 因为依赖而必须采用 Boost,那就别费力去改用标准库。比如有些 Boost 库(比如 Boost.Log)使用了 boost::shared_ptr,这时候是不能简单地改用 std::shared_ptr 的。

Boost【5】Boost.IO

本文代码:https://github.com/UMU618/test_boost

1. ios_state

痛点

使用 std::cout 指定进制打印数字时经常有一个烦恼:之前设置的进制会一直有效,比如临时想打印一个 16 进制数,然后都打印 10 进制,这时候需要 std::hex,打印,再 std::dec,如果忘记 std::dec,那么后面的数字就全是输出 16 进制形态了……而且,您怎么知道之前用的就是 std::dec?万一是 std::oct 呢?

解决方案

使用 boost::io::ios_all_saver 自动保存和还原 ios 状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

#include <boost/io/ios_state.hpp>

void PrintHex(std::ostream& os, char byte) {
// Try commenting out the next line
boost::io::ios_flags_saver ifs(os);

os << byte << " = " << std::hex << static_cast<unsigned>(byte) << '\n';
}

int main() {
PrintHex(std::cout, 'A');
std::cout << 123 << '\n';
PrintHex(std::cerr, 'b');
std::cout << 456 << '\n';
PrintHex(std::cerr, 'C');
std::cout << 789 << '\n';
}

2. ostream_joiner

需求

打印数组时,不想最后一个元素后面跟着一个分隔符。因为这会让完美主义纠结症患者抓狂!

解决方案

C 语言奇葩版,连 if 都不需要:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main(void) {
int a[6] = {1, 2, 3, 4, 5, 6}, i;
for (i = 0; i < 6; i++) {
printf(",%d" + !i, a[i]);
}
return 0;
}

上面的方案纯属炫技,还是用 ostream_joiner 来搞定,一样看不到 if:

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <array>

#include <boost/io/ostream_joiner.hpp>

int main() {
std::array<int,6> a{1, 2, 3, 4, 5, 6};
std::copy(a.begin(), a.end(), boost::io::make_ostream_joiner(std::cout, ','));
}

3. quoted

需求

大部分语言的字符串都是需要转义的,除非用原始字符串(raw string),有时候想打印出转移后的字符串。

解决方案

使用 C++14 的 std::quoted,或者 boost::io::quoted,默认参数就是 C/C++ 的转义风格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>

#include <boost/io/quoted.hpp>

int main() {
std::string buffer;
std::getline(std::cin, buffer);
std::cout << boost::io::quoted(buffer, '\\', '"') << '\n';
}

/*
* Input: C:\Program Files
* Output: "C:\\Program Files"
*
* Input: {"name":"UMU618","male":true}
* Output: "{\"name\":\"UMU618\",\"male\":true}"
*/

Boost【4】在 macOS 上安装

其它系统请参考:

软件环境

  • macOS arm64

  • XCode

1
brew install p7zip

brew 安装法

macOS 的 brew 更进 boost 很积极,现在就是最新版 1.76:

1
2
# install b2 and boost
brew install boost-build boost

然后跳到文末的“测试安装”。

源码安装法

1. 下载

1
2
cd ~/Downloads
wget https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.7z

2. 解压

1
7z x boost_1_76_0.7z

3. 编译和安装 b2

1
2
3
4
5
6
7
8
9
cd boost_1_76_0
# if you have multiple compilers
# ./bootstrap.sh --cxx=clang++
./bootstrap.sh
cd tools/build
cp ../../b2 ./
./b2 install
cd ../../
cp ./project-config.jam $HOME/user-config.jam

4. 用 b2 编译 Boost

1
b2 install

测试安装

可以用以下仓库验证前面操作是否正确:

https://github.com/UMU618/test_boost

1
2
3
git clone https://github.com/UMU618/test_boost
cd test_boost
sh build.sh

最终编译出来的程序应该打印“OK!”。