C++ 17 实用新特性总结

距离C++ 17已经过去两年了,但是大部分同学可能并没有用到其提供的遍历工具,由其是标准模板库STL的扩增和改善。下面博主将作为一个学习者,向大家介绍一下C++ 11以来增加的便利工具,希望提高大家的编码效率。

关键字 auto

auto关键字实际上有很多小伙伴已经非常熟练的运用了,但是你真的了解他的特性吗。auto用模板实参推导的规则推导类型,并且可伴随如 const 或 & 这样的修饰符,进行类型推导。

值得注意的是

  • 若表达式带有const时,auto不会保留const
  • auto与引用结合,表达式带有const时,auto才保留const属性

所以推荐使用auto进行推导时,配合const 和 & 修饰符使用,让代码更加可控。

当然其也可用于函数定义:

template <class T1,class T2>
auto add(T1 x,T2 y)->decltype(x+y){
	return x+y;
}

编译器将自动推导其函数类型。

模板参数推导

在C++17之前,我们需要使用<>进行类型声明,现在编译器可以帮我们自动推导类型,具体代码如下:

std::vector array{1, 2, 3};

虽然有auto关键字和模板参数自动推导的加持,但是有些情况下,编译器无法对类模板的参数做出自动推导,比如下面这种模板参数类型是个嵌套类型的情况。此时我们需要添加自动推断向导来帮助编译器来进行自动推导。

假如类模板名为container,参数列表为Ts...自动推断向导形式如下:

container(Ts...) -> container<Ts...>

具体的代码实现如下

template<typename T>
struct Tree { using type = T; };

template<typename T>
struct Plant
{
    Plant(typename Tree<T>::type v) {}
};

template<typename T>
Plant(T) -> Plant<T>;

int main()
{
    Plant plant{0};
}

元组类 std::tuple

相信大家都用过std::pair,其是一种结构体模板,实现两个元素的组合,可以看为一种特殊的元组类std::tuple,只能包含两种元素。这样解释大家应该了解其作用了。当然用过Python的同学也经常元组这样一类变量。直接上代码。

#include <tuple>
#include <string>
int main()
{
    auto student = std::tuple{4.5, 'A', "flame"};

    double gpa;
    char grade;
    std::string name;
    
	//get 获取单个元素
    gpa = std::get<0>(student);
    grade = std::get<1>(student);
    name = std::get<2>(student); 
       
    //tie 解包
    std::tie(gpa, grade, name) = student;
    
    // C++17 结构化绑定:
    auto [ gpa2, grade2, name2 ] = student;
}

上述代码中使用了三种方式获取std::tuple的元素

  1. std::tie用于创建左值引用的 tuple,并将 tuple 解包为独立对象。
  2. 而在C++ 17之后使用关键字auto结构化绑定则无需借助std::tie函数了。
  3. 使用std::get函数进行单个元素获取。同时还有类似的函数定义对于std::pairstd::arraystd::tuplestd::variant进行元素获取。

智能指针 std::share_ptr

大部分人对于智能智能一定都有所了解,但是我还是想讲一下,这是因为前段时间看c++并发编程时看到的一段代码,书中讲到两种实现方法,代码如下:

第一种实现方法,直接使用全局的互斥锁进行,权限的锁定和解锁:

#include <mutex>
std::mutex mt;
void addmethod(int a)
{
    mt.lock();
    addprocessing(...);
    mt.unlock();
}
void deletemethod(int a)
{
    mt.lock();
    deleteprocessing(...);
    mt.unlock();
}

第二种实现方法,使用智能锁对全局互斥做进行权限获取和释放:

std::mutex mt;
void addmethod(int a)
{
    std::unique_lock<std::mutex> lock(mt);
    addprocessing(...);
}
void deletemethod(int a)
{
    std::unique_lock<std::mutex> l(mt);
    deleteprocessing(...);
}

