1. lambda表达式(C11)

1.1 lambda表达式的组成

[=]/*1*/ ()/*2*/ mutable/*3*/ throw()/*4*/ -> int/*5*/ {}/*6*/
  • capture子句
  • 参数列表(optional)
  • 可变规范(optional)
  • 异常定义(optional)
  • 返回类型(optional)
  • 函数体

1.2 即看即用

语法:

[capture](parameters)->return-type {body}

[]叫做捕获说明符

parameters参数列表

->return-type表示返回类型,如果没有返回类型,则可以省略这部分。

我们可以这样输出”hello,world”

auto func = [] () { cout << "hello,world"; };  
func(); // now call the function

变量捕获与lambda闭包实现

string name;  
cin >> name;  
[&](){cout << name;}();

lambda函数能够捕获lambda函数外的具有自动存储时期的变量。函数体与这些变量的集合合起来叫闭包。

[ ] 不截取任何变量
[&} 截取外部作用域中所有变量,并作为引用在函数体中使用
[=] 截取外部作用域中所有变量,并拷贝一份在函数体中使用

[=, &foo] 截取外部作用域中所有变量,并拷贝一份在函数体中使用,但是对foo变量使用引用

[bar] 截取bar变量并且拷贝一份在函数体重使用,同时不截取其他变量

[x, &y] x按值传递,y按引用传递

[this] 截取当前类中的this指针。如果已经使用了&或者=就默认添加此选项。

1.3 基本概念和用法

c++中,普通的函数是长这样子的:

ret_value function_name(parameter) option { function_body; }

比如:

int get_value(int a) const {return a++;}

lambda表达式定义了一个匿名函数,并且可以捕获所定义的匿名函数外的变量。它的语法形式是:

[ capture ] ( parameter ) option -> return_type { body; };

其中:

  • capture 捕获列表
  • parameter 参数列表
  • option 函数选项
  • return_type 函数返回值类型
  • body 函数体

比如:

// defination
auto lamb = [](int a) -> int { return a++; };

// usage
std::cout << lamb(1) << std::endl;  // output: 2

组成lambda的各部分并不是都必须存在的:

  1. 当编译器可以推导出返回值类型的时候,可以省略返回值类型的部分
auto lamb = [](int i){return i;};   // OK, return type is int
auto lamb2 = [] () {return {1, 2};};    // Error
  1. lambda表达式没有参数时,参数列表可以省略
auto lamb = []{return 1;};      // OK

所以一个最简单的lambda表达式可以是下面这样,这段代码是可以通过编译的:

int main()
{
  []{};     // lambda expression
  return 0;
}

捕获列表

捕获列表是lambda表达式和普通函数区别最大的地方。[]内就是lambda表达式的捕获列表。一般来说,lambda表达式都是定义在其他函数内部,捕获列表的作用,就是使lambda表达式内部能够重用所有的外部变量。捕获列表可以有如下的形式:

  • [] 不捕获任何变量
  • [&]引用方式捕获外部作用域的所有变量
  • [=]赋值方式捕获外部作用域的所有变量
  • [=, &foo] 以赋值方式捕获外部作用域所有变量,以引用方式捕获foo变量
  • [bar] 以赋值方式捕获bar变量,不捕获其它变量
  • [this] 捕获当前类的this指针,让lambda表达式拥有和当前类成员同样的访问权限,可以修改类的成员变量,使用类的成员函数。如果已经使用了&或者=,就默认添加此选项。

捕获列表示例:

#include <iostream>

class TLambda
{
public:
    TLambda(): i_(0) { }
    int i_;

