C++模版元编程

模板元编程

模板函数自动推导参数类型。有时候,一个统一的实现满足不了某些特殊情况。只需添加一个 特化的重载、

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

template <class T>
T twice(T t) {
return t * 2;
}

std::string twice(std::string t) {
return t + t;
}

int main() {
std::cout << twice(21) << std::endl;
std::cout << twice(3.14f) << std::endl;
std::cout << twice(2.718) << std::endl;
std::cout << twice("hello") << std::endl;
}

但是这样也有一个问题,那就是如果我用 twice(“hello”) 这样去调用,他不会自动隐式转换到 std::string 并调用那个特化函数,而是会去调用模板函数 twice<char *>(“hello”),从而出错。

使用SFINAE机制,可以解决,比如利用 has_reserve 可以用来配合 enale_if 实现编译期的阻断,就是说,根据模板是否能成功展开,选择不同的编译目标代码段:

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
// template<class T>
// struct enable_if<true, T> { typedef T type; };

// template<bool B, class T = void>
// struct enable_if {};

// template< bool B, class T = void >
// using enable_if_t = typename enable_if<B,T>::type;

// 是 C 类型
template <typename C, typename T>
enable_if_t<has_reserve<C>::value,
void>
append(C& container, T* ptr,
size_t size)
{
container.reserve(
container.size() + size);
...
}

// 不是 C 类型
template <typename C, typename T>
enable_if_t<!has_reserve<C>::value,
void>
append(C& container, T* ptr,
size_t size)
{
....
}

默认参数类型

可以通过 template <class T = int> 表示调用者没有指定时,T 默认为 int

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

template <class T = int>
T two() {
return 2;
}

int main() {
std::cout << two<int>() << std::endl;
std::cout << two<float>() << std::endl;
std::cout << two<double>() << std::endl;
std::cout << two() << std::endl; // 等价于 two<int>()
}

整数也可以作为参数,不过模板参数只支持整数类型(包括 enum)。 浮点类型、指针类型,不能声明为模板参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

template <int N = 1, class T>
void show_times(T msg) {
for (int i = 0; i < N; i++) {
std::cout << msg << std::endl;
}
}

int main() {
show_times("one");
show_times<3>(42);
show_times<4>('%');
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

template <int N = 1, class T>
void show_times(T msg) {
for (int i = 0; i < N; i++) {
std::cout << msg << std::endl;
}
}

int main() {
show_times("one");
show_times<3>(42);
show_times<4>('%');
}

参数部分特化

func(vector t) 这样则可以限定仅仅为 vector 类型的参数。

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

template <class T>
T sum(std::vector<T> const &arr) {
T res = 0;
for (int i = 0; i < arr.size(); i++) {
res += arr[i];
}
return res;
}

int main() {
std::vector<int> a = {4, 3, 2, 1};
std::cout << sum(a) << std::endl;
std::vector<float> b = {3.14f, 2.718f};
std::cout << sum(b) << std::endl;
}

这里用了 const & 避免不必要的的拷贝。

为什么要支持整数作为模板参数

template 传入的 N,是一个编译期常量,每个不同的 N,编译器都会单独生成一份代码,从而可以对他做单独的优化。

而 func(int N),则变成运行期常量,编译器无法自动优化,只能运行时根据被调用参数 N 的不同。

比如 show_times<0>() 编译器就可以自动优化为一个空函数。因此模板元编程对高性能编程很重要

通常来说,模板的内部实现需要被暴露出来,除非使用特殊的手段,否则,定义和实现都必须放在头文件里。

但也正因如此,如果过度使用模板,会导致生成的二进制文件大小剧增,编译变得很慢等。

编译期优化案例

用一个 debug 参数控制是否输出调试信息。

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

int sumto(int n, bool debug) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
if (debug)
std::cout << i << "-th: " << res << std::endl;
}
return res;
}

int main() {
std::cout << sumto(4, true) << std::endl;
std::cout << sumto(4, false) << std::endl;
return 0;
}

但是这样 debug 是运行时判断,这样即使是 debug 为 false 也会浪费 CPU 时间。

因此可以把 debug 改成模板参数,这样就是编译期常量。编译器会生成两份函数 sumto<true> 和 sumto<false>。前者保留了调试用的打印语句,后者则完全为性能优化而可以去掉打印语句。

更进一步,可以用C++17的 if constexpr 语法,保证是编译期确定的分支:

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