一开始只是以为智能锁是一种更高级的实现方法,但是实际上是为了避免mt.lock()mt.unlock()不成对出现导致的线程锁保持不释放,永远阻塞线程导致异常,这是因为这两个语句调用之间语言有可能出现异常或者提前跳出该函数。所以有了智能锁,智能锁声明之初便将权限获取到手,并在智能锁这个变量在析构时释放权限,这样使得mt.lock()mt.unlock()总能成对出现,因为在跳出函数体时,所有变量均会被释放。

下面讲一下常用的三个智能指针的区别:

  • std::unique_ptr是通过指针占有并管理另一对象,并在 unique_ptr离开作用域时释放该对象的智能指针。复制时,会复制指针并转移所有权给目标。注意该方法主要用于单例模式,赋值操作可能导致异常在下列两者之一发生时用关联的删除器释放对象:

    • 销毁了管理的 unique_ptr对象
    • 通过 operator= 或 reset() 赋值另一指针给管理的 unique_ptr 对象。
  • std::share_ptr 是通过指针保持对象共享所有权的智能指针。多个 share_ptr对象可占有同一对象。但只能通过复制构造或复制赋值其值给另一 share_ptr ,将对象所有权与另一 share_ptr 共享。对于我来说基本上只用std::share_ptr,感觉好的的东西驾驭不了反而不好了下列情况之一出现时销毁对象并解分配其内存:

    • 最后剩下的占有对象的 share_ptr被销毁;
    • 最后剩下的占有对象的 share_ptr被通过 operator= 或 reset() 赋值为另一指针。
  • std::weak_ptr 是一种智能指针,它对被 std::share_ptrs 管理的对象存在非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::share_ptrs

    • std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用std::weak_ptr来跟踪该对象。需要获得临时所有权时,则将其转换为 std::share_ptrs,此时如果原来的 std::share_ptrs 被销毁,则该对象的生命期将被延长至这个临时的 std::share_ptrs同样被销毁为止。
    • std::weak_ptr 的另一用法是打断 std::share_ptrs 所管理的对象组成的环状引用。若这种环被孤立(例如无指向环中的外部共享指针),则 shared_ptr 引用计数无法抵达零,而内存被泄露。能令环中的指针之一为弱指针以避免此情况。

引用包装器 std::reference_wrapper

我们知道引用类型作为一种左值类型,实现std::vector<const T &>,所以我们无法实现这种引用数组甚至动态引用数组,加入我们希望对一系列数据使用两种数据结构存储操作,我们如何实现呢?直接上代码:

#include <algorithm>
#include <list>
#include <vector>
#include <iostream>
#include <numeric>
#include <random>
#include <functional>
int main()
{
    std::list<int> l(10);
    std::iota(l.begin(), l.end(), -4);
    std::vector<std::reference_wrapper<int>> v(l.begin(), l.end());
    // 随机打乱
    std::shuffle(v.begin(), v.end(), std::mt19937{std::random_device{}()});
    for (int n : l) std::cout << n << ' '; std::cout << '\n';
    for (int i : v) std::cout << i << ' '; std::cout << '\n';
    for (int& i : l) {
        i *= 2; //对链表元素乘以二
    }
    for (int i : v) std::cout << i << ' '; std::cout << '\n';
}

实验结果如下:

-4 -3 -2 -1 0 1 2 3 4 5
5 0 -2 3 1 -3 2 -4 -1 4
10 0 -4 6 2 -6 4 -8 -2 8

可见对于list进行操作,同时vector中的元素也相应的进行了变化。同时该模板类有自己的对象生成模板函数std::cref(t)std::ref(t) 。值得注意的是:std::cref(t) 返回的是 std::reference_wrapper<const T>(t)即可用于修改,而std::ref(t) 返回的是std::reference_wrapper<T>(t) 即不可修改;

转发调用包装器 std::bind

函数模板 bind 的作用是绑定一或多个实参到函数对象;即实现了类似于默认参数的固定参数。

#include <functional>
#include <iostream>
 