    void func(int x, int y) {
        int a;
        int b;

        // 无法访问i_, 必须捕获this,正确写法见l4
        auto l1 = [] () {
            return i_;
        };

        // 以赋值方式捕获所有外部变量,这里捕获了this, x, y
        auto l2 = [=] () {
            return i_ + x + y;
        };

        // 以引用方式捕获所有外部变量
        auto l3 = [&] () {
            return i_ + x + y;
        };

        auto l4 = [this] () {
            return i_;
        };

        // 以为没有捕获,所以无法访问x, y, 正确写法是l6
        auto l5 = [this] () {
            return i_ + x + y;
        };

        auto l6 = [this, x, y] () {
            return i_ + x + y;
        };

        auto l7 = [this] () {
            return ++i_;
        };

        // 错误,没有捕获a变量
        auto l8 = [] () {
            return a;
        };

        // a以赋值方式捕获,b以引用方式捕
        auto l9 = [a, &b] () {
            return a + (++b);
        };

        // 捕获所有外部变量,变量b以引用方式捕获,其他变量以赋值方式捕获
        auto l10 = [=, &b] () {
            return a + (++b);
        }

    }
};


int main()
{
    TLambda a;
    a.func(3, 4);

    return 0;
}

引用和赋值,就相当于函数参数中的按值传递和引用传递。如果lambda捕获的变量在外部作用域改变了,以赋值方式捕获的变量则不会改变。按值捕获的变量在lambda表达式内部也不能被修改,如果要修改,需使用引用捕获,或者显示的指定lambda表达式为 mutable

// [=]
int func(int a);

// [&]
int func(int& a);

// ------------------------------------------

int a = 0;
auto f = [=] {return a;};   // 按值捕获
a += 1;                    // a被修改
cout << f() << endl;        // output: 0

// ------------------------------------------

int a = 0;
auto f = [] {return a++;};      // Error
auto f = [] () mutable {return a++;}; // OK

1.4 lambda表达式的类型

上面一直使用auto关键字来自动推导lambda表达式的类型,那作为强类型语言的c++,这个lambda表达式到底是什么类型呢?lambda的表达式类型在c++11中被称为『闭包类型(Closure Tyep)』,是一个特殊的、匿名的、非联合(union)、非聚合(aggregate)的类类型。可以认为它是一个带有operator()的类,即防函数(functor)。因此可以使用std::functionstd::bind来存储和操作lambda表达式。

std::function<int (int)> f = [](int a) {return a;};
std::function<int (int)> f = std::bind([](int a){return a;}, std::placeholders::_1);
std::cout << f(22) << std::endl;

对于没有捕获任何变量的lambda,还可以转换成一个普通的函数指针。

using func_t = int (*)(int); 
func_t f = [](int a){return a;};
std::cout << f(22) << std::endl;

2. if的高级写法

if (a!=b,b!=c,a!=c)

C++的if语句使用逗号表达式,逗号表达式与加减乘除本质上是一样的, 它的求值是从左向右依次对表达式求值,
整个表达式的结果取逗号表达式中最后一个表达的的结果, 如果非零, 就会使 if 成立!
上面相当于:

a!=b;
b!=c;
if (a!=c)

3. 左值与右值(C11)

本质上说,左值是可以被寻址的值,即左值一定对应内存中的一块区域,而右值既可以是内存中的值,也可以是寄存器中的一个临时变量。一个表达式被用作左值时,使用的是它的地址,而被用作右值时,使用的是它的内容。

在cpp11中,右值又可以进一步分为纯右值(pure rvalue)和将亡值(expiring value);其中纯右值指的是临时变量或者不和变量关联的字面量;其中临时变量包括非引用类型的函数返回值,比如 int f()的返回值,和表达式的结果,比如(a+b)结果就是一个临时变量;字面量比如10,“abc”这些。将亡值是cpp11新增的,适合右值引用相关的概念,包括返回右值引用T&&的函数的返回值,std::move的返回值

如何准确的判断一个表达式是不是左值呢?

  • 如果这个表达式可以出现在等号的左边,一定是左值;
  • 如果可以对一个表达式使用&符号去地址,它一定是左值;
  • 如果它“有名字”,则一定是左值;

形如 const T&的常量左值引用是个万金油的引用类型,其可以引用非常量左值,常量左值和右值。而形如T&的非常量左值引用只能接受非常量左值对其进行初始化。