template <bool debug>
int sumto(int n) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
if constexpr (debug)
std::cout << i << "-th: " << res << std::endl;
}
return res;
}

int main() {
std::cout << sumto<true>(4) << std::endl;
std::cout << sumto<false>(4) << std::endl;
return 0;
}

编译期常量的限制就在于他不能通过运行时变量组成的表达式来指定。

编译期 constexpr 的表达式,一般是无法调用其他函数的:

如果能保证 isnegative 里都可以在编译期求值,将他前面也标上 constexpr 即可。

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

template <bool debug>
int sumto(int n) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
if constexpr (debug)
std::cout << i << "-th: " << res << std::endl;
}
return res;
}

constexpr bool isnegative(int n) {
return n < 0;
}

int main() {
constexpr bool debug = isnegative(-2014);
std::cout << sumto<debug>(4) << std::endl;
return 0;
}

constexpr 函数不能调用 non-constexpr 函数。而且 constexpr 函数必须是内联(inline)的,不能分离声明和定义在另一个文件里。标准库的很多函数如 std::min 也是 constexpr 函数,可以放心大胆在模板尖括号内使用。

定义和实现都必须放在头文件

如果我们试着像传统函数那样分离模板函数的声明与实现:

1
2
3
4
5
6
// sumto.h

#pragma once

template <bool debug>
int sumto(int n);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// sumto.cpp

#include "sumto.h"
#include <iostream>

template <bool debug>
int sumto(int n) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
if constexpr (debug)
std::cout << i << "-th: " << res << std::endl;
}
return res;
}
1
2
3
4
5
6
7
8
9
10
// main.cpp

#include "sumto.h"
#include <iostream>

int main() {
constexpr bool debug = true;
std::cout << sumto<debug>(4) << std::endl;
return 0;
}

会出现 undefined reference 错误。

因为编译器对模板的编译是惰性的,即只有当前 .cpp 文件用到了这个模板,该模板里的函数才会被定义。

而我们的 sumto.cpp 中没有用到 sumto<> 函数的任何一份定义,所以 main.cpp 里只看到 sumto<> 函数的两份声明,从而出错。

解决方法:在看得见 sumto<> 定义的 sumto.cpp 里,增加两个显式编译模板的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "sumto.h"
#include <iostream>

template <bool debug>
int sumto(int n) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
if constexpr (debug)
std::cout << i << "-th: " << res << std::endl;
}
return res;
}

template int sumto<true>(int n);
template int sumto<false>(int n);

延迟编译

只有当 main 调用了模板函数,才会被编译,才会报错。

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

template <class T>
void print(std::vector<T> const &a) {
std::cout << "{";
for (size_t i = 0; i < a.size(); i++) {
std::cout << a[i];
if (i != a.size() - 1)
std::cout << ", ";
}
std::cout << "}" << std::endl;
}

int main() {
std::vector<int> a = {1, 4, 2, 8, 5, 7};
print(a);
std::vector<double> b = {3.14, 2.718, 0.618};
print(b);
return 0;
}

另外,inline在如今的编译器,已经是自动优化的一部分,声明不声明对于函数没有意义。关键是要将声明和实现放在一个文件中。

另外,register等优化关键字,也逐渐被优化到编译中,不需要手动声明。

常引用(int const &)

const 修饰符的存在,使得 ref 不能被写入(赋值)。这样的好处是更加安全(编译器也能够放心大胆地做自动优化)。

auto 也可以用来定义引用,只需要改成 auto & 即可。

函数返回引用

函数的返回类型也可以是 auto & 或者 auto const &。比如懒汉单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <cstdio>
#include <string>
#include <map>

auto &product_table() {
static std::map<std::string, int> instance;
return instance;
}

int main() {
product_table().emplace("佩奇", 80);
product_table().emplace("妈妈", 100);
}

const:常值修饰符

与 & 修饰符不同,int const 和 int 可以看做两个不同的类型。不过 int const 是不可写入

因此 int const & 无非是另一个类型 int const 的引用罢了。这个引用不可写入。

C++ 规定 int && 能自动转换成 int const &,但不能转换成 int &。

1
2
3
4
void func(int const &i);

// 尽管 3 是右值 int &&,但却能传到类型为 int const & 的参数上
func(3);

一个方便查看类型名的小工具:

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
36
37
38
39
#include <iostream>
#include <cstdlib>
#include <string>
#if defined(__GNUC__) || defined(__clang__)
#include <cxxabi.h>
#endif

