Contents

常用的C++ 新特性(二)

本文采用知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,可适当缩放并在引用处附上图片所在的文章链接。

更方便的就地初始化

在定义类成员变量时直接初始化,如下:

1
2
3
4
class Device{
private:
    int device_id = 0;
}

另外,还可以直接初始化结构体成员:

1
2
3
4
5
6
7
typedef struct _Channel{
    int id;
    string name;
    int other;
}Channel;
Channel ch{1,"CH1",128};
Channel *ch_ptr = new Channel{1,"CH1",128};

也可以直接初始化类中的结构体成员

1
2
3
4
5
6
class Device{
private:
    int device_id = 0;
    Channel ch;
}
Device dev{ch{1,"CH1",128}};

也可以直接初始化各种STL容器

1
2
3
vector<int> vec = {1,2,3};
vector<int> vec{1,2,3};
map<string, int> m = { { "debug", 1}, {"self", 2}};

上面的例子中,初始化vector等容器的元素的个数是不固定的,这个如何做到的呢?有兴趣的话可以进一步研究初始化列表std::initializer_list,使用std::initializer_list可以初始化自定义的类型,这样就可以用多个个数不固定的元素,来初始化自定义类型。

类型推导

auto

auto可以让编译器在编译器就推导出变量的类型,看代码:

1
2
3
4
5
6
7
8
9
auto a = 10; // 10是int型,可以自动推导出a是int

int i = 10;
auto b = i; // b是int型

auto d = 2.0; // d是double型
auto f = []() { // f是啥类型?直接用auto就行
    return std::string("d");
}

利用auto可以通过=右边的类型推导出变量的类型。

什么时候使用auto呢?简单类型其实没必要使用auto,然而某些复杂类型就有必要使用auto,比如lambda表达式的类型,async函数的类型等,例如:

1
2
3
4
auto func = [&] {    
    cout << "xxx";
}; // 对于func你难道不使用auto吗,反正我是不关心lambda表达式究竟是什么类型。
auto asyncfunc = std::async(std::launch::async, func);

decltype

decltype和auto都可以进行自动类型推导,但他们有几个差异点: (1). auto需要初始化操作, decltype 除推导引用外不需要初始化. (2). auto 推导表达式类型时计算了表达式的值, decltype只是推导出表达式返回类型,并不只算标定式的值. (3). auto 忽略变量的const性质, decltype则保留变量的const性质. (4). 对于引用, auto 推导出引用的原有类型, 而decltype则推导出引用 用法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 int val = 5;
 decltype(val) val1 = 6;//val1为int类型, 初始化赋值为6
 decltype(val) val2; //val2为int类型, 可以不用初始化
  //auto val3;//编译不通过, 因为auto必须初始化
 
 const int vala = 7;
 decltype(vala) vala1 = 6;//vala1为const int类型, 初始化赋值为6
 // vala1 = 8; //编译不通过
 auto vala2 = vala;
 vala2 = 8; //编译通过,因为vala2类型为int类型而不是const int

  int valb = 8;
  int& valb1 = valb;
  decltype(valb1) valb2 = valb; //valb2为valb的引用
  valb2 = 10;
  std::cout<<"valb: "<<valb<<std::endl;//此时valb == 10
  
  auto valb3 = valb;
  valb3 = 11;
  std::cout<<"valb: "<<valb<<std::endl;//此时valb仍为10, 
  //因为valb3不是valb的引用, valb3值的改变并不影响valb的值
  
auto fun = [](auto a, auto b)->int{
    int sum = a + b;
    std::cout<<"sum: "<<sum<<std::endl;
    return sum;
  };
  auto fun1 = fun(1,2);
  std::cout<<"fun1: "<<fun1<<std::endl;//此时fun1类型为int, 值为3
  //并打印出sum: 3,表示计算了表达式的值并返回给fun1

  decltype(fun(1,2)) fun2;
  std::cout<<"fun2: "<<fun2<<std::endl;//此时fun1类型为int, 值为0
  //并不打印出sum: 3, 表示未计算表达式的值

智能指针

C++11新特性中主要有两种智能指针std::shared_ptr和std::unique_ptr。

那什么时候使用std::shared_ptr,什么时候使用std::unique_ptr呢?

  • 当所有权不明晰的情况,有可能多个对象共同管理同一块内存时,要使用std::shared_ptr;
  • 而std::unique_ptr强调的是独占,同一时刻只能有一个对象占用这块内存,不支持多个对象共同管理同一块内存。

两类智能指针使用方式类似,拿std::unique_ptr举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using namespace std;

struct A {
   ~A() {
       cout << "A delete" << endl;
   }
   void Print() {
       cout << "A" << endl;
   }
};


