Cpp20-STL-Cookbook速查


概述

阅读时请配合cpp reference使用,同时请确保编译器支持C++20。在bilibili上有对应的讲解,这位up主更是为cpp高手,以下大部分内容均是这位up主的笔记,为了方便速查选择我常用的部分搬运了过来。

第一章 C++20的新特性

1.2格式化文本

#include<iostream>
#include<algorithm>
#include<string_view>
#include<format>

template < typename... Args>
void print(const std::string_view fmt_str, Args&&... args) {
   auto fmt_args{ std::make_format_args(args...) };
   std::string outstr{ std::vformat(fmt_str, fmt_args) };
   fputs(outstr.c_str(), stdout);
}

struct Frac {
   int a, b;
};

template<>
struct std::formatter<Frac> {
   template<typename ParseContext>
   constexpr auto parse(ParseContext& ctx) {
       return ctx.begin();
   }
   template<typename FormatContext>
   auto format(const Frac& f, FormatContext& ctx)const {
       return std::format_to(ctx.out(), "{0:d}/{1:d}", f.a, f.b);
   }
};

int main() {
   Frac f{ 1,10 };
   print("{}", f);
}
//特化规则参见:    https://zh.cppreference.com/w/cpp/named_req/Formatter
out: 1/10
  1. 实现模板函数 print

使用与std::format()函数相同的参数。第一个参数是格式字符串的 std::string_view 对象,后面作为参数 的可变参数包。

std::make_format_args() 函数的作用: 接受参数包并返回一个对象,该对象包含适合格式化的已擦除 类型的值。

fmt_str就是传递的格式化字符串,fmt_args是一个保有格式化参数的对象,使用std::vformat(fmt_str, fmt_args)即可返回格式化完毕的字符串。我们使用 fputs() 将值输出到控制台上 (这比 cout 高效得多)。

  1. std::formatter 特化

对于自定义,或者说标准没有对其有特化的类型,需要我们自行特化std::formatter才可以正确的格式化。

parse() 函数解析格式字符串,从冒号之后 (若没有冒号,则在开大括号之后) 直到但不包括结 束大括号 (就是指定对象类型的部分)。其接受一个 ParseContext对象,并返回一个迭代器。这里,可以只返回 begin() 迭代器。因为我们的类型不需要新语法,所以无需准备任何东西。

format() 函数接受一个 Frac 对象和一个 FormatContext 对象,返回结束迭代器format_to() 函数可使这变得很容易,其可以接受一个迭代器、一个格式字符串和一个参数包。本例中,参数包是 Frac 类的两个属性,分子和分母。 需要做的就是提供一个简单的格式字符串“{0}/{1}”以及分子和分母的值 (0 和 1 表示参数的 位置)。

1.3使用编译时constexpr std::vectorstd::string

#include<iostream>
#include<vector>
constexpr auto f() {
    std::vector<int>v{ 1,2,3 };
    return v;
}
constexpr auto f2() {
    int* p = new int{ 10 };
    //未delete解除分配
    return *p;
}

int main() {
    constexpr auto n = f().size();//√
    //constexpr auto n2 = f()//error
    //constexpr auto n3 = f2()//error
}

C++20 允许在新的上下文中使用 constexpr,这些语句可以在编译时计算,其中包括在 constexpr 上下文中使用 stringvector 对象的能力。所以 ,这些对象本身可能不声 明为 constexpr但可以在编译时上下文中使用,同时也可以使用STL中的算法。C++20 开始,标准 stringvector 类具有constexpr限定的构造函数和析构函数,这是可在编译时使用的 前提。所以,分配给 stringvector 对象的内存,也必须在编译时释放。注意该对象在运行时实际上是不可用的,就算能通过编译也无法使用。

1.4安全比较不同类型的整数cmp_less

#include<iostream>

template<class T,class U>
constexpr bool cmp_less(T t, U u)noexcept {
    using UT = std::make_unsigned_t<T>;//有符号类型到无符号类型的安全转换。
    using UU = std::make_unsigned_t<U>;
    if constexpr (std::is_signed_v <T> == std::is_signed_v<U>)
        return t < u;
    else if constexpr (std::is_signed_v<T>)
        return t < 0 ? true : UT(t) < u;
    else
        return u < 0 ? false : t < UU(u);
}
int main() {
    std::cout << std::boolalpha << (5u < -1) << '\n';//true
    std::cout << std::boolalpha << ::cmp_less(5u, 1) << '\n';//false
    std::cout << std::boolalpha << ::cmp_less(5u, 2u) << '\n';//false
}

C++20utility 引入了一组比较函数,他们分别是:

  • std::cmp_equal
  • std::cmp_not_equal
  • std::cmp_less
  • std::cmp_greater
  • std::cmp_less_equal
  • std::cmp_greater_equal

1.5三路比较运算符

三路比较运算符表达式的形式为表达式1 <=> 表达式2该表达式将返回一个对象

  • 表达式1 < 表达式2,则(表达式1 <=> 表达式2) < 0
  • 表达式1 > 表达式2,则(表达式1 <=> 表达式2) > 0
  • 表达式1 == 表达式2,则(表达式1 <=> 表达式2) == 0

每当< > <= >= <=>被比较且重载决议选择该重载时,operator<=>都会被调用

1.7概念(concept)和约束(constraint)-创建更安全的模板

重点内容,这个功能可以说让模板焕发新生。

#include<iostream>

template<std::integral T>
void f(T t) {}

template<class T>
requires std::integral<T> || std::is_pointer_v<T>
struct X {};

template <class T>
requires std::is_integral_v<T>
T n{};

template <class T>
concept love = std::is_integral_v<T> && (std::is_same_v<int, T> || std::is_same_v<uint32_t, T>);

void f2(love auto){}