void f(int& n1, int& n2, const int& n3){
    ++n1; 
    ++n2; 
}
 
int main()
{
    int n1 = 1, n2 = 2, n3 = 3;
    std::function<void()> bound_f = std::bind(f, n1, std::ref(n2), std::cref(n3));
    n1 = 10; n2 = 11; n3 = 12;
    std::cout << "Before function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
    bound_f();
    std::cout << "After function: " << n1 << ' ' << n2 << ' ' << n3 << '\n';
}

实验结果如下:

Before function: 10 11 12
After function: 11 12 12

std::bind也可跟std::placeholders配合,std::placeholders主要功能是提供占位符,使得std::bind使用更加灵活,例如使用求平方,求立方便可以使用其实现:

auto square = std::bind(std::pow<double,int>,std::placeholders::_1,2);
std::cout << square(10); //10的平方
auto cube = std::bind(std::pow<double,int>,std::placeholders::_1,3);
std::cout << cube(10); //10的立方

多态函数包装器 std::function

知识补充 什么是Callable类型

A Callable type is a type for which the INVOKE operation (used by, e.g., std::function, std::bind, and std:🧵:thread) is applicable. This operation may be performed explicitly using the library function std::invoke. (since C++17)

简单来说Callable类型指的是所有可进行调用操作的类型

类模板 std::function 是通用多态函数封装器。 std::function 的实例能存储、复制及调用任何可调用 (Callable) 目标。例如:函数、 lambda 表达式、 bind 表达式或其他函数对象,还有指向成员函数指针和指向数据成员指针。

文字叙述可能太抽象直接上代码:

#include <functional>
#include <iostream>
 
struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_+i << '\n'; }
    int num_;
};
 
void print_num(int i)
{
    std::cout << i << '\n';
}
 
struct PrintNum {
    void operator()(int i) const
    {
        std::cout << i << '\n';
    }
};
 
int main()
{
    // 存储自由函数
    std::function<void(int)> f_display = print_num;
    f_display(-9);
 
    // 存储 lambda
    std::function<void()> f_display_42 = []() { print_num(42); };
    f_display_42();
 
    // 存储到 std::bind 调用的结果
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();
 
    // 存储到成员函数的调用
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    f_add_display(314159, 1);
 
    // 存储到数据成员访问器的调用
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';
}

实际上已经有小伙伴考虑到,为什么不使用函数指针进行操作,这是因为这相较于用指针操作更加安全的。

常用算法函数模板

常用的数据结构模板有 : std::bitsetstd::vectorstd::liststd::queuestd::dequepriority_queuestd::stackstd::arraystd::setstd::multisetstd::mapstd::multimap,可能会在下面用到,详细介绍见博文C++ 之 标准模板库(STL)的容器与迭代器
下面介绍一下常用算法的函数模板函数模板

堆操作模板函数

堆是非常常用的一种数据结构,但是通过标准模板库我们便可以实现堆这个数据结构,具体执行堆操作的模板函数如下:

  • meak_heap 将容器内元素堆化
  • push_heap 将元素压入
  • pop_heap 将堆顶元素弹出
  • sort_heap 进行堆排序
  • is_heap 判断是否是堆

注意:堆化操作只能针对随机访问迭代器

#include <iostream>
#include <algorithm>
#include <functional>
#include <vector>
int main()
{
    std::cout << "Max heap:\n";

    std::vector<int> v { 3, 2, 4, 1, 5, 9 };

    std::cout << "initially, v: ";
    for (auto i : v) std::cout << i << ' ';
    std::cout << '\n';

    std::make_heap(v.begin(), v.end(),std::less{});
    std::cout << "after make_heap, v: ";
    for (auto i : v) std::cout << i << ' ';
    std::cout << '\n';

    v.push_back(10);
    std::push_heap(v.begin(), v.end(),std::less{});
    std::cout << "after push_heap, v: ";
    for (auto i : v) std::cout << i << ' ';
    std::cout << '\n';

    std::pop_heap(v.begin(), v.end(),std::less{});
    v.pop_back();
    std::cout << "after pop_heap, v: ";
    for (auto i : v) std::cout << i << ' ';
    std::cout << '\n';

    std::cout << "is heap : "<<(std::is_heap(v.begin(), v.end(),std::less{}) ? "true" : "false") << "\n";
    std::sort_heap(v.begin(), v.end(),std::less{});
    std::cout << "after sort_heap, v: ";
    for (auto i : v) std::cout << i << ' ';
    std::cout << '\n';
    std::cout << "is heap : "<< (std::is_heap(v.begin(), v.end(),std::less{}) ? "true" : "false") << "\n";
}

