以下内容主要包含 C++11 及之后(当然也有 C++11 前未被广泛熟知)的实用特性、库等的理论和实践。由于这是分享会内容,为照顾面向人群,内容较为简略,对认真学习并使用 C++ 的老手可能意义不大。

内容部分摘录于 cppreference,大部分也取材于此。另参照 C++ 标准等资料。

另外第一部分关于 auto 的内容大部分与 Effective Modern C++ 开篇一致。然而在这部分动笔前并未参阅照搬此书(以前草草读过但大部分也都忘记了……),写完后才发现结构几乎一摸一样,大概所谓理解透彻了吧……(当然后续还是参阅了以丰富内容……)


1. auto 的几种用法

C++11 前用作自动存储期说明符,相关的关键字有 static, register(现已移除), thread_local 等。

C++11 后开始用作占位符类型,类型推导原则遵从模板实参推导规则。

  1. If the placeholder-type-specifier is of the form type-constraint opt auto, the deduced type T' replacing T is determined using the rules for template argument deduction. … [9.2.9.6.2 dcl.type.auto.deduct]

C++14 引入了 decltype(auto)。因为这里是遵循 decltype 推导的原则,所以可以推导引用类型。

  1. If the placeholder-type-specifier is of the form type-constraint opt decltype(auto), T shall be the placeholder alone. The type deduced for T is determined as described in 9.2.9.5, as though E had been the operand of the decltype. [9.2.9.6.2 dcl.type.auto.deduct]

示例代码如下。

#include <boost/type_index.hpp>
#include <iostream>
#include <type_traits>
#include <typeinfo>

template <typename T>
void f(T t)
{
    // std::cout << typeid(t).name() << std::endl;
    std::cout << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
}

template <typename T>
void g(T& t)
{
    // std::cout << typeid(t).name() << std::endl;
    std::cout << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
}

int main()
{
    double arr[20];
    const int seven = 7;

    f(arr);   // Pd, double*
    g(arr);   // A20_d, double [20]
    f(seven); // i, int
    g(seven); // i, int const
    f(7);     // i, int
    // g(7);  //右值绑定到 non-const 引用,不合法

    auto a1 = arr;  // Pd, double*
    auto& a2 = arr; // A20_d, double [20]

    auto b1 = seven;  // i, int
    auto& b2 = seven; // i, int const&

    decltype(auto) c1 = seven;   // i, const int
    decltype(auto) c2 = (seven); // i, const int&

    // static_assert(std::is_same_v<int, decltype(b1)>);
    // static_assert(std::is_same_v<const int&, decltype(b2)>);
    // static_assert(std::is_same_v<const int, decltype(c1)>);
    // static_assert(std::is_same_v<const int&, decltype(c2)>);
    std::cout << boost::typeindex::type_id_with_cvr<decltype(b1)>().pretty_name() << std::endl;
    std::cout << boost::typeindex::type_id_with_cvr<decltype(b2)>().pretty_name() << std::endl;
    std::cout << boost::typeindex::type_id_with_cvr<decltype(c1)>().pretty_name() << std::endl;
    std::cout << boost::typeindex::type_id_with_cvr<decltype(c2)>().pretty_name() << std::endl;
}

typeid 用于查看类型,因为名称修饰,可以使用 c++filt -t 来查看原名称。标准 [7.6.1.8 expr.typeid] 章节如下。

  1. … If the type of the type-id is a reference to a possibly cv-qualified type, the result of the typeid expression refers to a std::type_info object representing the cv-unqualified referenced type. If the type of the type-id is a class type or a reference to a class type, the class shall be completely-defined.

即,typeid 不会展示 cv 限定和引用类型。因此,上述的输入未区分 intconst int&,但可根据 static_assert 配合 std::is_same 在编译期检查类型。或者,也可以选用 Boost 库的 Boost.TypeIndex,它根据模板实例化时在 __PRETTY_FUNTION__ 宏中得到原类型的名字并进行处理,最终可以输出可供阅读的、包含 cv 限定和引用类型的类型信息。

