模板元编程与函数式

文章目录

1.模板函数

为什么需要模板函数(template)
避免重复写代码。

  • 比如,利用重载实现“将一个数乘以2”这个功能,需要:
  • eg:my_course/course/03/1_template/01/main.cpp
#include <iostream>

int twice(int t) {
    return t * 2;
}

float twice(float t) {
    return t * 2;
}

double twice(double t) {
    return t * 2;
}

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

CMakeLists.txt

cmake_minimum_required(VERSION 3.12)
set(CMAKE_CXX_STANDARD 17)
project(hellocpp LANGUAGES CXX)

add_executable(cpptest main.cpp)

set(VAR "wangji")
if ($VAR)
    message("VAR=${VAR}")
endif()

run.sh

set -e

cmake -B build
cmake --build build --target cpptest
build/cpptest
  • 测试:
    在这里插入图片描述

(1)模板函数:定义

  • 使用 template
  • 其中 T 可以变成任意类型。
  • 调用时 twice 即可将 T 替换为 int。
  • 注意有的教材上写做:
template <typename T>
是完全等价的,只是个人喜好不同。
  • eg:my_course/course/03/1_template/02/main.cpp
#include <iostream>

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

int main() {
    std::cout << twice<int>(21) << std::endl;
    std::cout << twice<float>(3.14f) << std::endl;
    std::cout << twice<double>(2.718) << std::endl;
}

(2)模板函数:自动推导参数类型

  • 那这样需要手动写 , 用起来还不如重载方便了?
  • 别担心,C++ 规定:
    当模板类型参数 T 作为函数参数时,则可以省略该模板参数。自动根据调用者的参数判断。
  • eg:my_course/course/03/1_template/03/main.cpp
#include <iostream>

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

int main()
{
    std::cout << twice<int>(21) << std::endl;
    std::cout << twice<float>(3.14f) << std::endl;
    std::cout << twice<double>(2.718) << std::endl;
}

(3)模板函数:特化的重载

  • 有时候,一个统一的实现(比如 t * 2)满足不了某些特殊情况。比如 std::string 就不能用乘法来重复,这时候我们需要用 t + t 来替代,怎么办呢?
  • 没关系,只需添加一个 twice(std::string) 即可,他会自动和已有的模板 twice(T) 之间相互重载。
  • eg:my_course/course/03/1_template/04/main.cpp
#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”) 这样去调用(const char*),他不会自动隐式转换到 std::string 并调用那个特化函数,而是会去调用模板函数 twice<char *>(“hello”),从而出错。

可能的解决方案:SFINAE。
就是在twice函数模板后面加上std::enable_if,if T!=const char*,这样就不会调用这个模板函数

  • eg:my_course/course/03/1_template/05/main.cpp
#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;
}

#include <iostream>

template <typename T, typename U = const char *, typename std::enable_if_t<!std::is_same_v<T, U>, U> = nullptr>
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;
}

(4)模板函数:默认参数类型

  • 但是如果模板类型参数 T 没有出现在函数的参数中,那么编译器就无法推断,就不得不手动指定了。

但是,可以通过
template <class T = int>
表示调用者没有指定时,T 默认为 int。(类似函数的默认参数)

  • eg:my_course/course/03/1_template/05/main.cpp
#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>()
}

(4)模板参数:整数也可以作为参数

template <class T>
可以声明类型 T 作为模板尖括号里的参数。

除了类型,任意整数也可以作为模板参数
template <int N>
来声明一个整数 N 作为模板参数。
  • 不过模板参数只支持整数类型(包括 enum)。

  • 浮点类型、指针类型,不能声明为模板参数。自定义类型也不可以,比如:

template <float F, glm::vec3 V>  // 错误!
  • eg:my_course/course/03/1_template/06/main.cpp
#include <iostream>

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

int main() {
    show_times<1>("one");
    show_times<3>("three");
    show_times<4>("four");
}

  • 测试:
    在这里插入图片描述

(5)模板参数:多个模板参数

  • int N 和 class T 可以一起使用。
  • 你只需要指定其中一部分参数即可,会自动根据参数类型(T msg)、默认值(int N = 1),推断尖括号里没有指定的那些参数。
  • eg:my_course/course/03/1_template/07/main.cpp
#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>('%');
}

  • 测试:
    在这里插入图片描述

(6)模板参数:参数部分特化

  • func(T t) 完全让参数类型取决于调用者。
  • func(vector t) 这样则可以限定仅仅为 vector 类型的参数。
  • T是另外一个vector模板的参数

这里用了 const & 避免不必要的的拷贝。
不过,这种部分特化不支持隐式转换

  • eg:my_course/course/03/1_template/08/main.cpp