运行结果:

Max heap:
initially, v: 3 2 4 1 5 9
after make_heap, v: 9 5 4 1 2 3
after push_heap, v: 10 5 9 1 2 3 4
after pop_heap, v: 9 5 4 1 2 3
is heap : true
after sort_heap, v: 1 2 3 4 5 9
is heap : false

红黑树模板类

相比与二叉堆,二叉搜索树的特性更好,虽然其实现过程较为复杂,C++标准模板库已经将其封装为了std::setstd::multisetstd::mapstd::multimap,前两个是标准的红黑树,分别代表了是否含有重复值的红黑树,而后两个与前两个的区别是后两个的节点实现是使用std::pair实现的,不单纯是数组或一个变量。所以map/multimap容器里放着的都是pair模版类的对象,且按first进行排序。

  • 内部元素有序排列,新元素插入的位置取决于它的值,查找速度快。
  • 除了各容器都有的函数外,还支持以下成员函数:
    • find:寻找带有特定键的元素
    • contain:检查容器是否含有带特定关键的元素
    • lower_bound:返回指向首个不小于给定键的元素的迭代器
    • upper_bound:返回指向首个大于给定键的元素的迭代器
    • equal_range:返回匹配特定键的元素范围
    • count:返回匹配特定键的元素数量
    • insert:插入元素或结点

值得注意的是其begin()返回的迭代器会根据比较函数是less还是greator确定是最小值还是最大值。end()则反之。

哈希表模板类

unordered_map 是关联容器,含有带唯一键的键-值 pair 。搜索、插入和元素移除拥有平均常数时间复杂度。

元素在内部不以任何特定顺序排序,而是组织进桶中。元素放进哪个桶完全依赖于其键的哈希。这允许对单独元素的快速访问,因为一旦计算哈希,则它准确指代元素所放进的桶。

#include <iostream>
#include <string>
#include <unordered_map>
 
int main()
{
    // 创建三个 string 的 unordered_map (映射到 string )
    std::unordered_map<std::string, std::string> u = {
        {"RED","#FF0000"},
        {"GREEN","#00FF00"},
        {"BLUE","#0000FF"}
    };
 
    // 迭代并打印 unordered_map 的关键和值
    for( const auto& n : u ) {
        std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
    }
 
    // 添加新入口到 unordered_map
    u["BLACK"] = "#000000";
    u["WHITE"] = "#FFFFFF";
 
    // 用关键输出值
    std::cout << "The HEX of color RED is:[" << u["RED"] << "]\n";
    std::cout << "The HEX of color BLACK is:[" << u["BLACK"] << "]\n";
 
    return 0;
}

排序算法模板函数

在C++的标准库中已有排序算法的实现,在无特殊需求时,我们可以使用标准模板函数进行排序,常用的四个函数为std::sortstd::stable_sortstd::partial_sortstd::list::sort,其使用的是快速排序和归并排序进行实现的,调用方式如下为:

#include <algorithm>
#include <array>
#include <iostream>
#include <list>
//用于链表打印
std::ostream& operator<<(std::ostream& ostr, const std::list<int>& list)
{
    for (auto &i : list) {
        ostr << i << " ";
    }
    return ostr;
}