int &a = 2;       # 非常量左值引用绑定到右值,编译失败
int b = 2;        # 非常量左值变量
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2;  # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2;  # 常量左值引用绑定到右值,编程通过

右值引用通常不能绑定任何左值,如果要绑定左值,需要使用std::move()将左值转换为右值;

int a;
int&& r1 = a; //非法,a是左值;
int&& r2 = std::move(a); //合法,通过std::move()将左值转换为右值。移动语义是C++11新增的重要功能,其重点是对右值的操作。右值可以看作程序运行中的临时结果,右值引用可以避免复制提高效率

下表列出了在C++11中各种引用类型可以引用的值的类型。值得注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。
在这里插入图片描述

4. 重载匹配

在这里插入图片描述

5. costexpr

5.1 costexpr变量

一般来说,在日益复杂的系统中确定变量的初始值到底是不是常量表达式并不是一件容易的事情。为了解决这个问题C++11允许将变量声明为costexpr类型以便由编译器验证变量的值是否是一个常量表达式

变量声明为constexpr类型,就意味着一方面变量本身是常量,也意味着它必须用常量表达式来初始化:

constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr float y{108};

constexpr int i; // Error! Not initialized
int j = 0;
 constexpr int k = j + 1; //Error! j not a constant expression

如果初始值不是常量表达式,就会发生编译错误

5.2 costexpr函数

除了能用常量表达式初始化constexpr变量声明为,还可以使用consstexpr函数。它是指能用于常量表达式的函数,也就是它的计算结果可以在编译时确定。

例如下面的计算阶乘的constexpr函数。

constexpr long long factorial(int n){

   return n <= 1? 1 : (n * factorial(n - 1));

}

constexpr long long f18 = factorial(20);

可以用它来初始化constexp变量。所有计算都在编译时完成,比较有趣的是像溢出这样的错误也会在编译期检出

定义的方法就是在返回值类型前加constexpr关键字。但是为了保证计算结果可以在编译期就确定,函数体中只能有一条语句,而且该语句必须是return语句,因此,下面:

constexpr int data(){
    const i = 1;
    return i;
}

无法通过编译。不过一些不会产生实际代码的语句在常量表达式函数的使用下,可以通过编译,比如说static_assert,using、typedef之类的。因此,下面:

constexpr int f(int x){
    static_assert(0 == 0, "assert fail.");
    return x;
}

可以通过编译。

  • 在使用前必须已经有定义。

    5.3 if constexpr

我们知道,constexpr将表达式或函数编译为常量结果。一个很自然的想法是,如果我们把这一特性引入到条件判断中去,让代码在编译时就完成分支判断,岂不是能让程序效率更高。C++17支持if constexpr(表达式),允许在代码中声明常量表达式的判断条件,如下:

#include <iostream>

template<typename  T>
auto print_type_info(const T & t){
    if constexpr (std::is_integral<T>::value){
        return  t + 1;
    }else{
        return t + 0.01;
    }
}

int main() {
    std::cout << print_type_info(5) << "\n";
    std::cout << print_type_info(5.5) << "\n";
}

在这里插入图片描述

6. Cpp函数指针

在Cpp中,函数名不可以作为形参或者返回参数,但是函数指针可以

#include <iostream>
#include<initializer_list>

using namespace std;

void show(string s, int i) {
    while (i--) cout << s << endl;
}

// 返回函数指针
// decltype返回的是函数类型,而函数只能返回函数指针
decltype(show) *getShow() {
    return show;
}

auto getShow2() -> void (*)(string, int) {
    return show;
}

// F是函数名
typedef decltype(show) F;
// PF是函数指针
typedef decltype(show) *PF;

using FF = decltype(show);
using PFF = decltype(show) *;

using FFF = void(string, int);
using PFFF = void (*)(string, int);

void recvFunc(void(*f)(string, int), string s, int i) {
    f(s, i);
}

void recvFunc2(PFF f, string s, int i) {
    f(s, i);
}

void recvFunc3(F *f, string s, int i) {
    f(s, i);
}