int main() {
    f(1);            // 1 是 int,约束满足
    f('*');          // '*' 是整数类型(Integer Type)之一,约束满足
    //f(1.2);
    X<int> x;        // int 满足两个约束的析取之一:std::integral<T>,约束满足
    //X<double>x2;
    X<double*> x3;   // double* 满足两个约束的析取之一:std::is_pointer_t<T>,约束满足
    n<int> = 3;
    //n<double>;
    std::cout << n<int> << '\n';
    f2(1);           // 满足合取 std::is_integral_v<T> 和 std::is_same_v<int, T>
    f2(1u);          // 满足合取 std::is_integral_v<T>,std::is_same_v<uint32_t, T>
    //f2(1l);
}

//Requires表达式 https://zh.cppreference.com/w/cpp/language/requires
//约束与概念 https://zh.cppreference.com/w/cpp/language/constraints

作为 C++20 引入的四大新特性之一:Concept ,提出了一种比 SFINAE 更好的约束方法,它易于理解和编写,也能在出现问题时给出更可读的编译期报错。概念的定义形式如下:

template < 模板形参列表 > *concept *概念名 属性(可选) **= 约束表达式 ;

在上述例子中,概念 love 的定义就是这样:

template <class T>
concept love = std::is_integral_v<T> && (std::is_same_v<int, T> || std::is_same_v<uint32_t, T>);

requires 关键字可用于进行多个约束的分开表达,约束之间的关系均为合取,分为以下多种情况:

  • 简单约束
// 1. 简单约束
template <typename T>
concept Addable = requires(T a, T b){
    a + b;    //编译器会检查该表达式是否 "合法"
}
  • 类型约束
template <typename T>
struct tmp{
    using value = T;
};

template <typename T, typename = std::enable_if_t<std::is_same_v<T, V>>
struct test {};

template <typename T>
using Ref = T&;

template <typename T>
concept Cpt = requires
{
    typename T::value;    // 检查 T 是否存在成员 T::value
    typename X<T>         // 检查是否存在模板类 S 的特化 S<T>
    typename Ref<T>       // 检查是否存在合法别名模板 Ref<T>
}
  • 复合约束 复合约束用于约束表达式的返回类型。其定义为:

{ 表达式 } noexcept(可选) -> 类型约束 ;

例如:

template <typename T>
concept C = requires(T x) {
  {x * 2} -> typename T::inner;    // 表达式 x * 2 的类型可转换为 T::inner
  {x + 3} -> std::same_as<int>;    // 表达式 x + 3 需要满足约束 std::same_as<int>
};

1.8模块

1.8模块.cpp
import test;

int main() {
    /*int n[]{
#include"t.txt"
    };
    for (auto i : n) {
        std::cout << i << ' ';
    }*/

    std::cout << mylib::add(1, 2) << '\n';
    //mylib::print("*");
    t();
}

//模块: https://zh.cppreference.com/w/cpp/language/modules
//编译设置:add_executable (Test1 "src/1.8模块.cpp" "src/test.ixx" "src/test2.ixx")

test.ixx

module;
#define PI 3.14

export module test;
export import<iostream>;
export import test2;

namespace mylib {

    export auto add(std::integral auto a, std::integral auto b) {
        return a + b;
    }

    auto print(auto t) {
        std::cout << t << '\n';
    }
}

test2.ixx

export module test2;
import<iostream>;

export void t() {
    std::cout << "乐\n";
}

t.txt

1,2,3,4,5

1.9视图

#include<iostream>
#include<ranges>
#include<vector>
namespace stdv = std::views;
namespace stdr = std::ranges;

void print(stdr::range auto v) {
    for (const auto& i : v)std::cout << i << ' ';
    endl(std::cout);
}

int main() {
    std::vector nums{ 1,2,3,4,5,6,7,8,9,10 };
    auto ret = nums | stdv::take(5) | stdv::reverse;
    print(ret);
    auto ret2 = nums | stdv::filter([](int i) {return i > 6; });
    print(ret2);
    auto ret3 = nums | stdv::transform([](int i) {return i * i; });
    print(ret3);
    print(nums);//视图是不会改变原来的数据的

    std::vector<std::string>strs{ "🐴","🐭","🥵","🤣" };
    auto ret4 = strs | stdv::reverse;
    print(ret4);

    auto ret5 = nums | stdv::filter([](int i) {return i % 2 != 0; }) | stdv::transform([](int i) {return i * 2; });
    print(ret5);

    auto nums_ = stdv::iota(1, 10);
    print(nums_);

    auto rnums = stdv::iota(1) | stdv::take(200);
    print(rnums);

    stdr::copy(strs | stdv::reverse | stdv::drop(2), std::ostream_iterator<std::string>(std::cout," "));
}

//范围库: https://zh.cppreference.com/w/cpp/ranges

第二章 STL的泛型特性

2.2span类

#include <iostream>
#include <format>
#include <span>
#include <vector>
#include <array>

template < typename... Args>
void print(const std::string_view fmt_str, Args&&... args) {
    auto fmt_args{ std::make_format_args(args...) };
    std::string outstr{ std::vformat(fmt_str, fmt_args) };
    fputs(outstr.c_str(), stdout);
}

template<class T>
void pspan(std::span<T> s) {
    print("number of elemnts: {}\n", s.size());//  返回序列中的元素个数
    print("size of span: {}\n", s.size_bytes());// 返回以字节表示的序列大小
    for (auto i : s) print("{} ", i);
    endl(std::cout);
}

int main() {
    int a[]{ 1, 2, 3, 4, 5, 6 };
    pspan<int>(a);

    std::endl(std::cout);
    std::vector<int> b{1, 2, 3, 4, 5 };
    pspan<int>(b);

    std::endl(std::cout);
    std::array<int, 4> c{ 1, 2, 3, 4 };
    pspan<int>(c);
}