#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(std::vector<int>{1,2}) << std::endl;
    std::vector<float> b = {3.14f, 2.718f};
    std::cout << sum(b) << std::endl;
}

  • 测试:
    在这里插入图片描述

2.为什么要支持整数作为模板参数:因为是编译期常量

你可能会想,模板只需要支持 class T 不就行了?反正 int N 可以作为函数的参数传入,模板还不支持浮点。

  • 模板参数可以作为编译期常量,编译期常量就能够自动优化
  • eg:一个是模板参数,一个是函数参数
template <int N> 
void func();void func(int N);

区别如下:

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

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

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

  • 通常来说,模板的内部实现需要被暴露出来,除非使用特殊的手段,否则,定义和实现都必须放在头文件里。
  • 但也正因如此,如果过度使用模板,会导致生成的二进制文件大小剧增,编译变得很慢等。

(1)模板的应用:编译期优化案例

  • 在下边这个案例中,我们声明了一个 sumto 函数,作用是求出从 1 到 n 所有数字的和。
  • 用一个 debug 参数控制是否输出调试信息。
  • 但是这样 debug 是运行时判断,这样即使是 debug 为 false 也会浪费 CPU 时间。
  • eg:my_course/course/03/2_constexpr/01/main.cpp
#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 改成模板参数,这样就是编译期常量。

  • 编译器会生成两份函数 sumto<true> 和 sumto<false>。前者保留了调试用的打印语句,后者则完全为性能优化而可以去掉打印语句。

  • 后者其实在编译器看来就是

if (false) std::cout << …
这样显然是会被他自动优化掉的。
debug若不是编译期常量,编译器也没法优化

#include <iostream>

template <bool debug>
int sumto(int n)
{
    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<true>(4) << std::endl;
    std::cout << sumto<false>(4) << std::endl;
    return 0;
}

(2)模板的应用:编译期分支

  • 更进一步,可以用C++17的 if constexpr 语法,保证是编译期确定的分支:
  • 效果类似#ifdef
  • eg:my_course/course/03/2_constexpr/02/main.cpp
#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;
}
  • 测试:
    在这里插入图片描述
jiwangreal@ubuntu:~/code/my_course/course/03/2_constexpr/01$ nm  build/cpptest |grep -i sumto
0000000000400926 W _Z5sumtoILb0EEii
00000000004008bb W _Z5sumtoILb1EEii

模板的难题:编译期常量的限制

  • 编译期常量的限制就在于:不能通过运行时变量组成的表达式来指定
  • 比如:这里在 if constexpr 的表达式里用到了运行时变量,从而无法作为编译期分支的条件。
    在这里插入图片描述
  • 除了 if constexpr 的表达式不能用运行时变量,模板尖括号内的参数也不能:
    在这里插入图片描述
  • 可以在 bool debug 变量的定义前面加上 constexpr 来解决:
    eg:my_course/course/03/2_constexpr/03/main.cpp
    在这里插入图片描述
  • 但这样 debug = 右边的值也必须为编译期常量,否则出错:
    在这里插入图片描述

模板的难题:编译期常函数

  • 编译期 constexpr 的表达式,一般是无法调用其他函数的。
  • eg:my_course/course/03/2_constexpr/03/main.cpp
    在这里插入图片描述
  • 解决:如果能保证 isnegative 里都可以在编译期求值,将他前面也标上 constexpr 即可
#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 函数,可以放心大胆在模板尖括号内使用。

(3)模板的难题:移到另一个文件中定义

  • 如果我们试着像传统函数那样分离模板函数的声明与实现:
  • 就会出现 undefined reference 错误:
    eg:my_course/course/03/2_constexpr/04/sumto.cpp
    my_course/course/03/2_constexpr/04/sumto.h
    在这里插入图片描述

原因:

  • 一般来说,我会建议模板不要分离声明和定义,直接写在头文件里即可。如果分离还要罗列出所有模板参数的排列组合,违背了开-闭原则。

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

  • sumto.cpp 中没有用到 sumto<> 函数的任何一份定义,所以 main.cpp 里只看到 sumto<> 函数的两份声明,从而出错。(sumto.h仅有模板函数声明,没有定义;所以需要手动实例化模板函数到cpp中,让main.cpp能找到实现)
    解决:在看得见 sumto<> 定义的 sumto.cpp 里,增加两个显式编译模板的声明:

sumto.h

#pragma once

template <bool debug>
int sumto(int n);

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.cpp

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

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

(4)模板的惰性:延迟编译

  • eg:my_course/course/03/2_constexpr/05/main.cpp
    要是编译器哪怕细看了一眼:字符串怎么可能被写入呢?肯定是会出错的。
    但是却没有出错,这是因为模板没有被调用,所以不会被实际编译!
  • 而只有当 main 调用了这个函数,才会被编译,才会报错!
    用一个假模板实现延迟编译的技术,可以加快编译的速度,用于代理模式等。