int main()
{
    std::array<int, 10> s = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3};
    std::cout << "before sort : ";
    for (auto a : s) {
        std::cout << a << " ";
    }
    std::cout << "\n";
    // 用标准库比较函数对象进行快速排序
    std::sort(s.begin(), s.end(), std::less<>());
    std::cout << "after sort : ";
    for (auto a : s) {
        std::cout << a << " ";
    }
    std::cout << ", which is sorted: " << std::boolalpha
              << std::is_sorted(s.begin(), s.end(),std::less{}) << '\n';

    s = std::array<int, 10>{5, 7, 4, 2, 8, 6, 1, 9, 0, 3};
    // 用标准库比较函数对象进行部分快速排序
    std::partial_sort(s.begin(), s.begin() + 3, s.end(), std::less<>());
    std::cout << "after partial sort : ";
    for (int a : s) {
        std::cout << a << " ";
    }
    std::cout << ", which is sorted: " << std::boolalpha
              << std::is_sorted(s.begin(), s.end(),std::less{}) << '\n';

    s = std::array<int, 10>{5, 7, 4, 2, 8, 6, 1, 9, 0, 3};
    // 用标准库比较函数对象进行归并排序
    std::stable_sort(s.begin(), s.end(), std::less<>());
    std::cout << "after stable sort : ";
    for (auto a : s) {
        std::cout << a << " ";
    }
    std::cout << ", which is sorted: " << std::boolalpha
              << std::is_sorted(s.begin(), s.end(),std::less{}) << '\n';

    std::list<int> list = { 8,7,5,9,0,1,3,2,6,4 };
    // 用标准库比较函数对象进行链表的归并排序
    list.sort(std::less<>());
    std::cout << "after list sort : ";
    std::cout << list;
    std::cout << ", which is sorted: " << std::boolalpha
              << std::is_sorted(list.begin(), list.end(),std::less{}) << '\n';
}

执行结果如下:

before sort : 5 7 4 2 8 6 1 9 0 3
after sort : 0 1 2 3 4 5 6 7 8 9 , which is sorted: true
after partial sort : 0 1 2 7 8 6 5 9 4 3 , which is sorted: false
after stable sort : 0 1 2 3 4 5 6 7 8 9 , which is sorted: true
after list sort : 0 1 2 3 4 5 6 7 8 9 , which is sorted: true

其他标准模板库函数可以点击博文C++ 之 标准模板库的七大算法查看,你会发现实际上有库C++也挺智能的。

连续赋值函数模板

在C++语言中,我们也常使用的一些从C语言继承而来的连续内存赋值操作函数,包含在头文件<cstring>中,包含有:std::memsetstd::memcpystd::memmovestd::strcpystd::strncpy

具体含义如下:

memcpy //复制一个缓冲区到另一个
memmove //移动一个缓冲区到另一个
memset //以一个字符填充缓冲区
strcpy //复制一段字符串到另一个
strncpy //复制来自一个字符串的一定量字符给另一个

针对C++标准模板容器,C++的标准模板库也定义了一些函数实现连续赋值。下面介绍一下这些方便的函数工具。


std::fill

std::fill将一个给定值复制赋值给一个范围内的每个元素。其执行结果与以下代码效果一样。

template< class ForwardIt, class T >
void fill(ForwardIt first, ForwardIt last, const T& value)
{
    for (; first != last; ++first) {
        *first = value;
    }
}

类似的函数有std::fill_n 将一个给定值复制赋值给一个范围内的 N 个元素。


std::generate

std::generate相继的函数调用结果赋值给一个范围中的每个元素。其执行结果与以下代码效果一样。

template<class ForwardIt, class Generator>
void generate(ForwardIt first, ForwardIt last, Generator g)
{
    while (first != last) {
        *first++ = g();
    }
}

std::iota
std::iota按顺序递增值进行填充。其执行结果与以下代码效果一样。

template<class ForwardIterator, class T>
void iota(ForwardIterator first, ForwardIterator last, T value)
{
    while(first != last) {
        *first++ = value;
        ++value;
    }
}