template <class T>
std::string cpp_type_name() {
const char *name = typeid(T).name();
#if defined(__GNUC__) || defined(__clang__)
int status;
char *p = abi::__cxa_demangle(name, 0, 0, &status);
std::string s = p;
std::free(p);
#else
std::string s = name;
#endif
if (std::is_const_v<std::remove_reference_t<T>>)
s += " const";
if (std::is_volatile_v<std::remove_reference_t<T>>)
s += " volatile";
if (std::is_lvalue_reference_v<T>)
s += " &";
if (std::is_rvalue_reference_v<T>)
s += " &&";
return s;
}

#define SHOW(T) std::cout << cpp_type_name<T>() << std::endl;

int main() {
int a;
auto &c = a;
auto const &b = a;
SHOW(decltype(a));
SHOW(decltype(b));
SHOW(decltype(c));
}

可以通过 decltype(变量名) 获取变量定义时候的类型。

注意 decltype(变量名) 和 decltype(表达式) 是不同的。 可以通过 decltype((a)) 来强制编译器使用后者,从而得到 int &。

万能推导(decltype(auto))

如果一个表达式,我不知道他是个可变引用(int &),常引用(int const &),右值引用(int &&),还是一个普通的值(int)。

想要定义一个和表达式返回类型一样的变量,这时候可以用:

decltype(auto) p = func(); 会自动推导为 func() 的返回类型。

decltype(auto) 能够同时适配 auto 和 auto & 返回值类型的两种情况。

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

int t;

int const &func_ref() {
return t;
}

int const &func_cref() {
return t;
}

int func_val() {
return t;
}

int main() {
decltype(auto) a = func_cref(); // int const &a
decltype(auto) b = func_ref(); // int &b
decltype(auto) c = func_val(); // int c
}

using:创建类型别名

除了 typedef 外,还可以用 using 创建类型别名:

1
2
typedef std::vector<int> VecInt;
using VecInt = std::vector<int>;

以上是等价的。

1
2
typedef int (*PFunc)(int);
using PFunc = int(*)(int);

以上是等价的。

一个例子

这是一个实现将两个不同类型 vector 逐元素相加的函数:

用 decltype(T1{} * T2{}) 算出 T1 和 T2 类型相加以后的结果,并做为返回的 vector 容器中的数据类型。

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

template <class T1, class T2>
auto add(std::vector<T1> const &a, std::vector<T2> const &b) {
using T0 = decltype(T1{} + T2{});
std::vector<T0> ret;
for (size_t i = 0; i < std::min(a.size(), b.size()); i++) {
ret.push_back(a[i] + b[i]);
}
return ret;
}

int main() {
std::vector<int> a = {2, 3, 4};
std::vector<float> b = {0.5f, 1.0f, 2.0f};
auto c = add(a, b);
for (size_t i = 0; i < c.size(); i++) {
std::cout << c[i] << std::endl;
}
return 0;
}

函数式编程

函数可以作为另一个函数的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cstdio>

void say_hello() {
printf("Hello!\n");
}

void call_twice(void func()) {
func();
func();
}

int main() {
call_twice(say_hello);
return 0;
}

函数作为模板类型

call_twice 会自动对每个不同的 func 类型编译一遍,从而允许编译器更好地进行自动适配与优化。

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

void print_float(float n) {
printf("Float %f\n", n);
}

void print_int(int n) {
printf("Int %d\n", n);
}

template <class Func>
void call_twice(Func func) {
func(0);
func(1);
}

int main() {
call_twice(print_float);
call_twice(print_int);
return 0;
}

lambda表达式

如果 lambda 表达式不通过 -> 指定返回类型,则和 -> auto 等价,自动根据函数体内的 return 语句决定返回类型,如果没有 return 语句则相当于 -> void。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <cstdio>

template <class Func>
void call_twice(Func func) {
func(0);
func(1);
}

int main() {
auto myfunc = [] (int n) {
printf("Number %d\n", n);
};
call_twice(myfunc);
return 0;
}

lambda 函数体中,还可以使用定义他的 main 函数中的变量,只需要把方括号 [] 改成 [&] 即可。

函数可以引用定义位置所有的变量,这个特性在函数式编程中称为闭包(closure)。

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

template <class Func>
void call_twice(Func func) {
std::cout << func(0) << std::endl;
std::cout << func(1) << std::endl;
}

int main() {
auto twice = [] (int n) -> int {
return n * 2;
};
call_twice(twice);
return 0;
}