#include <iostream>

template <class T = void>
void func_that_never_pass_compile() {
    "字符串" = 2333;
}

int main() {
    return 0;
}

(5)打印vector的模板函数

  • 比如,要打印任意一个 vector:
  • print和std::cout都是重载的模板函数,所以只需要写一遍,就可以运用到所有类型
  • eg:my_course/course/03/2_constexpr/06/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

模板函数:配合运算符重载

  • 实现用 std::cout << a 打印任意 vector:

os<<PRETTY_FUNCTION<<std::endl;可以打印出函数的名字和其所有的函数参数,模板参数
std::ostream就是std::cout类型

  • eg:my_course/course/03/2_constexpr/07/main.cpp
#include <iostream>
#include <vector>

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

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

  • 测试:
    在这里插入图片描述

(6)模板函数总结

类型作为参数:template <class T>
整数值作为参数:template <int N>
定义默认参数:template <int N = 0, class T = int>
使用模板函数:myfunc<T, N>(...)
模板函数可以自动推断类型,从而参与重载
模板具有惰性、多次编译(T变了,就会再次编译一次)的特点

3.自动类型推导(auto)

(1)自动类型推导:定义变量

没有 auto 的话,需要声明一个变量,必须重复一遍他的类型,非常麻烦:

  • 因此 C++11 引入了 auto,使用 auto 定义的变量,其类型会自动根据等号右边的值来确定
  • eg:course/03/3_auto/01/main.cpp
#include <cstdio>
#include <memory>

struct MyClassWithVeryLongName {
};

auto func() {
    return std::make_shared<MyClassWithVeryLongName>();
}

int main() {
    auto p = func();
}

自动类型推导:一些局限性

  • 不过 auto 也并非万能,他也有很多限制。
  • 因为需要等号右边的类型信息,所以没有 = 单独声明一个 auto 变量是不行的
auto p;
  • 类成员也不可以定义为 auto:
struct MyClassWithVeryLongName {
	auto x=std::make_shared<int>();
};

(2)自动类型推导:函数返回值

除了可以用于定义变量,还可以用作函数的返回类型

  • eg:course/03/3_auto/01/main.cpp
auto func() {
    return std::make_shared<MyClassWithVeryLongName>();
}

与上一种种等价:
std::shared_ptr<MyClassWithVeryLongName> func() {
    return std::make_shared<MyClassWithVeryLongName>();
}

使用 auto 以后,会自动被推导为 return 右边的类型。

  • 不过也有三点注意事项:
    (1)当函数有多条 return 语句时,所有语句的返回类型必须一致,否则 auto 会报错。
    (2)当函数没有 return 语句时,auto 会被推导为 void。
    (3)如果声明和实现分离了,则不能声明为 auto。比如:auto func(); // 错误

4.引用

(1)引用(int &)

C++ 中有一种特殊的类型,叫做引用。

  • 只需要在原类型后面加一个 & 即可。
  • 引用的本质无非是指针,当我们试图修改一个引用时,实际上是修改了原来的对象:
  • eg:course/03/3_auto/02/main.cpp
    可见,和C语言的int *相比无非是减少了&和*的麻烦而已。
#include <cstdio>

int main() {
    int x = 233;
    int &ref = x;
    ref = 42;
    printf("%d\n", x);    // 42
    x = 1024;
    printf("%d\n", ref);  // 1024
}

  • 测试:
    在这里插入图片描述

(2)常引用(int const &)

如果说 int & 相当于 int *,那么 int const & 就相当于 int const *。

  • const 修饰符的存在,使得 ref 不能被写入(赋值)。
  • 这样的好处是更加安全(编译器也能够放心大胆地做自动优化):
  • ref必须进行初始化
  • eg:course/03/3_auto/03/main.cpp
#include <cstdio>

int main() {
    int x = 233;
    int const &ref = x;
    // ref = 42;  // 会出错!
    printf("%d\n", x);    // 233
    x = 1024;
    printf("%d\n", ref);  // 1024
}

(3)自动类型推导:定义引用(auto &)

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

  • eg:
#include <cstdio>

int main() {
    int x = 233;
    auto &ref = x;
    // ref = 42;  // 会出错!
    printf("%d\n", x);    // 233
    x = 1024;
    printf("%d\n", ref);  // 1024
}

(4)自动类型推导:定义常引用(auto const &)

auto const & 可以定义常引用:

  • eg:
#include <cstdio>

int main() {
    int x = 233;
    const auto &ref = x;
    // ref = 42;  // 会出错!
    printf("%d\n", x);    // 233
    x = 1024;
    printf("%d\n", ref);  // 1024
}

(5)自动类型推导:函数返回引用