int main() {
   auto ptr = std::unique_ptr<A>(new A);
   auto tptr = std::make_unique<A>(); // error, c++11还不行,需要c++14
   std::unique_ptr<A> tem = ptr; // error, unique_ptr不允许移动,编译失败
   ptr->Print();
   return 0;
}

std::lock相关

C++11提供了两种锁封装,通过RAII方式可动态的释放锁资源,防止编码失误导致始终持有锁。

这两种封装是std::lock_guard和std::unique_lock,使用方式类似,看下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std;
std::mutex mutex_;

int main()
{
  auto func1 = [](int k) {
    // std::lock_guard<std::mutex> lock(mutex_);
    std::unique_lock<std::mutex> lock(mutex_);
    for (int i = 0; i < k; ++i)
    {
      cout << i << " ";
    }
    cout << endl;
  };
  std::thread threads[5];
  for (int i = 0; i < 5; ++i)
  {
    threads[i] = std::thread(func1, 200);
  }
  for (auto &th : threads)
  {
    th.join();
  }
  return 0;
}

普通情况下建议使用std::lock_guard,因为std::lock_guard更加轻量级,但如果用在条件变量的wait中环境中,必须使用std::unique_lock。

条件变量

条件变量是C++11引入的一种同步机制,它可以阻塞一个线程或多个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock。

这里使用条件变量实现一个CountDownLatch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class CountDownLatch {
   public:
    explicit CountDownLatch(uint32_t count) : count_(count);

    void CountDown() {
        std::unique_lock<std::mutex> lock(mutex_);
        --count_;
        if (count_ == 0) {
            cv_.notify_all();
        }
    }

    void Await(uint32_t time_ms = 0) {
        std::unique_lock<std::mutex> lock(mutex_);
        while (count_ > 0) {
            if (time_ms > 0) {
                cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
            } else {
                cv_.wait(lock);
            }
        }
    }

    uint32_t GetCount() const {
        std::unique_lock<std::mutex> lock(mutex_);
      return count_;
    }

   private:
    std::condition_variable cv_;
    mutable std::mutex mutex_;
    uint32_t count_ = 0;
};

原子操作

C++11提供了原子类型std::atomic,用于原子操作,使用这种方式既可以保证线程安全,也不需要使用锁来进行临界区保护,对一些普通变量来说尤其方便,看代码:

1
2
3
4
5
std::atomic<int> atomicInt;
atomicInt++;
atomicInt--;
atomicInt.store(2);
int value = atomicInt.load();

多线程

什么是多线程这里就不过多介绍,新特性关于多线程最主要的就是std::thread的使用,它的使用也很简单,看代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

#include <iostream>
#include <thread>

using namespace std;

int main() {
   auto func = []() {
       for (int i = 0; i < 10; ++i) {
           cout << i << " ";
      }
       cout << endl;
  };
   std::thread t(func);
   if (t.joinable()) {
       t.detach();
  }
   auto func1 = [](int k) {
       for (int i = 0; i < k; ++i) {
           cout << i << " ";
      }
       cout << endl;
  };
   std::thread tt(func1, 20);
   if (tt.joinable()) { // 检查线程可否被join
       tt.join();
  }
   return 0;
}

这里记住,std::thread在其对象生命周期结束时必须要调用join()或者detach(),否则程序会terminate(),这个问题在C++20中的std::jthread得到解决,但是C++20现在多数编译器还没有完全支持所有特性,先暂时了解下即可,项目中没必要着急使用。

左值右值移动语义相关

大家可能都听说过左值右值,但可能会有部分读者还没有搞清楚这些概念。这里解惑下:

关于左值和右值,有两种方式理解:

概念1

左值:可以放到等号左边的东西叫左值。

右值:不可以放到等号左边的东西就叫右值。

概念2

左值:可以取地址并且有名字的东西就是左值。

右值:不能取地址的没有名字的东西就是右值。

举例来说:

1
2
int a = b + c;
int d = 4; // d是左值,4作为普通字面量,是右值

a是左值,有变量名,可以取地址,也可以放到等号左边, 表达式b+c的返回值是右值,没有名字且不能取地址,&(b+c)不能通过编译,而且也不能放到等号左边。 左值一般有:

  • 函数名和变量名
  • 返回左值引用的函数调用
  • 前置自增自减表达式++i、–i
  • 由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
  • 解引用表达式*p
  • 字符串字面值"abcd"

介绍右值前需要先介绍两个概念:纯右值和将亡值。

运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。例如:

  • 除字符串字面值外的字面值
  • 返回非引用类型的函数调用
  • 后置自增自减表达式i++、i–
  • 算术表达式(a+b, a*b, a&&b, a==b等)
  • 取地址表达式等(&a)