[&] 不仅可以读取 main 中的变量,还可以写入 main 中的变量,比如可以通过 counter++ 记录该函数被调用了多少次。

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

template <class Func>
void call_twice(Func func) {
std::cout << func(0) << std::endl;
std::cout << func(1) << std::endl;
}

int main() {
int fac = 2;
int counter = 0;
auto twice = [&] (int n) {
counter++;
return n * fac;
};
call_twice(twice);
std::cout << "调用了 " << counter << " 次" << std::endl;
return 0;
}

此外,最好把模板参数的 Func 声明为 Func const & 以避免不必要的拷贝。

因为闭包的需要,Func在参数传递复制时,需要将局部变量fac和counter也进行复制传递。所以不加const会使得参数空间为16字节。(64位机器)

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

template <class Func>
void call_twice(Func const &func) {
std::cout << func(0) << std::endl;
std::cout << func(1) << std::endl;
std::cout << "Func 的大小: " << sizeof(Func) << std::endl;
}

int main() {
int fac = 2;
int counter = 0;
auto twice = [&] (int n) {
counter++;
return n * fac;
};
call_twice(twice);
std::cout << "调用了 " << counter << " 次" << std::endl;
return 0;
}

作为返回值

函数可以作为参数,当然也可以作为返回值。由于 lambda 表达式永远是个匿名类型,我们需要将 make_twice 的返回类型声明为 auto 让他自动推导。

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

template <class Func>
void call_twice(Func const &func) {
std::cout << func(0) << std::endl;
std::cout << func(1) << std::endl;
std::cout << "Func 大小: " << sizeof(Func) << std::endl;
}

auto make_twice() {
return [] (int n) {
return n * 2;
};
}

int main() {
auto twice = make_twice();
call_twice(twice);
return 0;
}

如果用 [&],请保证 lambda 对象的生命周期不超过他捕获的所有引用的寿命。这时,我们可以用 [=] 来捕获,[=] 会给每一个引用了的变量做一份拷贝。性能可能会不如 [&]。

lambda 作为参数:通常用 [&] 存储引用。

lambda 作为返回值:总是用 [=] 存储值。

函数指针

如果你的 lambda 没有捕获任何局部变量,也就是 [],那么不需要用 std::function<int(int)>,直接用函数指针的类型 int(int) 或者 int(*)(int) 即可。

函数指针效率更高一些,但是 [] 就没办法捕获局部变量了(全局变量还是可以的)。

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

void call_twice(std::function<int(int)> const &func) {
std::cout << func(0) << std::endl;
std::cout << func(1) << std::endl;
std::cout << "Func 大小: " << sizeof(func) << std::endl;
}

std::function<int(int)> make_twice(int fac) {
return [=] (int n) {
return n * fac;
};
}

int main() {
auto twice = make_twice(2);
call_twice(twice);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <functional>

void call_twice(int func(int)) {
std::cout << func(0) << std::endl;
std::cout << func(1) << std::endl;
std::cout << "Func 大小: " << sizeof(func) << std::endl;
}

int main() {
call_twice([] (int n) {
return n * 2;
});
return 0;
}

lambda + 模板

可以将 lambda 表达式的参数声明为 auto,声明为 auto 的参数会自动根据调用者给的参数推导类型,基本上和 template <class T> 等价。

auto const & 也是同理,等价于模板函数的 T const &。

带 auto 参数的 lambda 表达式,和模板函数一样,同样会有惰性、多次编译的特性。

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

void call_twice(auto const &func) {
std::cout << func(3.14f) << std::endl;
std::cout << func(21) << std::endl;
}

int main() {
auto twice = [] <class T> (T n) {
return n * 2;
};
call_twice(twice);
return 0;
}

/* 等价于:
auto twice(auto n) {
return n * 2;
}
*/

C++20

函数也可以 auto,lambda 也可以 <class T>。

1
2
3
4
5
auto wrap(auto f) {
return [=] (auto ...args) {
return f(f, args...);
};
}

举例:yield模式

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
#include <iostream>
#include <vector>

template <class Func>
void fetch_data(Func const &func) {
for (int i = 0; i < 32; i++) {
func(i);
func(i + 0.5f);
}
}

int main() {
std::vector<int> res_i;
std::vector<float> res_f;
fetch_data([&] (auto const &x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, int>) {
res_i.push_back(x);
} else if constexpr (std::is_same_v<T, float>) {
res_f.push_back(x);
}
});
std::cout << res_i.size() << std::endl;
std::cout << res_f.size() << std::endl;
return 0;
}