函数的返回类型也可以是 auto & 或者 auto const &。

  • 比如懒汉单例模式:
    懒汉单例模式:当第一次进入该函数时,static变量才会初始化
  • eg:course/03/3_auto/04/main.cpp
#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);
}

(6)typeid,区分const*和*const,type_traits

typeid最好配合有虚函数的类一起使用

struct A{};
struct B : public A{};
A* a=new B;
typeid(*a);//A

struct A{
	virtual ~A()=default;
};
struct B : public A{};
A* a=new B;
typeid(*a);//B

要看在const的左边还是右边(去掉const)

  • 下面的*都在const的右边,说明都是一样的
const int*
int const*

type_traits类型萃取,判断类型是否是一样的
std::cout<<std::is_same_v<int* const,const int*> << std::endl;

移除const
std::cout<<std::is_same_v<std::remove_const_t<int const>,int> << std::endl;

移除&
std::cout<<std::is_same_v<std::remove_const_t<std::remove_reference_t<int const&>>,int> << std::endl;int const &->变成int
std::cout<<std::is_same_v<std::decay_t<int const&>,int> << std::endl;int[]退化成int*
std::cout<<std::is_same_v<std::decay_t<int []>,int *> << std::endl;



以前C++11是有::type,才会加上typename,现在是_t,_v
#include <iostream>
#include <type_traits>
template<class T>
using decay_t = std::decay<T>::type;
int main()
{
	typename std::decay<int>::type;//yiqian
	std::decay_t<int>;
	std::is_same<int,int>::value;//yiqian
	std::is_samve_v<int,int>;
	return 0;
}

(7)c++类模板template中的typename

类作用域

  • 在类外部访问类中的名称时,可以使用类作用域操作符,形如MyClass::name的调用通常存在三种:静态数据成员、静态成员函数和嵌套类型:
struct MyClass
{
 
static int A;
 
static int B();
 
typedef int C;
 
}

MyClass::A, MyClass::B, MyClass::C分别对应着上面三种。

引入typename的原因

  • 无法区分iterator是一个类型还是变量,所以引入了typename
template <class T>
 
void foo() {
 
T::iterator * iter;
 
// ...
 
}

问题:
T::iterator实际上可以是以下三种中的任何一种类型:
静态数据成员
静态成员函数
嵌套类型

(1)假设实例化foo模板函数的类型是像这样的
struct ContainsAType {
 
struct iterator { /*...*/ };
 
// ...
 
};

实例化foo:
foo<ContainsAType>();
这样是没问题的

(2)但是,如果模板的类型是下面这样的