out:
    number of elemnts: 6
    size of span: 24
    1 2 3 4 5 6

std::span 在C++20中被引入

它给具有连续对象的序列提供了轻量级的视图,以更加安全的方式对其进行迭代和索引,比如std::arraystd::vector、原生数组和原生指针。

常用于去包裹原生数组,并提供了更加安全的一系列函数:如front()begin(), size(), empty()

经典的实现中只有两个成员:

private:
    pointer _ptr;//指向元素的指针

2.3结构化绑定

注意,由于结构化绑定使用自动类型推导,所以类型声明必须使用 auto,且使用的变量名在该作用域内唯一,同时保证标识符列表内的标识符(即[a, b, c] 中的变量a,b,c)个数等于所指代对象的子元素个数。

Lambda表达式(C++11 起) 在C++17起才允许捕获结构化绑定的变量

struct S { int p{6}, q{7}; };
const auto& [b, d] = S{};
auto l = [b, d] { return b * d; }; // C++17 起合法
assert(l() == 42);

2.4if&switch中的初始化

void ifFunc(int n) {
    if (auto flag = [n]() {return n; }(); flag % 2 == 0) {// C++17起,允许if语句内声明表达式,它可以是这里的lambda表达式
        print("This is a even Number: {}\n", n);
    }
}

void switchFunc() {
    switch (char c = getchar(); c)// C++17起,允许switch语句内声明表达式,它可以是一条语句
    {
    case 'a':
        print("a\n");
        break;
    default:
        print("input not a b c\n");
        break;
    }
}

通过if & switcht 初始化语句限制了变量的作用域,避免了与其他变量名发生冲突,并且会自动调用对应的析构函数,确保内存被安全释放。

2.5模板参数推导(CTAD)

#include"print.h"

using namespace std::string_literals;

template<class T>
struct X {
    T v{};
    template<class...Args>
    X(Args&&...args) : v{ (args + ...) } {}
};

template<class...Ts>
X(Ts...ts) -> X<std::common_type_t<Ts...>>;//确定所有类型Ts...都能隐式转换到的类型

int main() {
    X x("Hello ", "World🤣"s);
    print("{}\n", x.v);
}

运行结果:

Hello World🤣