这里用了 type_traits 来获取 x 的类型。 decay_t<int const &> = int is_same_v<int, int> = true is_same_v<float, int> = false

举例:立即求值

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

int main() {
std::vector<int> arr = {1, 4, 2, 8, 5, 7};
int tofind = 5;
int index = [&] {
for (int i = 0; i < arr.size(); i++)
if (arr[i] == tofind)
return i;
return -1;
}();
std::cout << index << std::endl;
return 0;
}

不需要flag变量。

举例:局部实现递归

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

int main() {
std::vector<int> arr = {1, 4, 2, 8, 5, 7, 1, 4};
std::set<int> visited;
auto dfs = [&] (auto const &dfs, int index) -> void {
if (visited.find(index) == visited.end()) {
visited.insert(index);
std::cout << index << std::endl;
int next = arr[index];
dfs(dfs, next);
}
};
dfs(dfs, 0);
return 0;
}

C++ 新特性

tuple

std::tuple<...> 可以将多个不同类型的值打包成一个。尖括号里填各个元素的类型。之后可以用 std::get<0> 获取第0个元素,std::get<1> 获取第1个元素,以此类推(从0开始数数)。

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

int main() {
auto tup = std::tuple<int, float, char>(3, 3.14f, 'h');

int first = std::get<0>(tup);
float second = std::get<1>(tup);
char third = std::get<2>(tup);

std::cout << first << std::endl;
std::cout << second << std::endl;
std::cout << third << std::endl;
return 0;
}

C++17 的新特性:CTAD。当用于构造函数时,std::tuple<...> 尖括号里的类型可以省略。

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

int main() {
auto tup = std::tuple(3, 3.14f, 'h');

auto first = std::get<0>(tup);
auto second = std::get<1>(tup);
auto third = std::get<2>(tup);

std::cout << first << std::endl;
std::cout << second << std::endl;
std::cout << third << std::endl;
return 0;
}

通过 auto 自动推导 get 的返回类型。

结构化绑定

利用一个方括号,里面是变量名列表,即可解包一个 tuple。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <tuple>

int main() {
auto tup = std::tuple(3, 3.14f, 'h');

auto [first, second, third] = tup;

std::cout << first << std::endl;
std::cout << second << std::endl;
std::cout << third << std::endl;
return 0;
}

结构化绑定也支持绑定为引用,这样相当于解包出来的 x, y, ... 都是 auto & 推导出来的引用类型。对引用的修改可以影响到原 tuple 内的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <tuple>

int main() {
auto tup = std::tuple(3, 3.14f, 'h');

auto &[first, second, third] = tup;

std::cout << std::get<0>(tup) << std::endl;
first = 42;
std::cout << std::get<0>(tup) << std::endl;

return 0;
}

万能推导

注意一下万能推导的 decltype(auto),由于历史原因,他对应的结构化绑定是 auto &&。

1
2
auto &&[x, y, ...] = tup;           // 正确!
decltype(auto) [x, y, ...] = tup; // 错误!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <tuple>

int main() {
auto tup = std::tuple(3, 3.14f, 'h');

auto &&[first, second, third] = tup;

std::cout << std::get<0>(tup) << std::endl;
first = 42;
std::cout << std::get<0>(tup) << std::endl;

return 0;
}

结构化绑定不仅可以解包 std::tuple,还可以解包任意用户自定义类。

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

struct MyClass {
int x;
float y;
};

int main() {
MyClass mc = {42, 3.14f};

auto [x, y] = mc;

std::cout << x << ", " << y << std::endl;
return 0;
}

optional

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

std::optional<float> mysqrt(float x) {
if (x >= 0.f) {
return std::sqrt(x);
} else {
return std::nullopt;
}
}

int main() {
auto ret = mysqrt(-3.14f);
printf("成功!结果为:%f\n", ret.value());
return 0;
}

value_or() 方便地指定一个缺省值:

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

std::optional<float> mysqrt(float x) {
if (x >= 0.f) {
return std::sqrt(x);
} else {
return std::nullopt;
}
}

int main() {
auto ret = mysqrt(-3.14f);
printf("成功!结果为:%f\n", ret.value_or());
return 0;
}

value() 会检测是否为空,空则抛出异常 std::bad_optional_access:

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

std::optional<float> mysqrt(float x) {
if (x >= 0.f) {
return std::sqrt(x);
} else {
return std::nullopt;
}
}