std::copy
std::copy将某一范围的元素复制到一个新的位置。实践中,若 value_type 为可平凡复制 (TriviallyCopyable) ,则 std::copy 避免多次赋值并使用大批量复制函数,如 std::memmove 。其执行结果与以下代码效果一样。

template<class InputIt, class OutputIt>
OutputIt copy(InputIt first, InputIt last, 
              OutputIt d_first)
{
    while (first != last) {
        *d_first++ = *first++;
    }
    return d_first;
}

相似的函数还有以下三个:

  • std::copy_backward 按从后往前的顺序复制一个范围内的元素
  • std::reverse_copy 创建一个范围的逆向副本 std::copy_n 将一定数目的元素复制到一个新的位置
  • std::remove_copystd::remove_copy_if 复制一个范围的元素,忽略满足特定判别标准的元素

移动语义 std::move

知识补充:

左值引用:可用于建立既存对象的别名和在函数调用中实现按引用传递语义
右值引用:可用于为临时对象延长生存期(注意,左值引用亦能延长临时对象生存期,但不能通过左值引用修改它们)

一般用一个&修饰符表示左值引用,用两个&修饰符表示右值引用,其他情况下会使用引用坍缩策略(两两相消),降维上述两种情况。

关于引用声明值类别的详细解释可以去网站cppreference查看。

std::move(t) 用于指示对象 t 可以“被移动”,即允许从 t 到另一对象的有效率的资源传递。std::move(t) 是一种亡值表达式。其等价于 static_cast<typename std::remove_reference<T>::type&&>(t)。简单来说就是获取一个右值引用。

std::remove_reference<T> 的功能为 若类型 T 为引用类型,则提供成员 typedef type ,其为 T 所引用的类型。否则 type 为 T 。实际上就是获取一个变量的类型,并把引用修饰符去掉。

右值引用变量的名称是左值,而若要绑定到接受右值引用参数的重载,就必须转换到亡值,此乃移动构造函数与移动赋值运算符典型地使用std::move的原因。

值得注意的是右值引用意味着这是一个亡值,所以若参数标识一个占有资源的对象,拥有移动参数所保有的任何资源的选择。

#include <utility>
#include <vector>
#include <string>
int main()
{
    std::string str = "Hello";
    std::vector<std::string> v;    
    v.push_back(str);
    v.push_back(std::move(str)); 
}
  1. 第一句压入数据使用 push_back(const T&) 重载,通过复制str构造一个元素插入
  2. 第二句压入数据使用右值引用push_back(T&&)重载,其通过移动str构造一个元素插入,虽然开销比较低,但也意味着str所指为空。即通过移动构造函数将str资源占有。

完美转发 std::forward

std::forward用以实现完美转发。完美转发的定义:

一种方法能够按照参数原来的类别转发到另一个函数,这种转发类型称为完美转发

配合完美转发则需要右值引用的另一种用法转发引用,详细解释如下:

转发引用是一种特殊的引用,它保持函数实参的值类别,使得能利用 std::forward 转发实参。

转发引用和std::forward配合使用的代码如下:

template<class T>
void wrapper(T&& arg) 
{
    // arg 始终是左值
    foo(std::forward<T>(arg)); // 转发为左值或右值,依赖于 T
}

简单来说std::forward 等价于 static_cast<typename std::remove_reference<T>::type&&>(t)static_cast<typename std::remove_reference<T>::type&>(t),这取决于输入参数的类型T。为什么需要这么呢?

根据C++ 标准的定义,输入参数arg始终是一个左值。那加入输入参数为右值,如何保持其原始值类别呢。为了解决这个问题 C++ 11引入了完美转发和转发引用。std::forward会根据原始值类别进行转发,保持其左右值的特性
这样的处理就完美的转发了原有参数的左右值属性,不会造成一些不必要的拷贝。

posted @ 2021-04-28 21:41  FlameAlpha  阅读(713)  评论(0编辑  收藏  举报