在C++17,当我们给定类模板实参时,编译器会对其进行自动类型推导,如上面代码代码中的实例化对象x, 而之前为了实现`x对象的实例化,我们可能需要这样写:

X<const char*, std::string> x("Hello", "World"s);

虽然有了类模板实参推导,但该类模板只接收一种类型,所以需要使用std::common_type_t来对类模板实参进行一个都可隐式转换的类型的提取

因此,当我们初始化STL容器时,可以省略类型的书写:

std::pair p{ 2, 3.14 };// 省略容器元素的类型
std:vector vec{ 1, 2, 3, 4 };
std::sort(vec.begin(), vec.end(), std::greater<>());//省略比较器的类型

2.6编译期if

#include"print.h"

template<class T>
auto f(const T& v) {
    if constexpr (std::is_pointer_v<T>)
        print("is pointer\n");
    else
        print("not pointer\n");
}

template<class T,class...Args>
void show(T t, Args&&...args) {
    print("{}\t",t);
    if constexpr (sizeof...(args)) {
        show(args...);
    }
}

int main() {
    int* p{};
    f(p);
    f(1);
    show(5,314, "🤣", '*');
    print("\n");
}

运行结果:

is pointer
not pointer
5       314     🤣   *

std::is_pointer用于编译器判断参数类型T是否为对象/函数指针

if constexpr 开始的语句被称为 constexpr if 语句, 在 constexpr if 语句中, 若表达式的值可转换到bool类型的常量表达式,如果值为true,舍弃false分支(如果存在),反之亦然

被舍弃的分支中的return 语句不参与函数的返回值类型推导,且可以使用未定义的变量(大概是因为他不会被执行到,所以无关紧要)

sizeof...`在编译期求出参数包的大小,值为0时,被决为`false

第三章 STL容器

3.3使用擦除函数从容器中擦除项

#include"print.h"
#include<vector>
#include<list>

template<typename Tc,typename Tv>
void remove_value(Tc& c, const Tv& v) {//C++20之前的做法
    auto remove_it = std::remove(c.begin(), c.end(), v);//remove_it是首个需要被删除元素的位置
    c.erase(remove_it, c.end());//删除remove_it到end()这个范围的元素
}

int main() {
    std::vector v{ 1,2,3,4,5 };
    print(v);
    ::remove_value(v, 1);
    print(v);
    std::erase(v,5);
    print(v);
    std::erase_if(v, [](int i) {return i % 2 != 0; });
    print(v);

    std::list list{ 1,2,3,4,5,6,7,8,9,10 };
    std::erase(list, 5);
    std::erase_if(list, [](int i) {return i % 2 == 0; });
    print(list);

    std::map<int, std::string> map{ {1,"🤣"},{2,"🥵"},{3,"🐴"},{4,"🐭"} };
    print(map);
    std::erase_if(map, [](auto& i) {
        const auto& [k, v] = i;
        return v == "🥵";
    });
    print(map);
}

运行结果

    size: 5  [ 1 2 3 4 5 ]
    size: 4  [ 2 3 4 5 ]
    size: 3  [ 2 3 4 ]
    size: 2  [ 2 4 ]
    size: 4  [ 1 3 7 9 ]
    size: 4 [ 1:🤣 2:🥵 3:🐴 4:🐭 ]
    size: 3 [ 1:🤣 3:🐴 4:🐭 ]

解析

std::remove

  • 功能: 该函数用于将迭代器中与值匹配的元素移动到末尾,并返回操作完毕后首个与参数值匹配的元素位置
  • 参数 _First 需要进行操作的容器的起始位置
  • 参数 _Last 需要进行操作的容器的截止位置
  • 参数 _Val 需要操作的值
  • Ps: std::remove 提供了可自定义操作规则的 std::remove_if

std::erase

  • 功能: 删除给定容器中与 _Value 匹配的元素
  • 参数 _Cont 需要被擦除元素的容器
  • 参数 _Value 需要被擦除的值
  • Ps: 该函数从 C++20 起,功能同 remove_value()

std::erase_if

  • 功能: std::erase 的自定义删除规则版本
  • 参数 _Cont 需要被擦除元素的容器
  • 参数 _Pred 当该参数为 true 时,擦除对应元素。该参数必须是一个可转换为 bool 类型的表达式(此处使用一个lambda 表达式 来判断是否擦除)
  • Ps: 该函数是 std::erase 的改进版本,相较于旧版本只能单一匹配值来进行删除,std::erase_if可以实现类似示例中的自定义删除规则

注意: std::erasestd::erase_if 会使序列容器迭代器失效

3.4常数时间内从未排序的向量中删除项

#include"print.h"
#include<vector>
#include<ranges>
namespace stdr = std::ranges;

//使用下标的版本
template<typename T>
void quick_delete(T& v, size_t idx) {
    if (idx < v.size()) {
        v[idx] = std::move(v.back());
        v.pop_back();
    }
}
//使用迭代器的版本
template<typename T>
void quick_delete(T& v, typename T::iterator it) {
    if (it < v.end()) {
        *it = std::move(v.back());
        v.pop_back();
    }
}
//若 vector 中项目的顺序不重要,就可以优化这个过程,使其花费 O(1)(常数) 时间
//做法很简单,将传入的要删除的迭代器或索引赋值为末尾元素的值,然后将末尾元素删除,就完成了,但是没有顺序

int main() {
    std::vector v{ 1,2,3,4,5 };
    print(v);
    auto it = stdr::find(v, 3);
    quick_delete(v, it);
    print(v);//顺序不对,正常现象

    quick_delete(v, 2);
    print(v);
}

3.5安全的访问std::vector元素

#include"print.h"
#include<vector>

void test1() {
    std::vector v{ 1,2,3,4,5 };
    v[5] = 2001;//写入非法内存,访问也是越界
    auto& i = v[5];//引用了错误的内存
    print("{}\n", i);//可能发生错误,不保证
}

void test2()try {
    std::vector v{ 1,2,3,4,5 };
    auto& i = v.at(5);// at会进行越界检查,保证了程序的安全
    print("{}\n", i);
}
catch (std::exception& e) {
    print("{}\n", e.what());
}

void test3()try {
    std::vector v{ 1,2,3,4,5 };
    auto& i = v[5];
    print("{}\n", i);
}
catch (std::exception& e) {
    print("{}\n", e.what());
}
int main() {
    //test1();//error
    test2();
    //test3();//error
}

3.7高效的将元素插入到std::map

#include"print.h"

struct X {
    std::string s;
    X() { print("default construct\n"); }
    X(const char* s) :s{ s } { print("construct\n"); }
    X(const X&) { print("copy construct\n"); }
};
void printm(const std::map<int, X>& map) {
    for (const auto& [k, v] : map) {
        print("[ {}:{} ]", k, v.s);
    }
    print("\n");
}

int main() {
    std::map<int, X>map{};
    map[1] = "🐴";//两个构造的开销,有参和默认
    print("\n");
    //直接转发,只有一个有参构造的开销,这里使用try_emplace和emplace效果完全一样
    map.emplace(2,"🥵");
    map.emplace(3, "🤣");
    printm(map);
    print("\n");

    map.emplace(1, "乐");//添加一个具有重复键的元素
    map.try_emplace(1, "乐");
    printm(map);
}
//重复键元素的问题参见 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92300

3.8高效的修改std::map项的键值

#include"print.h"
#include<string>

template<typename M,typename K>
bool node_swap(M& m, K k1, K k2) {
    //extract 是更换 map 的键而不重分配的唯一方式
    auto node1{ m.extract(k1) };
    auto node2{ m.extract(k2) };
    if (node1.empty() || node2.empty())
        return false;
    std::swap(node1.key(), node2.key());
    m.insert(std::move(node1));
    m.insert(std::move(node2));
    return true;
}

int main() {
    std::map<uint32_t, std::string>maps{
        {1,"🐴"},{2,"🥵"},{3,"🤣"},{4,"🐭"},{5,"😘"}
    };
    print(maps);
    ::node_swap(maps, 3, 5);
    print(maps);

    auto node = maps.extract(maps.begin());
    node.key() = 5;
    auto t =maps.insert(std::move(node));
    print(maps);
    if (!t.inserted) {
        print("插入失败 {}\n",t.position->second);
    }
}

3.10使用set进行输入和筛选

#include"print.h"
#include<set>
#include<string>
#include<ranges>

int main() {
    std::set<std::string> sets;//set 容器用于存储键适用于索引关键字

    std::copy(std::istream_iterator<std::string>{std::cin}, {},
        std::inserter(sets, sets.end()));

    print(sets);
}

std::copy 用于将数据拷贝到对应容器中

  • 参数1 _First 需要拷贝的起始迭代器(这里使用istream的迭代器来读取输入流字符串)
  • 参数2 _Last 拷贝的截止迭代器 (这里使用 {} 占位 即拷贝所有输入流中的字符)
  • 参数2 _Dest 如何拷贝(这里使用std::inserter进行插入)

std::inserter 将每一组输入的字符串作为 key 插入到容器中

  • 参数1 _Cont 需要插入数据的容器(这里是sets)
  • 参数2 _Where 需要插入的位置(这里始终插入到sets的尾部)

运行结果

    输入:  1 12 3 3 3 3 3 ^Z
    输出:  size: 3 [ 1 12 3 ]

set 容器的 key 是不可重复的,如果需要运行重复 keyset 可以使用 `std::multiset