int main() {
    void (*f1)(string, int);
    void (*f2)(string, int);
    // 对于函数指针,show 和 &show是等价的
    f1 = show;
    f2 = &show;
    f1("Hello", 1);
    f2("World !", 2);


    F *f3 = show;
    PF f4 = show;
    f3("Hello", 1);
    f4("World !", 2);


    FF *f5 = show;
    PFF f6 = show;
    f5("Hello", 1);
    f6("World !", 2);

    recvFunc(show, "Do!", 3);
    recvFunc2(show, "HA!", 3);
    recvFunc3(show, "LA!", 3);

    FFF *f7 = show;
    PFFF f8 = show;
}

7. std::string_view(C17)

std::string_view是C++ 17标准中新加入的类,正如其名,它提供一个字符串的视图,即可以通过这个类以各种方法“观测”字符串,但不允许修改字符串。由于它只读的特性,它并不真正持有这个字符串的拷贝,而是与相对应的字符串共享这一空间。即——构造时不发生字符串的复制。同时,你也可以自由的移动这个视图,移动视图并不会移动原定的字符串。

const char *cstr_pointer = "pointer";
char cstr_array[] = "array";
std::string stdstr = "std::string";

std::string_view
    sv1(cstr_pointer, 5),  // 使用C风格字符串-指针构造,并指定大小为5
    sv2(cstr_array),    // 使用C风格字符串-数组构造
    sv3("123456", 4),   // 使用字符串字面量构造,并指定大小为4
    sv4(stdstr),        // 使用std::string构造
    sv5("copy"sv);      // 使用拷贝构造函数构造(sv是std::string_view字面量的后缀)

std::cout
    << sv1 << endl    // point
    << cstr_pointer << endl // pointer
    << sv2 << endl    // array
    << sv3 << endl    // 1234
    << sv4 << endl    // std::string
    << sv5 << endl;   // copy

7.1 span(C20)

std::span是指向一组连续的对象的对象, 是一个视图view, 不是一个拥有者owner

一组连续的对象可以是 C 数组, 带着大小的指针, std::array, 或者std::string

 #include <ranges>
 #include <vector>
 #include <iostream>
 #include <span>
 #include <format>
 ​
 void printSpan(std::span<int> container)
 {
     std::cout << std::format("container size: {} \n", container.size());
     for (auto ele : container)
     {
         std::cout << ele << " ";
     }
     std::cout << std::endl;
     std::cout << std::endl;
 }
 ​​
 int main()
 {
     std::vector v1{1, 2, 3, 4, 5, 8};
     std::vector v2{9, 2, 4, 2, 6, 78};
 ​
     std::span<int> dynamicSpan(v1);
     std::span<int, 6> staticSpan(v2);
 ​
     printSpan(dynamicSpan);
     printSpan(staticSpan);
 }

8. stack、queue和priority_queue

8.1 stack(栈)

首先,你得写个头文件:

#include <stack>

那么如何定义一个栈呢?

stack <类型> 变量名

接下来是一些关于栈的基本操作~

stack <int> s;(以这个为例子)

1.把元素a加入入栈:s.push(a);
2.删除栈顶的元素:s.pop();
3.返回栈顶的元素:s.top();
4.判断栈是否为空:s.empty();(为空返回TRUE)
5.返回栈中元素个数:s.size();
6.把一个栈清空:(很抱歉没有这个函数,你得写这些:)

while (!s.empty())
    s.pop();

8.2 queue

queueq;创建一个int型空队列q
q.empty();判断队列是否为空,为空返回true
q.push(s);将变量s从队尾入队
q.pop();将队头元素弹出
q.front();只返回队头元素
q.back();只返回队尾元素
q.size()返回队列中元素个数

queue从队首弹出,先入先出

8.3 deque

deque dq;创建一个数双端队列dq
dq.empty();判断队列是否为空,为空返回true
dq.push_front(s);将s从队头入队
dq.push_back(s);将s从队尾入队,和普通队列方式一样
dq.front();只返回队头元素
dq.back();只返回队尾元素
dq.pop_front();将队头元素弹出
dq.pop_back;将队尾元素弹出
dq.clear();将队列清空