而将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。例如:

1
2
3
4
5
6
class A {
    xxx;
};
A a;
auto c = std::move(a); // c是将亡值
auto d = static_cast<A&&>(a); // d是将亡值

这块的概念太多了,涉及很多知识点,这里不太展开介绍了,具体可以看这篇文章:《左值引用、右值引用、移动语义、完美转发,你知道的不知道的都在这里

std::function和lambda表达式

这两个可以说是我最常用的特性,使用它们会让函数的调用相当方便。使用std::function可以完全替代以前那种繁琐的函数指针形式。

还可以结合std::bind一起使用,直接看一段示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

std::function<void(int)> f; // 这里表示function的对象f的参数是int,返回值是void
#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';

   // 存储到成员函数及对象的调用
   using std::placeholders::_1;
   std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
   f_add_display2(2);

   // 存储到成员函数和对象指针的调用
   std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
   f_add_display3(3);

   // 存储到函数对象的调用
   std::function<void(int)> f_display_obj = PrintNum();
   f_display_obj(18);
}

从上面可以看到std::function的使用方法,当给std::function填入合适的参数表和返回值后,它就变成了可以容纳所有这一类调用方式的函数封装器。std::function还可以用作回调函数,或者在C++里如果需要使用回调那就一定要使用std::function,特别方便。

lambda表达式可以说是C++11引入的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:

1
auto func = [capture] (params) opt -> ret { func_body; };

其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。

看下面这段使用lambda表达式的示例吧:

1
2
auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; }; cout << func1(1) << " " << func2(2) << endl;

std::function和std::bind使得我们平时编程过程中封装函数更加的方便,而lambda表达式将这种方便发挥到了极致,可以在需要的时间就地定义匿名函数,不再需要定义类或者函数等,在自定义STL规则时候也非常方便,让代码更简洁,更灵活,提高开发效率。

lambda 表达式能够方便地构造匿名函数,假设你的代码里面存在大量的小函数,而这些函数一般仅仅被调用一次。那么最好还是将他们重构成 lambda 表达式.

lambda 表达式

C++11 的 lambda 表达式规范例如以下:

**[** capture **]** **(** params **)** mutableexceptionattribute **->** ret **{**body **}** (1)
**[** capture **]** **(** params **)** **->** ret **{** body **}** (2)
**[** capture **]** **(** params **)** **{** body **}** (3)
**[** capture **]** **{** body **}** (4)

当中

  • (1) 是完整的 lambda 表达式形式。
  • (2) const 类型的 lambda 表达式,该类型的表达式不能改捕获(“capture”)列表中的值。
  • (3)省略了返回值类型的 lambda 表达式。可是该 lambda 表达式的返回类型能够依照下列规则推演出来:
    • 假设 lambda 代码块中包括了 return 语句,则该 lambda 表达式的返回类型由 return 语句的返回类型确定。
    • 假设没有 return 语句。则类似 void f(…) 函数。
  • (4)省略了參数列表,类似于无參函数 f()。

mutable 修饰符说明 lambda 表达式体内的代码能够改动被捕获的变量。而且能够訪问被捕获对象的 non-const 方法。

exception 说明 lambda 表达式是否抛出异常(**noexcept**)。以及抛出何种异常,类似于void f()throw(X, Y)。

attribute 用来声明属性。

另外,capture 指定了在可见域范围内 lambda 表达式的代码内可见得外部变量的列表。详细解释例如以下:

  • **[a,&b]** a变量以值的方式呗捕获,b以引用的方式被捕获。
  • **[this]** 以值的方式捕获 this 指针。
  • **[&]** 以引用的方式捕获全部的外部自己主动变量。
  • **[=]** 以值的方式捕获全部的外部自己主动变量。
  • **[]** 不捕获外部的不论什么变量。

此外,params 指定 lambda 表达式的參数。

1
2
3
int main()
{ Test test; test.Add(add, 1, 2); TestAdd testAdd; test.Add(std::bind(&TestAdd::Add, testAdd, std::placeholders::_1, std::placeholders::_2), 1, 2); test.Add([](int a, int b)->int { std::cout << "lamda add fun" << std::endl; return a + b; },1,2); return 0;
}

std::file_system

C++17正式将file_system纳入标准中,提供了关于文件的大多数功能,基本上应有尽有,这里简单举几个例子:

1
2
3
4
5
namespace fs = std::filesystem;
fs::create_directory(dir_path);
fs::copy_file(src, dst, fs::copy_options::skip_existing);
fs::exists(filename);
fs::current_path(err_code);