set 容器内部通过一颗 R&B树(红黑树)来存储数据,其对字符串的排序方式是按照 字典序故输出时 12 出现在 3 之前

第四章 兼容迭代器

略过,详细可以点击标题了解。

第五章 lambda表达式

5.3用于作用域可重用代码

#include"print.h"

int main() {
    auto one = []() {return "one"; };
    auto two = [] {return "two"; };
    print("{} {}\n", one(), two());

    auto p = [](auto f) {//泛型lambda,C++20之前只能使用这种方式
        print("{}\n", f()); 
    };
    p([] {return "乐"; });

    auto p2 = []<typename T>(T&& f) { print("{}\n", f()); };
    p2(one);
    p2(std::move(one));

    [] <typename T>(T&& f) { print("{}\n", f()); }(two);

    int num{};
    auto p3 = [num]()mutable {num++; };
    for (auto i = 0; i < 5; i++)p3();

    print("{}\n", num);

    auto p4 = [&]()mutable {num++; };
    print("{}\n", sizeof(p4));

    constexpr int n = []()constexpr {return 10 * 10; }();

    auto p5 = []()->int {return 10; };
}

5.4算法库中作为谓词

#include"print.h"
#include<vector>

bool is_div4(int i) {
    return i % 4 == 0;
}

struct is_div4_ {
    bool operator()(int i) {
        return i % 4 == 0;
    }
};

auto is_div_by(int divisor) {
    return [=](int i) {return i % divisor == 0; };
}

int main() {
    std::vector v{ 1,2,3,4,44,8,10 };
    auto count1 =std::count_if(v.begin(), v.end(), is_div4);
    auto count2 = std::count_if(v.begin(), v.end(), is_div4_{});
    print("{} {}\n", count1, count2);
    auto count3 = std::count_if(v.begin(), v.end(), [](int i) {return i % 4 == 0; });
    print("{}\n", count3);

    for (int i : {3, 4, 5}) {
        auto count = std::ranges::count_if(v, is_div_by(i));
        print("{} ", count);
    }

    //不带捕获的lambda表达式可以有转换函数,隐式转换到对应的函数指针
    int(*p)(int) = [](int a) {return a; };
    print("{}\n", p(10));
}

5.5与std::function一起作为多态包装器

#include"print.h"
#include<vector>
#include<functional>
#include<list>
#include<deque>

void hello() {
    print("hello\n");
}

struct Hello_ {
    void greeting() {
        print("hello\n");
    }
};

int main() {
    std::deque<int>d;
    std::list<int>l;
    std::vector<int>v;

    auto print_c = [](const auto& c) {
        for (const auto& i : c)print("{} ", i);
        print("\n");
    };
    auto push_c = [](auto& container) {
        return [&container](auto value) {
            container.push_back(value);
        };
    };
    const std::vector<std::function<void(int)>>consumers{ push_c(d),push_c(l),push_c(v) };
    //consumers[0](10);
    //print_c(d);
    for (auto& i : consumers) {
        for (size_t j = 0; j < 10; j++) {
            i(j);
        }
    }
    print_c(d);
    print_c(l);
    print_c(v);

    std::function f{ hello };
    f();
    Hello_ h;
    std::function<void(void)>ff{ std::bind(&Hello_::greeting,&h) };
    ff();
    std::bind(&Hello_::greeting, &h)();
}

5.7将谓词与逻辑连接词连接起来

#include"print.h"
#include <functional>

static bool begins_with_a(const std::string& s)
{
    return s.find("a") == 0;
}
static bool ends_with_b(const std::string& s)
{
    return s.rfind("b") == s.length() - 1;
}

template <typename A, typename B, typename F>
auto combine(F binary_func, A a, B b) {
    return [=](auto param) {
        return binary_func(a(param), b(param));
    };
}

int main() {
    auto a_xxx_b{ combine(std::logical_and<int>{},begins_with_a, ends_with_b) };

    std::copy_if(std::istream_iterator<std::string>{std::cin}, {},
        std::ostream_iterator<std::string>{std::cout, ", "}, a_xxx_b);
    std::cout << '\n';
}

5.8用相同的输入调用多个lambda

#include"print.h"

auto braces(const char a, const char b) {
    return [a, b](const auto v) {
        print("{}{}{} ", a, v, b);
    };
}

int main() {
    auto a = braces('(', ')');
    auto b = braces('[', ']');
    auto c = braces('{', '}');
    auto d = braces('|', '|');
    for (int i : {1, 2, 3, 4, 5}) {
        for (auto x : { a,b,c,d }) {
            x(i);
        }
        print("\n");
    }
}

5.9对跳转表使用映射lambda

#include"print.h"

const char prompt(const char* p) {
    std::string r;
    print("{} > ", p);
    std::getline(std::cin, r, '\n');

    if (r.size() < 1) return '\0';//如果走这个分支,就是直接下一个循环
    if (r.size() > 1) {
        print("响应时间过长\n");
        return '\0';
    }
    return toupper(r[0]);
}

int main() {
    using jumpfunc = void(*)();

    std::map<const char, jumpfunc> jumpmap{
        { 'A', [] { print("func A\n"); } },
        { 'B', [] { print("func B\n"); } },
        { 'C', [] { print("func C\n"); } },
        { 'D', [] { print("func D\n"); } },
        { 'X', [] { print("Bye!\n"); } }
    };

    char select{};
    while (select != 'X') {
        if ((select = prompt("select A/B/C/D/X"))) {
            auto it = jumpmap.find(select);
            if (it != jumpmap.end()) it->second();
            else print("没有对应的选项!\n");
        }
    }
}

第六章 STL算法

6.2基于迭代器的复制

#include"print.h"
#include<vector>
namespace stdr = std::ranges;