queue可以访问两端但是只能修改队头

8.4 priority_queue

push() 将一个元素置于priority queue中
top() 返回priority queue中的“下一个元素”
pop() 从priority queue 中移除一个元素
注意:如果priority queue 内没有元素,执行top()和pop()会导致未定义的行为,应该先采用size()或empty()来检验容器是否为空。

9. ranges(C20)

9.1 视图(view)

视图的概念相信大家不陌生。在 Python 中,一个 numpyndarray 对象经常只有一个实体,经过切片操作后只会生成一个指向原实体的对象,这就是一个视图(view)。可见,视图的复制、移动等操作应该是常数时间,并且视图可能做到对实体的修改。

C++20 中,视图的引入正是建立在范围的基础上的。视图是一个概念,并且视图本身是一个范围,下面给出视图的完整定义:

// 摘自 cppreference
template<class T>
concept view = ranges::range<T> && std::semiregular<T> && ranges::enable_view<T>;

其中,ranges::enable_view<T> 表示 T 是一个空类 view_base 的派生类。而 std::semiregular<T> 表示 T 可复制亦可默认构造。

我们规定:view_base 类型的对象可以在常数时间进行复制、移动或赋值。如果不行,这个对象不应是 view_base 类型的对象。为了便于理解,我们称里一个类型满足视图概念的对象为一个可迭代视图

可迭代视图与 Python 中的生成器很相似:可迭代、惰性。标准库中内置了一些常用的可迭代视图,下面举一例作为引导。

程序 3:C++ 中的 range(这里的 range 指 Python 中的 range)

#include <iostream>
#include <ranges>
using namespace std;

int main()
{
    for (auto i : views::iota(0, 10))
        cout << i << " ";
}

程序:Python 中的 range

for i in range(10):
    print(i, end=' ')

下面我们来重点解析 std::views::iota 是什么。由于 IDE 的支持有限,我们只能先做猜测。有两种可能:

  1. iota 是一个类型,本身是一个可迭代视图。
  2. iota 是一个函数(或者仿函数),返回一个可迭代视图。

经查证,gcc 10.1 的实现中,iota 是一个仿函数,返回值是一个 iota_view 类型的可迭代视图,而 iota_viewbeginend 方法则返回一个合规的迭代器。

cppreference 中给出了基本完整的文档,可以看出,上面的验证是无误的。

9.2 范围适配器(range adaptor)

范围适配器的作用是得到一个可迭代对象(range)的可迭代视图(view)。范围适配器有两种形式2

  1. 将可迭代对象作为参数传入一个范围适配器(可见范围适配器是一个仿函数);
  2. 将可迭代对象作为二元运算符 | 的左操作数,将范围适配器作为右操作数,则表达式的结果是一个可迭代视图。其中 | 运算符的重载应该由范围适配器提供。

范围适配器只接受可视图化范围(viewable_range)。得益于概念的引入,viewable_range 有一个准确的定义:

template<class T>
  concept viewable_range =
    ranges::range<T> && (ranges::borrowed_range<T> || ranges::view<std::remove_cvref_t<T>>);

也就是说,一个类型如果是一个 viewable_range,仅当它是一个可迭代对象,并且它要么是一个 borrowed_range(见文档),要么本身就是一个视图。

有了以上概念,结合文档,我们可以写出以下程序。

程序 4:filter

#include <vector>
#include <ranges>
#include <iostream>

int main()
{
    auto ints = std::views::iota(0, 10);
    auto even = [](int i) { return 0 == i % 2; };
    auto square = [](int i) { return i * i; };

    for (int i : ints | std::views::filter(even) | std::views::transform(square))
        std::cout << i << ' ';
}

如何将任意可迭代对象转化为范围?可以使用 views::all。基本内容到此为止了吧……

9.3 局限性

C++20 标准中已有的范围适配器少得可怜。没有 zip,没有 enumerate

由于 C++ 是静态类型,因此很容易编译错误。