```cpp
struct ContainsAnotherType {
 
static int iterator;
 
// ...
 
};


实例化foo的类型参数:
foo<ContainsAnotherType>();
那么,模板函数中T::iterator * iter;被编译器实例化为ContainsAnotherType::iterator * iter;
这是一个静态成员变量而不是类型,那么这便成了一个乘法表达式,只不过iter在这里没有定义,编译器会报错:
error C2065: ‘iter’ : undeclared identifier

所以:直接告诉编译器T::iterator是类型而不是变量,只需用typename修饰:

template <class T>
 
void foo() {
 
typename T::iterator * iter;
 
// ...
 
}

使用typename的规则

模板定义之外,即typename只能用于模板的定义中
非限定类型,比如前面介绍过的int,vector之类
基类列表中,比如template class C1 : T::InnerType不能在T::InnerType前面加typename
构造函数的初始化列表中

  • eg:对于不会引起歧义的情况,仍然需要在前面加typename
template <class T>
 
void foo() {
 
typename T::iterator iter;
 
// ...
 
}
template <class T>
 
void foo() {
 
typedef typename T::iterator iterator_type;
 
// ...
 
}
  • eg:将__type_traits<T>这个模板类中的has_trivial_destructor嵌套类型定义一个叫做trivial_destructor的别名,清晰明了。
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;

5.理解右值:

右值

即将消失的,不长时间存在于内存中的值

引用又称为左值(l-value)。

  • lvalue 是 loactor value 的缩写,rvalue 是 read value 的缩写
  • 左值通常对应着一个长时间存在于内存中的变量。

右值(r-value)。

#include <cstdio>

void test(int &) {
    printf("int &\n");
}

void test(int const &) {
    printf("int const &\n");
}

void test(int &&) {
    printf("int &&\n");
}

int main() {
    int a = 0;
    int *p = &a;
    test(a);      // int &
    test(*p);     // int &
    test(p[a]);   // int &
    test(1);      // int &&
    test(a + 1);  // int &&
    test(*p + 1); // int &&

    const int b = 3;
    test(b);      // int const &
    test(b + 1);  // int &&
}

(1)const:只读变量

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

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

唯一特殊之处: C++ 规定 int && 能自动转换成 int const &(万能引用类型),但不能转换成 int &。

  • 例如,尽管 3 是右值 int &&,但却能传到类型为 int const & 的参数上:
void func(int const &i);//OK
void func(int &i);//error

func(3);
就会报错。
  • 查看类型名的小工具
#include <iostream>
#include <cstdlib>
#include <string>
#include <type_traits>
#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()
{
    SHOW(int);
    SHOW(const int &);
    typedef const float *const &MyType;
    SHOW(MyType);

    int &&a = 1;
    SHOW(decltype(a));
}


  • 测试
    在这里插入图片描述

(2)获取变量和表达式的类型:decltype

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

  • eg:03/3_auto/07/main.cpp
#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 &。

  • eg:03/3_auto/08/main.cpp
#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, *p;
    SHOW(decltype(3.14f + a));
    SHOW(decltype(42));
    SHOW(decltype(&a));
    SHOW(decltype(p[0]));
    SHOW(decltype('a'));

    SHOW(decltype(a));    // int
    SHOW(decltype((a)));  // int &
    // 后者由于额外套了层括号,所以变成了 decltype(表达式)
}
  • 测试:
    在这里插入图片描述

(3)自动类型推导:万能推导(decltype(auto))

如果一个表达式,我不知道他是个可变引用(int &),常引用(int const &),右值引用(int &&),还是一个普通的值(int)。
但我就是想要定义一个和表达式返回类型一样的变量,这时候可以用:

  • 会自动推导为 func() 的返回类型。
decltype(auto) p = func();
等价于
decltype(func()) p = func();
  • eg:03/3_auto/09/main.cpp
#include <cstdio>

int t;

int const &func_ref()
{
    return t;
}

int &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
}

  • 在代理模式中,用于完美转发函数返回值。
decltype(auto) at(size_t i) const {
  return m_internal_class.at(i);/返回值是常引用,也可能是可变引用,所以这么写}

(4)using:创建类型别名

  • 除了 typedef 外,还可以用 using 创建类型别名:
typedef std::vector<int> VecInt;
using VecInt = std::vector<int>;
以上是等价的。


typedef int (*PFunc)(int);
using PFunc = int(*)(int);
以上是等价的。

decltype:一个例子

  • 模板函数的部分特化,03/3_auto/10/main.cpp
    这是一个实现将两个不同类型 vector 逐元素相加的函数。
  • 用 decltype(T1{} * T2{}) 算出 T1 和 T2 类型相加以后的结果,并做为返回的 vector 容器中的数据类型。
#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;
}

6.函数即对象

(1)函数作为另外一个函数的参数

函数也是对象:函数式编程

  • 函数可以作为另一个函数的参数
#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形参的函数参数与实际函数的类型匹配,可以之间调用实际函数,下面的写法其实等价于函数指针
#include <cstdio>

void print_number(int n) {
    printf("Number %d\n", n);
}

void call_twice(void func(int)) {
    func(0);
    func(1);
}

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

  • 测试:
    在这里插入图片描述

(2)函数作为模板类型

甚至可以直接将 func 的类型作为一个模板参数,从而不需要写 void(int)。
这样还会允许函数的参数类型为其他类型,比如 void(float)。
这样 call_twice 会自动对每个不同的 func 类型编译一遍,从而允许编译器更好地进行自动适配与优化。

  • eg:03/4_lambda/03/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

(3)lambda表达式

C++11 引入的 lambda 表达式允许我们在函数体内创建一个函数,大大地方便了函数式编程。

  • 语法就是先一个空的 [],然后是参数列表,然后是 {} 包裹的函数体。
  • 等价于全局的myfunc,好处是全局名字不会冲突
  • eg:
#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表达式:返回类型

  • lambda 表达式的返回类型写在参数列表后面,用一个箭头 -> 表示。
#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;
}
  • 测试:
    在这里插入图片描述

lambda表达式:自动推导返回类型

  • 如果 lambda 表达式不通过 -> 指定类型,则和 -> auto 等价,自动根据函数体内的 return 语句决定返回类型,如果没有 return 语句则相当于 -> void。
#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) {
        return n * 2;  // 返回类型自动推导为 int
    };
    call_twice(twice);
    return 0;
}

  • 测试:
    在这里插入图片描述

lambda表达式:捕获main中的变量

  • lambda 函数体中,还可以使用定义他的 main 函数中的变量,只需要把方括号 [] 改成 [&] 即可:
  • 函数可以引用定义位置所有的变量,这个特性在函数式编程中称为闭包(closure)。
  • eg:03/4_lambda/07/main.cpp
#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;
    auto twice = [&] (int n) {
        return n * fac;
    };
    call_twice(twice);
    return 0;
}

  • 测试:
    在这里插入图片描述

lambda表达式:修改main中的变量

  • [&] 不仅可以读取 main 中的变量,还可以写入 main 中的变量,比如可以通过 counter++ 记录该函数被调用了多少次:
  • eg:03/4_lambda/08/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

lambda表达式:传常引用避免拷贝开销

  • 最好把模板参数的 Func 声明为 Func const & 以避免不必要的拷贝:
  • eg:03/4_lambda/09/main.cpp
#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;
}

为什么 Func 的大小是 16 字节?

  • 提示:一个指针大小为 8 字节,捕获了 2 个指针(fac指针+counter指针)。

  • 测试:
    在这里插入图片描述

lambda表达式:作为返回值

  • 既然函数可以作为参数,当然也可以作为返回值!
    由于 lambda 表达式永远是个匿名类型,我们需要将 make_twice 的返回类型声明为 auto 让他自动推导。
  • 所以lambda是无法声明的
  • eg:03/4_lambda/10/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

  • 错误做法:03/4_lambda/11/main.cpp

#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(int fac) {
    return [&] (int n) {
        return n * fac;
    };
}

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

  • 测试
    在这里插入图片描述

然而当我们试图用 [&] 捕获参数 fac 时,却出了问题:
fac 似乎变成 32764 了?
这是因为 [&] 捕获的是引用,是 fac 的地址,而 make_twice 已经返回了,导致 fac 的引用变成了内存中一块已经失效的地址。(不要返回局部变量的指针,C语言知识)
总之,如果用 [&],请保证 lambda 对象的生命周期不超过他捕获的所有引用的寿命。

  • 错误问题的解决办法,03/4_lambda/12/main.cpp
#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(int fac) {
    return [=] (int n) {
        return n * fac;
    };
}

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

  • 测试:
    在这里插入图片描述

lambda表达式:如何避免用模板参数?

虽然\ <class Func> 这样可以让编译器对每个不同的 lambda 生成一次,有助于优化。
但是有时候我们希望通过头文件的方式分离声明和实现,或者想加快编译,这时如果再用 template class 作为参数就不行了。
为了灵活性,可以用 std::function 容器。

  • 只需在后面尖括号里写函数的返回类型和参数列表即可,比如:
std::function<int(float, char *)>;

开销较大,里面有虚表指针等(开销和使用虚函数是一样的),所以大小是32
std::function 容器利用了类型擦除技术,把一切()形式调用的函数变成一个虚函数
std::any能将具体类型擦除,只保留:拷贝、析构、移动作为虚函数

  • eg:03/4_lambda/13/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

如何避免用模板参数:将无捕获的 lambda 可以传为函数指针

另外,如果你的 lambda 没有捕获任何局部变量,也就是 [],那么不需要用 std::function<int(int)>,直接用函数指针的类型 int(int) 或者 int(*)(int) 即可。
函数指针效率更高一些,但是 [] 就没办法捕获局部变量了(全局变量还是可以的)。
最大的好处是可以伺候一些只接受函数指针的 C 语言的 API 比如 pthread 和 atexit。

  • 形参调用无捕获的lambda ,无捕获的lambda退化成一个函数指针,func指向call_twice函数的地址,

  • 无捕获的lambda就是无状态的lambda

  • eg:03/4_lambda/14/main.cpp

#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;
}

  • 测试:
    在这里插入图片描述

(4)lambda + 模板

Lambda变成一个模板函数

  • 可以将 lambda 表达式的参数声明为 auto,声明为 auto 的参数会自动根据调用者给的参数推导类型
  • C++14以上才支持
  • auto const & 也是同理,等价于模板函数的 T const &。
  • 带 auto 参数的 lambda 表达式,和模板函数一样,同样会有惰性、多次编译的特性。
  • 但是这种Lambda模板函数不能部分特化,即:函数模板中twice(T n) 不能变成vector
  • eg:03/4_lambda/15/main.cpp
#include <iostream>
#include <functional>
template <typename T>
void call_twice(const T &func)
{
    std::cout << func(3.14f) << std::endl;
    std::cout << func(12) << std::endl;
    std::cout << "Func 大小: " << sizeof(func) << std::endl;
}

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

  • 测试:
    在这里插入图片描述

(5)C++20前瞻:函数也可以 auto,lambda 也可以

  • eg:03/4_lambda/16/main.cpp
#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;
}
*/

  • 测试:
    在这里插入图片描述

(6)lambda 用途举例:yield模式

  • 这里用了 type_traits 来获取 x 的类型。

decay_t<int const &> = int,
decay_t可以去掉&,也可以去掉const,也可以去掉数组[]
is_same_v<int, int> = true
is_same_v<float, int> = false

  • 由于lambda参数带有auto,这里会实例化两个lambda函数
  • eg:03/4_lambda/17/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

(7)lambda 用途举例:立即求值

#include <iostream>
#include <vector>

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

  • 测试:
    在这里插入图片描述

(8)lambda 用途举例:局部实现递归

搜索关键字:匿名递归返回值类型必须是void

  • eg:03/4_lambda/19/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

(9)小结

lambda 作为参数:用 template 然后 Func const & 做类型。

  • lambda 作为参数:通常用 [&] 存储引用。
template <class Func>
void fetch_data(Func const &func) {
    for (int i = 0; i < 32; i++) {
        func(i);
        func(i + 0.5f);
    }
}

lambda 作为返回值:用 auto 做类型。

#include <iostream>
#include <functional>
template <typename T>
void call_twice(const T &func)
{
    std::cout << func(3.14f) << std::endl;
    std::cout << func(12) << std::endl;
    std::cout << "Func 大小: " << sizeof(func) << std::endl;
}

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

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

  • 牺牲性能但存储方便:std::function 容器。
#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;
}

(10)如何逐渐优化成一个模板函数?

#include <cstdio>

struct print_t {
    void operator()(float n) {
        printf("Float %f\n", n);
    }

    void operator()(int n) {
        printf("Int %d\n", n);
    }
};
print_t print;

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

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

#include <cstdio>

struct print_t {
    void operator()(float n) const {
        printf("Float %f\n", n);
    }

    void operator()(int n) const {
        printf("Int %d\n", n);
    }
};
print_t print;

template <class Func>
void call_twice(Func const &func) {
    func(0);
    func(1);
    func(3.14f);
}

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

#include <iostream>

struct print_t {
    template <class T>
    void operator()(T const &t) const {
        std::cout << t << std::endl;
    }
};
print_t print;

template <class Func>
void call_twice(Func const &func) {
    func(0);
    func(1);
    func(3.14f);
    func("Hello");
}

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

  • 测试:
    在这里插入图片描述

7.常用容器:tuple

std::tuple<…> 可以将多个不同类型的值打包成一个。尖括号里填各个元素的类型。

  • 之后可以用 std::get<0> 获取第0个元素,std::get<1> 获取第1个元素,以此类推(从0开始数数)。
  • eg:03/5_containers/01/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

(1)C++17 的新特性:CTAD

当用于构造函数时,std::tuple<…> 尖括号里的类型可以省略

  • 通过 auto 自动推导 get 的返回类型。
  • eg:03/5_containers/02/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

(2)tuple:结构化绑定

可是需要一个个去 get 还是好麻烦。

  • 可以用结构化绑定的语法:

auto [x, y, …] = tup;
利用一个方括号,里面是变量名列表,即可解包一个 tuple。里面的数据会按顺序赋值给每个变量

  • eg:03/5_containers/03/main.cpp
#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;
}

tuple:结构化绑定为引用

  • 结构化绑定也支持绑定为引用:

auto &[x, y, …] = tup;
这样相当于解包出来的 x, y, … 都是 auto & 推导出来的引用类型。对引用的修改可以影响到原 tuple 内的值。
同理,通过 auto const & 绑定为常引用:
auto const &[x, y, …] = tup;
常引用虽然不能修改,但是可以避免一次不必要拷贝。

  • eg:03/5_containers/04/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

tuple:结构化绑定为万能推导

不过要注意一下万能推导的 decltype(auto),由于历史原因,他对应的结构化绑定是 auto &&:
auto &&[x, y, …] = tup; // 正确!
decltype(auto) [x, y, …] = tup; // 错误!

  • eg:03/5_containers/04/main.cpp
#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::get 并不支持自定义类。
  • eg:03/5_containers/05/main.cpp
#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;
}

(3)tuple:用于函数多个返回值

std::tuple 可以用于有多个返回值的函数。

当函数返回值确定时,return 可以用 {} 表达式初始化,不必重复写前面的类名 std::tuple。

  • eg:03/5_containers/06/main.cpp
#include <iostream>
#include <tuple>
#include <cmath>

std::tuple<bool, float> mysqrt(float x) {
    if (x >= 0.f) {
        return {true, std::sqrt(x)};
    } else {
        return {false, 0.0f};
    }
}

int main() {
    auto [success, value] = mysqrt(3.f);
    if (success) {
        printf("成功!结果为:%f\n", value);
    } else {
        printf("失败!找不到平方根!\n");
    }
    return 0;
}

  • 测试:
    在这里插入图片描述

(4)常用容器:optional

有些函数,本来要返回 T 类型,但是有可能会失败!
上个例子中用 std::tuple<bool, T>,其中第一个 bool 表示成功与否。但是这样尽管失败了还是需要指定一个值 0.0f,非常麻烦

这种情况推荐用 std::optional。
成功时,直接返回 T。失败时,只需返回 std::nullopt 即可。

optional:operator*() 不检测是否为空,不会抛出异常

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

  • 请确保在 has_value() 的分支内使用 *ret,否则就是不安全的。

  • 如果 optional 里的类型是结构体,则也可以用 ret->xxx 来访问该结构体的属性。

  • eg:03/5_containers/08/main.cpp

#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;
}
  • 测试:
    在这里插入图片描述

optional:operator bool() 和 has_value() 等价

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

这样看来 optional 是在模仿指针,nullopt 则模仿 nullptr。但是他更安全,且符合 RAII 思想,当设为 nullopt 时会自动释放内部的对象。
利用这一点可以实现 RAII 容器的提前释放。和 unique_ptr 的区别在于他的对象存储在栈上,效率更高。
Std::nullptr_t不是一个指针,而是一个特殊的类型,他可以转换为任何指针
Std::nullopt唯一的作用是作为构造函数的重载,能够初始化为空,他的价值是他的价值与众不同,其实现大致如下:

  • eg:03/5_containers/09/main.cpp
#include <iostream>
#include <optional>
#include <cmath>
#include <type_traits>

struct nollopt_t
{
    /* data */
};

template <typename T>
struct optional
{
    optional(nollopt_t) {}
    optional(T) {}
};

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)
    {
        printf("成功!结果为:%f\n", *ret);
    }
    else
    {
        printf("失败!找不到平方根!\n");
    }

    std::cout << std::is_pointer_v<std::nullptr_t> << std::endl;
    int *p = nullptr;

    std::cout << std::is_pointer_v<decltype(p)> << std::endl;
    return 0;
}


  • 测试:
    在这里插入图片描述

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

ret.value_or(3) 等价于:
ret.has_value() ? ret.value() : 3

  • eg:03/5_containers/09/main.cpp
#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(142.111f));
    return 0;
}

  • 测试:
    在这里插入图片描述

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

  • 当 ret 没有值时(即 nullopt),ret.value() 会抛出一个异常,类型为 std::bad_optional_access。
#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;
}

  • 测试:
    在这里插入图片描述

8.variant:安全的 union

variant:安全的 union,存储多个不同类型的值

有时候需要一个类型“要么存储 int,要么存储 float”,这时候就可以用 std::variant<int, float>。

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

  • 给 variant 赋值只需用普通的 = 即可。

variant 的特点是只存储其中一种类型。
tuple 的特点是每个类型都有存储。

variant:获取容器中的数据用 std::get

要获取某个类型的值,比如要获取 int 用 std::get。如果当前 variant 里不是这个类型,就会抛出异常:std::bad_variant_access。
此外,还可以通过 std::get<0> 获取 variant 列表中第 0 个类型,这个例子中和 std::get 是等价的。

  • eg:03/5_containers/10/main.cpp
#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;
}

variant:判断当前是哪个类型用 std::holds_alternative

  • 可以用 std::holds_alternative 判断当前里面存储的是不是 int。
  • eg:03/5_containers/11/main.cpp
#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;
}

  • 测试:
    在这里插入图片描述

variant:判断当前是哪个类型用 v.index()

  • 可以用成员方法 index() 获取当前是参数列表中的第几个类型。这样也可以实现判断。
  • eg:03/5_containers/12/main.cpp
#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;
}

variant:批量匹配 std::visit

  • 如果你的 if-else 每个分支长得都差不多(除了 std::get<> 的类型不一样以外),可以考虑用 std::visit,他会自动用相应的类型,调用你的 lambda,lambda 中往往是个重载函数。
  • 这里用到了带 auto 的 lambda,利用了他具有多次编译的特性,实现编译多个分支的效果。

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

  • eg:
#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;
}

std::visit:还支持多个参数

其实还可以有多个 variant 作为参数。
相应地 lambda 的参数数量要与之匹配。
std::visit 会自动罗列出所有的排列组合!
所以如果 variant 有 n 个类型,那 lambda 就要被编译 n² 次,编译可能会变慢。
但是标准库能保证运行时是 O(1) 的(他们用函数指针实现分支,不是暴力 if-else)

#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;
}

std::visit:可以有返回值

std::visit里面的 lambda 可以有返回值,不过都得同样类型。
利用这一点进一步优化:
std:visit要指定返回值

#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;
}

  • eg:其他type_traits:
    错误做法:
#include <iostream>
#include <vector>

template <class T>
auto sum(T const &arr) {
    return res;
}

int main() {
    std::vector<int> arr = {4, 3, 2, 1};
    auto res = sum(arr);
    std::cout << res << std::endl;
    return 0;
}

正确做法:

#include <iostream>
#include <vector>

template <class T>
auto sum(T const &arr) {
    typename T::value_type res = 0;
    for (auto &&x: arr) {
        res += x;
    }
    return res;
}

int main() {
    std::vector<int> arr = {4, 3, 2, 1};
    auto res = sum(arr);
    std::cout << res << std::endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜欢打篮球的普通人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值