int main() {
    std::vector<std::string>v1{ "alpha","beta","gamma","delta","epsilon" };
    printc(v1,"v1");
    std::vector<std::string>v2(v1.size());
    std::copy(v1.begin(), v1.end(), v2.begin());
    printc(v2, "v2");

    std::copy(v1.begin(), v1.end(), std::back_inserter(v2));
    printc(v2, "v2");

    std::vector<std::string>v3{};
    std::copy_n(v1.begin(), 3, std::back_inserter(v3));
    printc(v3, "v3");

    std::vector<std::string>v4{};
    /*std::copy_if(v1.begin(), v1.end(), std::back_inserter(v4), [](auto& s) {
        return s.size() > 4;
    });*/
    stdr::copy_if(v1,std::back_inserter(v4), [](auto& s) {
        return s.size() > 4;
        });
    printc(v4, "v4");

    stdr::copy(v1, std::ostream_iterator<std::string>{std::cout, " "});
    print("\n");

    stdr::move(v1, v2.begin());
    printc(v1, "after move: v1");
    printc(v2, "after move: v2");
}

6.3将容器元素连接到以供字符串当中

#include"print.h"
#include<vector>
#include<sstream>
#include<list>
#include<numbers>

namespace bw {
    template<typename T>
    std::ostream& join(T it, T end_it, std::ostream& o, std::string_view sep = "") {
        if (it != end_it)o << *it++;
        while (it != end_it)o << sep << *it++;
        return o;
    }

    template<typename I>
    std::string join(I it, I end_it, std::string_view sep = "") {
        std::ostringstream ostr;
        join(it, end_it, ostr, sep);
        return ostr.str();
    }

    std::string join(const auto& c, std::string_view sep = "") {
        return join(std::begin(c), std::end(c), sep);
    }
}

int main() {
    std::vector<std::string>greek{ "alpha","beta","gamma",
        "delta","epsilon" };
    for (const auto& c : greek) std::cout << c << ",";
    print("\n");
    auto greek_view = greek | std::views::join;
    for (const auto& c : greek_view) std::cout << c;
    print("\n");

    bw::join(greek.begin(), greek.end(), std::cout, ", ") << '\n';

    auto s = bw::join(greek.begin(), greek.end(), ", ");
    print("{}\n", s);

    auto s2 = bw::join(greek, ", ");
    print("{}\n", s2);

    std::list<double>list{ std::numbers::pi,std::numbers::e,std::numbers::sqrt2 };
    print("{}\n", bw::join(list, ": "));
}

6.4std::sort排序容器元素=

#include"print.h"
#include<vector>
#include<random>

void check_sorted(auto& c) {
    if (!std::is_sorted(c.begin(), c.end()))print("un");
    print("sorted: ");
}

void printc_(const auto& c) {
    check_sorted(c);
    for (auto& e : c)print("{} ", e);
    print("\n");
}

void randomize(auto& c) {
    static std::random_device rd;
    static std::default_random_engine rng(rd());
    std::shuffle(c.begin(), c.end(), rng);
}

struct things {
    std::string s_;
    int i_;
    std::string str()const {
        return std::format("({}, {})", s_, i_);
    }
};

void print_things(const auto& c) {
    for (auto& v : c)print("{} ", v.str());
    print("\n");
}

int main() {
    std::vector<int>v{ 1,2,3,4,5,6,7,8,9,10 };
    printc_(v);

    for (int i{ 3 }; i; i--) {
        randomize(v);
        printc_(v);
    }
    std::sort(v.begin(), v.end());
    printc_(v);

    print("partial_sort:\n");
    randomize(v);
    auto middle{ v.begin() + (v.size() / 2) };
    std::partial_sort(v.begin(), middle, v.end());
    printc_(v);

    std::partition(v.begin(), v.end(), [](int i) {return i > 5; });
    printc_(v);

    std::vector<things>vthings{ {"🐴",1},{"😘",2},{"🤣",3},{"🥵",4},{"🤡",5} };
    std::sort(vthings.begin(), vthings.end(),
        [](const things& lhs, const things& rhs) {
            return lhs.i_ > rhs.i_;
    });

    print_things(vthings);
}

6.5std::transform修改容器内容

#include"print.h"
#include<vector>

std::string str_lower(const std::string& s) {
    std::string outstr{};
    for (const char& c : s) {
        outstr += tolower(c);
    }
    return outstr;
}

int main() {
    std::vector<int>v1{ 1,2,3,4,5,6,7,8,9,10 };
    std::vector<int>v2;
    printc(v1, "v1");
    std::transform(v1.begin(), v1.end(), std::back_inserter(v2), [](int x) {return x * x; });
    printc(v2, "v2");

    std::vector<std::string>vstr1{ "Aaa","Bbb","Ccc","DDD" };
    std::vector<std::string>vstr2;
    printc(vstr1, "vstr1");
    print("str_lower:\n");
    std::transform(vstr1.begin(), vstr1.end(), std::back_inserter(vstr2),
        [](std::string& x) {return str_lower(x); });
    printc(vstr2, "vstr2");

    print("ranges sequares:\n");
    auto view1 = std::views::transform(v1, [](int x) {return x * x; });
    printc(view1, "view1");

    v2.clear();
    std::ranges::transform(v1, std::back_inserter(v2), [](int x) {return x * x; });
    printc(v2, "v2");
}

其中一个常用操作就是配合tolower()函数是把字符串都转化为小写字母;touppre()函数是把字符串都转化为大写字母:

transform(first.begin(),first.end(),second.begin(),::tolower);
transform(first.begin(),first.end(),second.begin(),::touppre();

6.6查找特定项

#include"print.h"
#include<vector>
#include<algorithm>

struct City {
    std::string name{};
    unsigned pop{};
    bool operator==(const City& o)const {
        return name == o.name;
    }
    std::string str()const {
        return std::format("[{}, {}]", name, pop);
    }
};

int main() {
    const std::vector<int>v{ 1,2,3,4,5,6,7,8,9,10 };

    auto it1 = std::find(v.begin(), v.end(), 7);
    if (it1 != v.end())print("found: {}\n", *it1);
    else print("not found:\n");

    const std::vector<City>c{
        {"London",8425622},
        {"Berlin",3566791},
        {"Tokyo",37435191},
        {"Cairo",20485965}
    };
    auto it2 = std::find(c.begin(), c.end(), City{ "Berlin" });
    if (it2 != c.end())print("found: {}\n", it2->str());
    else print("not found:\n");

    auto it3 = std::find_if(begin(c), end(c), [](const City& item) {
        return item.pop > 20000000;
    });
    if (it3 != c.end())print("found: {}\n", it3->str());
    else print("not found:\n");

    auto vwl = std::views::filter(c, [](const City& item) {
        return item.pop > 20000000;
    });
    for (const City& e : vwl)print("{}\n", e.str());
}

这个内容大概四个部分

  1. 使用std::find查找标量元素
  2. 使用std::find查找自定义类型元素(需要重载operator==)
  3. 使用 std::find_if 查找自定义类型符合谓词要求的元素
  4. 使用 std::views::filter 返回符合谓词要求的视图,可以像普通容器一样遍历

std::findstd::find_if的返回值是迭代器,如果没有查找到,则返回end()

6.10合并已排序容器

#include"print.h"
#include<vector>
#include<algorithm>

int main() {
    std::vector<std::string>vs1{ "dog","cat","veloiraptor" };
    std::vector<std::string>vs2{ "kirk","sulu","spock" };
    std::vector<std::string>dest{};
    printc(vs1, "vs1");
    printc(vs2, "vs2");

    std::ranges::sort(vs1);
    std::ranges::sort(vs2);
    printc(vs1, "vs1");
    printc(vs2, "vs2");

    std::merge(vs1.begin(), vs1.end(), vs2.begin(), vs2.end(), std::back_inserter(dest));
    printc(dest, "dest");
}

运行结果:

vs1: [dog] [cat] [veloiraptor]
vs2: [kirk] [sulu] [spock]
vs1: [cat] [dog] [veloiraptor]
vs2: [kirk] [spock] [sulu]
dest: [cat] [dog] [kirk] [spock] [sulu] [veloiraptor]

std::merge算法接受两个已排序的序列,并创建第三个已合并并排序的序列。前面四个参数表示两个输入范围,第五个参数表示结果序列发送的输出迭代器

第七章 字符串、流和格式化

STL 字符串类是一个功能强大的全功能工具,用于存储、操作和显示基于字符的数据。在高级脚本语言中,可以找到的许多字符串相关的便利、快速和敏捷的功能。

std::string 类基于 std::basic_string,这是一个连续的容器类,可以用字符类型实例化。其类签名是这样

template <class _Elem, class _Traits = char_traits<_Elem>, class _Alloc = allocator<_Elem>>
class basic_string

TraitAllocator 模板参数通常保留默认值。basic_string 的底层存储是一个连续的 CharT 序列,可以通过 data() 成员函数访问:

#include<string>
#include<iostream>

int main() {
    const std::basic_string<char>s{ "hello" };
    const char* sdata = s.data();
    for (size_t i = 0; i < s.size(); i++){
        std::cout << sdata[i] << ' ';
    }
    std::cout << '\n';
}

运行结果:

h e l l o

data() 成员函数返回一个指向底层字符数组的 CharT*。从 C++11 起,data() 返回的数组以空结 束,使得 data() 等价于 c_str()

basic_string 类包含许多在其他连续存储类中可以找到的方法,包括 insert()erase()push_back()pop_back() 等,这些方法可以操作底层的 CharT 数组。

std::stringstd::basic_string<char> 类型的别名:

using string  = basic_string<char, char_traits<char>, allocator<char>>;

7.3轻量字符串对象string_view

#include"print.h"
#include<string>
using namespace std::literals;

std::string_view sv() {
    const char text[]{ "hello" };
    std::string_view greeting{ text };
    return greeting;
}

void f(const std::string& str) {

}
void f2(std::string_view str) {

}

int main() {
    char str[10]{ "hello" };
    std::string str2{ str };
    print("{}\n", str2);
    str[0] = 'a';
    print("{}\n", str2);

    std::string_view sview{ str };
    print("{}\n", sview);
    str[0] = 'b';
    print("{}\n", sview);

    auto t = sv();
    print("{}\n", t);

    print("{}\n", "hello"sv.substr(1,4));

    constexpr std::string_view str3{ "哈哈" };
    //constexpr std::string str4{ "哈哈" };//error

    print("{}\n", str3);

    std::string str4{ "1" };
    const std::string str5{ "1" };
    f(str4);
    f(str5);
    f("1");//开销大,需要构造临时的std::string对象

    f2("1");
    f2(str4);
    f2(str5);
}

std::string_view是C++17添加的一个字符串视图类,它的构成和原理也十分简单:它的构造函数只是把自己的数据成员const pointer以及size初始化而已,这是通常的实现,也就是自己不存储任何数据,副本,只是视图,依靠指针进行一切访问操作,不提供修改操作

7.4连接字符串

#include"print.h"
#include<sstream>
#include<ostream>
#include<chrono>
using std::chrono::high_resolution_clock;
using std::chrono::duration;

void timer(auto(f)()->std::string) {
    auto t1 = high_resolution_clock::now();
    std::string s{ f() };
    auto t2 = high_resolution_clock::now();
    duration<double, std::milli>ms = t2 - t1;
    print("{}", s);
    print("duration: {} ms\n", ms.count());
}

std::string concat_string() {
    print("concat_string\n");
    std::string a{ "a" };
    std::string b{ "b" };
    long n{};
    while (++n) {
        std::string x{};
        x += a + ", " + b + "\n";
        if (n >= 10000000)return x;
    }
    return "error\n";
}

std::string append_string() {
    print("append_string\n");
    std::string a{ "a" };
    std::string b{ "b" };
    long n{};
    while (++n) {
        std::string x{};
        x.append(a);
        x.append(", ");
        x.append(b);
        x.append("\n");
        if (n >= 10000000)return x;
    }
    return "error\n";
}

std::string concat_ostringstream() {
    print("ostringstream\n");
    std::string a{ "a" };
    std::string b{ "b" };
    long n{};
    while (++n) {
        std::stringstream x{};
        x << a << ", " << b << "\n";
        if (n >= 10000000)return x.str();
    }
    return "error\n";
}

std::string concat_format() {
    print("append_format\n");
    std::string a{ "a" };
    std::string b{ "b" };
    long n{};
    while (++n) {
        std::string x{};
        x += std::format("{}, {}\n", a, b);
        if (n >= 10000000)return x;
    }
    return "error\n";
}

int main() {
    timer(append_string);
    timer(concat_string);
    timer(concat_ostringstream);
    timer(concat_format);
}

运行结果:

append_string
a, b
duration: 5285.7537 ms
concat_string
a, b
duration: 19286.9228 ms
ostringstream
a, b
duration: 21790.0884 ms
append_format
a, b
duration: 29601.7629 ms

7.5转换字符串

#include"print.h"

char char_upper(const char& c) {
    return static_cast<char>(std::toupper(c));
}
char char_lower(const char& c) {
    return static_cast<char>(std::tolower(c));
}
char rot13(const char& x) {
    auto rot13a = [](char x, char a)->char {
        return a + (x - a + 13) % 26;
    };
    if (x >= 'A' && x <= 'Z')return rot13a(x, 'A');
    if (x >= 'a' && x <= 'z')return rot13a(x, 'a');
    return x;
}
std::string title_case(std::string& s) {
    auto begin{ s.begin() };
    auto end{ s.end() };
    *begin++ = char_upper(*begin);
    bool space_flag{ false };
    for (auto it{ begin }; it != end; ++it) {
        if (*it == ' ')space_flag = true;
        else {
            if (space_flag)*it = char_upper(*it);
            space_flag = false;
        }
    }
    return s;
}

int main() {
    std::string s{ "hello jimi\n" };
    print("{}", s);
    std::transform(s.begin(), s.end(), s.begin(), char_upper);
    print("{}", s);
    for (auto& c : s)c = rot13(c);
    print("{}", s);
    for (auto& c : s)c = rot13(char_lower(c));
    print("{}", s);

    title_case(s);
    print("{}", s);
}

运行结果:

hello jimi
HELLO JIMI
URYYB WVZV
hello jimi
Hello Jimi

7.7删除字符串的空白

#include"print.h"
#include<ranges>

std::string trimstr(const std::string& s) {
    constexpr const char* whitespace{ " \t\r\n\v\f" };
    if (s.empty())return s;
    const auto first{ s.find_first_not_of(whitespace) };
    if (first == std::string::npos)return{};
    const auto last{ s.find_last_not_of(whitespace) };
    return s.substr(first, (last - first + 1));
}

int main() {
    std::string s{ " \t ten-thumbed input \t  \n \t" };
    print("[{}]\n", s);
    print("[{}]\n", trimstr(s));
}

运行结果:

[        ten-thumbed input
        ]
[ten-thumbed input]

7.9统计文件中的单词数

#include"print.h"
#include<fstream>
#include<filesystem>

size_t wordcount(auto& is) {
    using it_t = std::istream_iterator<std::string>;
    return std::distance(it_t{ is }, {});
}

int main() {
    const char* fn{ "E:/自制视频教程/《C++20 STL Cookbook》2023/src/src/the-raven.txt" };
    std::ifstream infile{ fn,std::ios_base::in };
    size_t wc{ wordcount(infile) };
    print("There are {} words in the file.\n", wc);
    print("size: {}\n", std::filesystem::file_size(fn));
}

第八章 实用工具类

C++标准库包括为特定任务设计的各种工具类。有些是常见的,读者们可能在这本书的其他示 例中见过很多这样的类。 本章在以下主题中介绍了一些通用的工具,包括时间测量、泛型类型、智能指针等:

std::optional 管理可选值

std::any 保证类型安全

std::variant 存储不同的类型

std::chrono 的时间事件

• 对可变元组使用折叠表达式

std::unique_ptr 管理已分配的内存

std::shared_ptr 的共享对象

• 对共享对象使用弱指针

• 共享管理对象的成员

• 比较随机数引擎

• 比较随机数分布发生器

感兴趣的可以点击标题去看具体内容

第九章 并发和并行

并发性和并行性指的是在不同的执行线程中运行代码的能力。并发性 是在后台运行线程的能力,并行性 是在处理器的不同内核中同时运行线程的能力。 运行时库以及主机操作系统,将为给定硬件环境中的线程,在并发和并行执行模型之间进行选择。 在现代多任务操作系统中,main() 函数已经代表了一个执行线程。当一个新线程启动时,可由 现有的线程派生。 C++ 标准库中,std::thread 类提供了线程执行的基本单元。其他类构建在线程之上,以提供锁、 互斥和其他并发模式。根据系统架构的不同,执行线程可以在一个处理器上并发运行,也可以在不 同的内核上并行运行。

• 休眠一定的时间

std::thread——实现并发

std::async——实现并发(重点看一下)

• STL 算法与执行策略

• 互斥锁和锁——安全地共享数据(重点看一下)

std::atomic——共享标志和值

std::call_once——初始化线程

std::condition_variable——解决生产者-消费者问题

• 实现多个生产者和消费者

第十章 文件系统

不多介绍


评论
  目录