int main() {
auto ret = mysqrt(-3.14f);
printf("成功!结果为:%f\n", ret.value());
return 0;
}

除了 ret.value() 之外还可以用 *ret 获取 optional 容器中的值,不过operator*() 不检测是否为空,不会抛出异常,更加高效,但是要注意安全。

在 if 的条件表达式中,其实可以直接写 if (ret),他和 if (ret.has_value()) 等价。

nullopt

nullopt 则模仿 nullptr,但是他更安全,且符合 RAII 思想,当设为 nullopt 时会自动释放内部的对象。和 unique_ptr 的区别在于他的对象存储在栈上,效率更高。

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

std::optional<float> mysqrt(float x) {
if (x >= 0.f) {
return std::sqrt(x);
} else {
return std::nullopt;
}
}

int main() {
auto ret = mysqrt(-3.14f);
if (ret.has_value()) {
printf("成功!结果为:%f\n", *ret);
} else {
printf("失败!找不到平方根!\n");
}
return 0;
}

variant

安全的 union。和 union 相比,variant 符合 RAII 思想,更加安全易用。

image-20220608224818039

要获取某个类型的值,比如要获取 int 用 std::get。如果当前 variant 里不是这个类型,就会抛出异常:std::bad_variant_access。

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

int main() {
std::variant<int, float> v = 3;

std::cout << std::get<int>(v) << std::endl; // 3
std::cout << std::get<0>(v) << std::endl; // 3

v = 3.14f;

std::cout << std::get<float>(v) << std::endl; // 3.14f
std::cout << std::get<int>(v) << std::endl; // 运行时错误

return 0;
}

可以用 std::holds_alternative 判断当前里面存储的是不是 int。

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

void print(std::variant<int, float> const &v) {
if (std::holds_alternative<int>(v)) {
std::cout << std::get<int>(v) << std::endl;
} else if (std::holds_alternative<float>(v)) {
std::cout << std::get<float>(v) << std::endl;
}
}

int main() {
std::variant<int, float> v = 3;
print(v);
v = 3.14f;
print(v);
return 0;
}

除了这个之外,还可以用成员方法 index() 获取当前是参数列表中的第几个类型。

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

void print(std::variant<int, float> const &v) {
if (v.index() == 0) {
std::cout << std::get<0>(v) << std::endl;
} else if (v.index() == 1) {
std::cout << std::get<1>(v) << std::endl;
}
}

int main() {
std::variant<int, float> v = 3;
print(v);
v = 3.14f;
print(v);
return 0;
}

批量匹配 std::visit

如果你的 if-else 每个分支长得都差不多(除了 std::get<> 的类型不一样以外),可以考虑用 std::visit,他会自动用相应的类型,调用你的 lambda。

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

void print(std::variant<int, float> const &v) {
std::visit([&] (auto const &t) {
std::cout << t << std::endl;
}, v);
}

int main() {
std::variant<int, float> v = 3;
print(v);
v = 3.14f;
print(v);
return 0;
}

这里用到了带 auto 的 lambda,利用了他具有多次编译的特性,实现编译多个分支的效果。

std::visit、std::variant 的这种模式称为静态多态,和虚函数、抽象类的动态多态相对。静态多态的优点是:性能开销小,存储大小固定。缺点是:类型固定,不能运行时扩充。

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

void print(std::variant<int, float> const &v) {
std::visit([&] (auto const &t) {
std::cout << t << std::endl;
}, v);
}

auto add(std::variant<int, float> const &v1,
std::variant<int, float> const &v2) {
std::variant<int, float> ret;
std::visit([&] (auto const &t1, auto const &t2) {
ret = t1 + t2;
}, v1, v2);
return ret;
}

int main() {
std::variant<int, float> v = 3;
print(add(v, 3.14f));
return 0;
}

所以如果 variant 有 n 个类型,那 lambda 就要被编译 n² 次,编译可能会变慢。但是标准库能保证运行时是 O(1) 的(他们用函数指针实现分支,不是暴力 if-else)。

std::visit里面的 lambda 可以有返回值:

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

void print(std::variant<int, float> const &v) {
std::visit([&] (auto const &t) {
std::cout << t << std::endl;
}, v);
}

auto add(std::variant<int, float> const &v1,
std::variant<int, float> const &v2) {
return std::visit([&] (auto const &t1, auto const &t2)
-> std::variant<int, float> {
return t1 + t2;
}, v1, v2);
}

int main() {
std::variant<int, float> v = 3;
print(add(v, 3.14f));
return 0;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!