模板元编程
模板函数自动推导参数类型 。有时候,一个统一的实现满足不了某些特殊情况。只需添加一个
特化的重载、
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 <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 ); ... }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
#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 ; }
整数也可以作为参数,不过模板参数只支持整数类型(包括 enum)。
浮点类型、指针类型,不能声明为模板参数。
#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 >('%' ); }
#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 函数,可以放心大胆在模板尖括号内使用。
定义和实现都必须放在头文件
如果我们试着像传统函数 那样分离模板函数 的声明与实现:
#pragma once template <bool debug>int sumto (int n) ;
#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; }
#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
里,增加两个显式编译模板的声明:
#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
&。比如懒汉单例模式 :
#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 &。
void func (int const &i) ; 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(); decltype (auto ) b = func_ref(); decltype (auto ) c = func_val(); }
using:创建类型别名
除了 typedef 外,还可以用 using 创建类型别名:
typedef std ::vector <int > VecInt;using VecInt = std ::vector <int >;
以上是等价的。
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 ; }
函数式编程
函数可以作为另一个函数的参数:
#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。
#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)。
#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 ; }
#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 ; }
C++20
函数也可以 auto,lambda 也可以 <class T>。
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
举例:立即求值
#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开始数数)。
#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<...>
尖括号里的类型可以省略。
#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。
#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 内的值。
#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
&&。
auto &&[x, y, ...] = tup; decltype (auto ) [x, y, ...] = tup;
#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,还可以解包任意用户自定义类。
#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。
#include <iostream> #include <variant> int main () { std ::variant<int , float > v = 3 ; std ::cout << std ::get <int >(v) << std ::endl ; std ::cout << std ::get <0 >(v) << std ::endl ; v = 3.14f ; std ::cout << std ::get <float >(v) << std ::endl ; 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。
#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 ; }