file_system之前,想拷贝个文件、获取文件信息等都需要使用好多C语言API搭配使用才能完成需求,而有了file_system,一切都变得相当简单。file_system是C++17才引入的新功能,但其实在C++14中就可以使用了,只是file_system在std::experimental空间下。

std::chrono

chrono很强大,也是我常用的功能,平时的打印函数耗时,休眠某段时间等,我都是使用chrono。

在C++11中引入了duration、time_point和clocks,在C++20中还进一步支持了日期和时区。这里简要介绍下C++11中的这几个新特性。

duration

std::chrono::duration表示一段时间,常见的单位有s、ms等,示例代码:

1
2
// 拿休眠一段时间举例,这里表示休眠100ms
std::this_thread::sleep_for(std::chrono::milliseconds(100));

sleep_for里面其实就是std::chrono::duration,表示一段时间,实际是这样:

1
2
typedef duration<int64_t, milli> milliseconds;
typedef duration<int64_t> seconds;

duration具体模板如下:

1
template <class Rep, class Period = ratio<1> > class duration;

Rep表示一种数值类型,用来表示Period的数量,比如int、float、double,Period是ratio类型,用来表示【用秒表示的时间单位】比如second,常用的duration已经定义好了,在std::chrono::duration下:

  • ratio<3600, 1>:hours
  • ratio<60, 1>:minutes
  • ratio<1, 1>:seconds
  • ratio<1, 1000>:microseconds
  • ratio<1, 1000000>:microseconds
  • ratio<1, 1000000000>:nanosecons]

ratio的具体模板如下:

1
template <intmax_t N, intmax_t D = 1> class ratio;

N代表分子,D代表分母,所以ratio表示一个分数,我们可以自定义Period,比如ratio<2, 1>表示单位时间是2秒。

time_point

表示一个具体时间点,如2020年5月10日10点10分10秒,拿获取当前时间举例:

1
2
3
4
std::chrono::time_point<std::chrono::high_resolution_clock> Now() {
   return std::chrono::high_resolution_clock::now();
}
// std::chrono::high_resolution_clock为高精度时钟,下面会提到

clocks

时钟,chrono里面提供了三种时钟:

  • steady_clock
  • system_clock
  • high_resolution_clock

steady_clock

稳定的时间间隔,表示相对时间,相对于系统开机启动的时间,无论系统时间如何被更改,后一次调用now()肯定比前一次调用now()的数值大,可用于计时。

system_clock

表示当前的系统时钟,可以用于获取当前时间:

1
2
3
4
5
6
7
8
int main() {
   using std::chrono::system_clock;
   system_clock::time_point today = system_clock::now();
   std::time_t tt = system_clock::to_time_t(today);
   std::cout << "today is: " << ctime(&tt);
   return 0;
}
// today is: Sun May 10 09:48:36 2020

high_resolution_clock

high_resolution_clock表示系统可用的最高精度的时钟,实际上就是system_clock或者steady_clock其中一种的定义,官方没有说明具体是哪个,不同系统可能不一样,我之前看gcc chrono源码中high_resolution_clock是steady_clock的typedef。

tuple元组

元组类似具有多列的记录,举例,保存人员信息时,先定义如下结构体

1
2
3
4
5
typedef struct _Person{
int id;
string name;
int age;
}Person;

修改为使用tuple,可以如下定义

1
typedef tuple<int,string,int> Person;

操作元组

1
2
3
4
5
Person person = make_tuple(1, "DebugSelf", 18); // 构造元组
cout<<"ID:"<<get<0>(person)<<endl;              // 读取元组的成员
cout<<"Name:"<<get<1>(person)<<endl;
cout<<"Age:"<<get<2>(person)<<endl;
get<2>(person) = 30;                            // 修改元组的成员

新增类型转换函数std::to_string

在C++11之前,要想把int转换为string,需要

1
2
3
4
int value=10;
char buff_temp\[100\]={0};
sprintf(buff_temp,"%d",value);
string str = buff_temp;

每次写这段转换代码时,心里都在暗骂,MD这个功能不应该是标准库自带的吗?C++11后终于引入了to_string把int转换为string,上述代码简化为:

1
string str=to_string(value);

to_string其实功能还比较弱,连转换格式控制都不提供,不过总比没有强吧,汗。

另外,std::stoi/stol/stoll是把string转换为数值。

for_each

使用for_each,可以针对容器中的元素,依次调用指定的函数

1
2
3
4
5
6
7
void print(int& elem) {
    cout << elem << endl;
}
int main() {
    vector<int> vec{0,1,2,3,4,5,6,7,8,9};
    for_each(vec.begin(), vec.end(), print);
}

参考

en.cppreference

我常用的 10 个 C++ 新特性

C++11常用新特性使用经验总结