简单总结,由于在模板实参推导时,未指定引用和 cv 限定的形参类型会导致传入的实参类型退化(即类似 std::decay<T> 的行为,将数组类型退化为指针、将函数类型退化为指针或得到 std::remove_cv_t<std::remove_refernce_t<T>> 的类型。而 auto 的推导规则与模板实参推导一致,因此会出现 auto 无法直接识别引用类型的情况。

理论上,autodecltype(auto) 可以出现在所有需要填写类型的位置作为占位符。以下未特殊说明的示例均为 C++11 起开始支持的写法。

  • auto x = func();
  • auto func() -> int { return x; };
  • auto func() { return x; }; (C++14)
  • void func(const auto& x); (C++14)
  • template<auto I> struct X; (C++17, 非类型模板形参)
  • const auto& [a, b] = std::make_tuple(1, 2); (C++17, 结构化绑定)

2. 关于 lambda 表达式和算法库

1. 从 std::find_if 说起

示例代码如下。

#include <iostream>
#include <vector>

struct Point {
    int x = 0;
    int y = 0;
};

Point find(const std::vector<Point>& v) {
    for (const auto& point : v) {
        if (point.x < 0 && point.y < 0)
            return point;
    }
    return Point{};
}

int main()
{
    std::vector<Point> v{
        {0, -1},{1, 1},{-1, -1},{0, 0} };
    Point p = ::find(v);
    std::cout << p.x << " " << p.y << std::endl;
}

这段代码中基于范围的 for 循环在实现中等价于根据 begin()end() 产生范围表达式。等价的代码如下(C++ Insights, use libc++)。

//...

Point find(const std::vector<Point, std::allocator<Point>>& v)
{
    {
        const std::vector<Point, std::allocator<Point>>& __range1 = v;
        std::__wrap_iter<const Point*> __begin1 = __range1.begin();
        std::__wrap_iter<const Point*> __end1 = __range1.end();
        for (; std::operator!=(__begin1, __end1); __begin1.operator++()) {
            const Point& point = __begin1.operator*();
            if ((point.x < 0) && (point.y < 0)) {
                return Point(point);
            }
        }
    }
    return Point{ {0}, {0} };
}

//...

按照此类查找方法需要额外构造对象,而若返回 const 引用也有着无法简单地表示未找到对应元素的情况。因此,可以改用指针或行为类似的迭代器。

std::vector<Point>::iterator find(std::vector<Point>::iterator begin, std::vector<Point>::iterator end) {
    for (; begin != end; ++begin) {
        if (begin->x < 0 && begin->y < 0)
            return begin;
    }
    return begin;
}

如果将判断的内容改为传入函数对象(或函数指针),既有如下代码。

bool pred(const Point& point) {
    return point.x < 0 && point.y < 0;
}

// 若改用函数指针则第三个参数为 bool (*f)(const Point&)
std::vector<Point>::iterator find(std::vector<Point>::iterator begin, std::vector<Point>::iterator end, std::function<bool(const Point&)> f) {
    for (; begin != end; ++begin) {
        // 若改用函数指针此处也可写为 (*f)(*begin)
        // 解引用函数指针生成标识被指向函数的左值
        if (f(*begin))
            return begin;
    }
    return begin;
}

int main()
{
    //std::vector<Point> v\{\{0, -1}, {1, 1}, {-1, -1}, {0, 0\}\};
    // 由于存在函数到指针的隐式转换,若改用函数指针此处可以不改为 &pred
    std::vector<Point>::iterator it = ::find(v.begin(), v.end(), pred);
    std::cout << it->x << " " << it->y << std::endl;
}

至此,只需要将上述代码通过模板泛型即已经得到了标准库中 std::find_if 的实现。以下代码摘自 libc++ 12。

template <class _InputIterator, class _Predicate>
_LIBCPP_NODISCARD_EXT inline
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX17
_InputIterator
find_if(_InputIterator __first, _InputIterator __last, _Predicate __pred)
{
    for (; __first != __last; ++__first)
        if (__pred(*__first))
            break;
    return __first;
}

几乎一致。算法库中类似的设施在 C++11 前就已经存在。而基于范围的 for 循环也有类似的替代,std::for_each,实现摘自 libc++ 12。

template <class _InputIterator, class _Function>
inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX17
_Function
for_each(_InputIterator __first, _InputIterator __last, _Function __f)
{
    for (; __first != __last; ++__first)
        __f(*__first);
    return __f;
}

2. lambda 表达式和函数对象

算法库中许多传入迭代器和谓词的算法在 C++11 前已经存在。按照之前的示例。

bool pred(const Point& point) {
    return point.x < 0 && point.y < 0;
}

std::vector<Point> v{ {0, -1}, {1, 1}, {-1, -1}, {0, 0} };
std::vector<Point>::iterator it = v.begin();

it = std::find_if(v.begin(), v.end(), pred); //函数隐式转化为指针
it = std::find_if(v.begin(), v.end(), &pred);

也可传入重载 operator() 的对象。

struct Pred {
    bool operator()(const Point& point) {
        return point.x < 0 && point.y < 0;
    }
};

it = std::find_if(v.begin(), v.end(), Pred());

C++11 引入 std::bind 后也可以用其构造函数对象。

it = std::find_if(v.begin(), v.end(), std::bind(pred, std::placeholders::_1));

当然,最简洁的是传入 lambda 表达式。

it = std::find_if(v.begin(), v.end(), [](const Point& point) {
    return point.x < 0 && point.y < 0;
});

等价的代码如下(C++ Insights, use libc++)。

class __lambda_14_72 {
public:
    inline /*constexpr */ bool operator()(const Point& point) const
    {
        return (point.x < 0) && (point.y < 0);
    }

    using retType_14_72 = bool (*)(const Point&);
    inline /*constexpr */ operator retType_14_72() const noexcept
    {
        return __invoke;
    };

private:
    static inline bool __invoke(const Point& point)
    {
        return (point.x < 0) && (point.y < 0);
    }

public:
    // /*constexpr */ __lambda_14_72() = default;
};

根据标准(7.5.5.2 [expr.prim.lambda.closure]),lambda 表达式的类型是闭包对象(closure object,类似函数对象)。具体要求和定义可参阅标准cppreference

lambda 表达式有两种捕获方式:[=] 复制捕获和 [&] 引用捕获。此外可以对每个使用的变量单独捕获,以 [a] 方式捕获的为复制捕获,以 [&a] 方式捕获的为引用捕获,而对当前对象的 [this] 捕获为引用捕获,C++17 中引入了对当前对象的 [*this] 复制捕获。

int num = 1;
auto plus = [num](int x) { return x + num; };
auto plus_num = [&num](int x) { return num = x + num; };
plus(2);     // =num = 1; return = 3, num = 1
plus_num(2); // &num = 1; return = 3, num = 3
plus(3);     // =num = 1; return = 4, num = 3
plus_num(3); // &num = 3; return = 6, num = 6

其中 lambda 表达式的等价代码如下(C++ Insights, use libc++)。

int num = 1;

class __lambda_4_17 {
public:
    inline /*constexpr */ int operator()(int x) const
    {
        return x + num;
    }

private:
    int num;

public:
    __lambda_4_17(int& _num)
        : num{_num}
    {
    }
};

__lambda_4_17 plus = __lambda_4_17{num};

class __lambda_5_21 {
public:
    inline /*constexpr */ int operator()(int x) const
    {
        return num = (x + num);
    }

private:
    int& num;

public:
    __lambda_5_21(int& _num)
        : num{_num}
    {
    }
};

__lambda_5_21 plus_num = __lambda_5_21{num};

// ...

此外还有 [...] 的形参包展开捕获。

template<class... Args>
void f(Args... args)
{
    auto print = [args...]() {
        // C++17 折叠表达式
        // 等价于 ...(std::operator<<(std::operator<<(std::cout, __args0), __args1)...
        (std::cout << ... << args);
    };
    print();
}

f("Hello", ",", "world", "!", "\n");

其中等价代码如下(C++ Insights, use libc++)。

template <>
void f<const char*, const char*, const char*, const char*, const char*>(const char* __args0,
    const char* __args1, const char* __args2, const char* __args3, const char* __args4)
{

    class __lambda_6_18 {
    public:
        inline /*constexpr */ void operator()() const
        {
            std::operator<<(
                std::operator<<(
                    std::operator<<(
                        std::operator<<(std::operator<<(std::cout, __args0), __args1), __args2),
                    __args3),
                __args4);
        }

    private:
        const char* __args0;
        const char* __args1;
        const char* __args2;
        const char* __args3;
        const char* __args4;

    public:
        __lambda_6_18(const char* ___args0, const char* ___args1, const char* ___args2,
            const char* ___args3, const char* ___args4)
            : __args0{___args0}
            , __args1{___args1}
            , __args2{___args2}
            , __args3{___args3}
            , __args4{___args4}
        {
        }
    };

    __lambda_6_18 print = __lambda_6_18{__args0, __args1, __args2, __args3, __args4};
    print.operator()();
}

C++14 引入了捕获初始化器,可用于移动捕获、捕获 const 引用等。

int num = 1;
auto plus = [&n = std::as_const(num)](int x) {
    return x + n;
};

3. 算法库常用设施

1. <algorithm>

  • std::for_each

    循环并对每个元素应用函数。

    std::string s("Hello, world!");
    std::for_each(s.begin(), s.end(), [](char& c) {
        c = std::toupper(c);
    });
    
  • std::all_of, std::any_of, std::none_of

    检查函数是否对范围内的所有元素返回 true 检查函数是否对范围内的任一元素返回 true 检查函数是否对范围内的所有元素返回 false

    std::vector v(10, 0);
    std::iota(v.begin(), v.end(), 0);
    bool result = std::all_of(v.begin(), v.end(), [](const auto& i) {
        return i >= 0;
    });
    
  • std::find, std::find_if, std::find_if_not

    返回范围中满足等于传入值的首个元素 返回范围中满足对传入函数返回 true 的首个元素 返回范围中满足对传入函数返回 false 的首个元素

  • std::transform

    应用给定的函数到范围并存储结果于另一范围。

    std::string s("Hello, world!")
    std::string u;
    std::transform(s.begin(), s.end(), std::back_inserter(u), [](const char& c) {
        return std::toupper(c);
    });
    
  • std::copy, std::copy_if

    复制范围内所有元素并存储结果于另一范围。 复制范围内对传入函数返回 true 的元素并存储结果于另一范围。

    std::string s("Hello, world!");
    std::string u;
    std::copy_if(s.begin(), s.end(), std::back_inserter(u), [](const char& c) {
        return std::isalpha(c);
    });
    
  • std::remove, std::remove_if

    移除范围内等于传入值的所有元素,并返回范围新结尾的尾后迭代器。 移除范围内对传入函数返回 true 的所有元素,并返回范围新结尾的尾后迭代器。

    std::string s("Hello, world!");
    s.erase(std::remove_if(s.begin(), s.end(), [](const char& c) {
        return std::ispunct(c);
    }), s.end());
    // 原范围内容:
    // 'H' 'e' 'l' 'l' 'o' ',' ' ' 'w' 'o' 'r' 'l' 'd' '!'
    // std::remove_if 后范围内容:
    // 'H' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' 'd' '!'
    // remove_if 返回的迭代器指向                      ↑
    
  • std::count, std::count_if

    返回范围中满足等于传入值的元素数量 返回范围中满足对传入函数返回 true 的元素数量

    std::string s("Hello, world!");
    std::cout << std::count_if(s.begin(), s.end(), [](const char& c) {
        return std::isalpha(c);
    });
    
  • std::sample (C++17)

    范围内随机取样 N 个并存储结果于另一范围。

  • std::shuffle

    随机重排范围内元素。

  • std::sort

    按照特定比较结果对范围内元素不稳定排序(默认按照 operator<std::less<>)。

    std::vector v(10, 0);
    std::iota(v.begin(), v.end(), 0);
    std::shuffle(v.begin(), v.end(), std::mt19937(std::random_device()()));
    std::sort(v.begin(), v.end(), std::greater());
    
    std::vector<int> s;
    std::sample(v.begin(), v.end(), std::back_inserter(s), 5, std::mt19937{std::random_device{}()});
    

2. <random>

  • 种子

    std::randome_device

  • 随机数引擎

    • std::linear_congruential_engine 线性同余
    • std::mersenne_twister_engine 梅森缠绕
    • std::subtract_with_carry_engine 延迟斐波那契

    常用的预定义引擎为 std::mt19937,32位梅森缠绕器。

  • 分布

    • std::uniform_int_distribution, uniform_real_distribution 均匀分布
    • std::bernoulli_distribution 伯努利分布
    • std::normal_distribution 正态分布

示例代码如下。

std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(0, 9);
std::map<int, int> map;
for (int i = 0; i < 1000000; ++i) {
    map[dist(gen)]++;
}

/*
* 结果大致如下:
*    0 100552
*    1 100280
*    2 100019
*    3 99904
*    4 100029
*    5 100026
*    6 99674
*    7 99730
*    8 100235
*    9 99551
*/

3. <numeric>

  • std::iota

    以传入初始值递增填充范围。

  • std::accumulate, std::reduce

    • 遍历范围以传入初始值累计计算范围元素。
    • 类似 std::transform,但可以按照传入执行策略执行。 (C++17)

    示例如下。目前只有 GCC 和 libstdc++ 支持执行策略,根据系统库配置和默认工具链的不同可能会需要链接 Intel tbb 库。

std::vector v(10, 0);
std::iota(v.begin(), v.end(), 1);
std::reduce(std::execution::par, v.begin(), v.end(), 1, std::multiplies());

3. 通用工具库,<tuple><utility>

1. std::pairstd::tuple

std::pair 定义于 <utility>,是一对异构值的模板,成员为 firstsecond,使用 std::make_pair 构造。标准库中主要应用在 std::mapstd::unordered_map 等容器中,也用在部分函数的返回值中,例如 std::maptry_emplace 等。

std::tuple 定义于 <tuple>,是多个异构值的模板,类似扩展的 std::pair,使用 std::get 获取元素,使用 std::make_tuple 构造。

std::tuple 相关的几个用法如下。

  • std::tie

    返回含左值引用的 std::tuple 对象。可用于解包 std::pairstd::tuple

    auto t = std::make_tuple(1, 2, 3);
    int a, b, c;
    std::tie(a, b, c) = t; // a = 1, b = 2, c = 3
    

    许多语言用 _ 表示可忽略的值,包括 Python、Go 等。C++ 中在解包 tuple 时使用 std::ignore

    std::set<int> s;
    // std::set 的 insert 返回 std::pair<iterator,bool>
    bool result;
    std::tie(std::ignore, result) = s.insert(1);
    

    std::tie 根据引用构建 std::tuple,而 std::tuple 默认根据字典序比较大小,因此也可用于自定义结构体比较。

    struct Rank {
        uint64_t id;
        uint32_t score;
        time_t timestamp;
      
        bool operator>(const Rank& rhs) {
            return std::tie(score, rhs.timestamp, id) > std::tie(rhs.score, timestamp, rhs.id);
        }
    };
      
    std::vector<Rank> v{ {6, 300, 1618381861}, {8, 400, 1618554644}, {10, 300, 1618468154}, {12, 500, 1618468154} };
    std::sort(v.begin(), v.end(), std::greater<>());
    /*
    * 结果如下:
    * 12    500    1618468154
    * 8     400    1618554644
    * 6     300    1618381861
    * 10    300    1618468154
    */
    
  • 结构化绑定

    结构化绑定为解包 std::tuple 提供了比 std::tie 更方便的方法,其应用范围也不仅限于 std::tuple。可以实现类似 Go 中 result, err := function() 写法。类似于依次使用 auto 推导变量类型并引入当前作用域。

    auto t = std::make_tuple(1, 2, 3);
    {
        // 复制 t 到新的 tuple,再 auto a = std::get<0>(tuple) ...
        auto [a, b, c] = t; // a = 1, b = 2, c = 3
    }
    {
        // 创建 t 的引用 tuple,再 auto& a = std::get<0>(tuple) ...
        auto& [a, b, c] = t; // a = 1, b = 2, c = 3
    }
    

    此外还可以用于绑定数组和数据成员,但结构化绑定不支持嵌套,也不支持 std::ignore

    struct Point {
        int x;
        int y;
    };
    
    std::tuple t = std::make_tuple(Point{0, 0}, Point{1, 1}, Point{-1, -1});
    const auto& [p1, p2, p3] = t; // p1 = {0, 0}, p2 = {1, 1}, p3 = {-1, -1}
    const auto& [x1, x2] = p1;    // x1 = 0, x2 = 0
    

    结构化绑定除了简化写法外,也能实现一些很难以用 std::tie 实现的功能。

    int x = 0, y = 0, z = 0;
    auto t = std::make_tuple(std::ref(x), std::ref(y), std::ref(z));
    auto& [a, b, c] = t;
    a = 1, b = 2, c = 3; // x = 1, y = 2, z = 3;
    

    上述代码的等价代码如下(C++ Insights, use libc++)。

    int x = 0;
    int y = 0;
    int z = 0;
    std::tuple<int&, int&, int&> t = std::make_tuple(std::ref(x), std::ref(y), std::ref(z));
    std::tuple<int&, int&, int&>& __t10 = t;
    int& a = std::get<0UL>(__t10);
    int& b = std::get<1UL>(__t10);
    int& c = std::get<2UL>(__t10);
    ((a = 1), (b = 2)), (c = 3);
    

2. 通用工具库的新增内容

  • std::optional (C++17)

    std::optional 用于管理一个可能存在的对象及其生存期。其管理的对象可能在 std::optional 外构造或析构,类似指针,但与指针不同的是 std::optional 为值语义,其内部包含的值不会发生动态内存分配。

    虽然 std::optional 为值语义,但其接口和使用上很多地方和指针有相似之处。例如,若其不含值,或初始化为空时,内部包含 std::nullopt 对象,类似 nullptr。 此外它也有 operator*()operator->(),若内部不含值则出现未定义行为,也与指针类似。检查其内部是否含值可以使用 has_value(),或者类似指针直接隐式转换为 bool

    除了使用类似指针的解引用运算外,也可使用 value() 访问内部值,或者使用 value_or() 当值存在时获取内部值,当值不存在时获取传入的另一个值。reset() 可以用于销毁当前内部值。

    std::optional<std::pair<int, int>> op = std::nullopt;
    if (!op) {
        op = std::make_pair(0, 1);
    }
    std::cout << op->first << " " << op->second << std::endl; // 0 1
    op.reset();
    std::cout << op.value_or(std::make_pair(2, 3)).first << std::endl; // 2, op.has_value() = false
    

    std::optional 在类型理论中属于和类型,更适合用于表示可能失败的值。但由于此特性在 C++17 才引入,而 C++ 之前更多使用指针、特殊返回值或类似 std::setinsert 返回 std::pair<iterator, bool> 等方法进行这种判断,因此此特性使用并不太广泛。

  • std::variant, std::any (C++17)

    略。