auto & decltype

  • auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。
auto a = 10; // 10是int型,可以自动推导出a是int
  • decltype:相对于auto用于推导变量类型,而decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。
cont int &i = 1;
decltype(i) b = 2; // b是const int&

autodecltype结合起来有一些有意思的用法,比如自动推导函数的返回值
看下面这段代码

template<typename T, typename U>
return_value add(T t, U u) { // t和u类型不确定,无法推导出return_value类型
  return t + u;
}

那使用decltype

template<typename T, typename U>
decltype(t + u) add(T t, U u) { // 在decltype进行推导时,t和u尚未定义
  return t + u;
}

使用auto配合进行返回值类型后置

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
  return t + u;
}

初始化列表

C++ 11新增了列表初始化的概念。
在C++ 11中可以直接在变量名后面加上初始化列表来进行对象的初始化。

struct A {
	int a;
};
int main() {
  A a = { 123 };
  A d{123}; // c++11
  int e = {123};
  int f{123}; // c++11
  return 0;
}

列表初始化也可以用在函数的返回值上

std::vector<int> func() {     
  return {};
}

初始化列表的使用条件、限制

  • 类型是一个普通数组,如int[5],char[],double[]等
  • 类型是一个类,且满足以下条件
    • 没有用户声明的构造函数
    • 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
    • 没有私有或保护的非静态数据成员
    • 没有基类
    • 没有虚函数
    • 没有{}和=直接初始化的非静态数据成员
    • 没有默认成员初始化器

std::initializer_list
对于STL的初始化列表,发现可以是任意长度。由std::initializer_list实现,类似

struct CustomVec {
  std::vector<int> data;
  CustomVec(std::initializer_list<int> list) {
    for (auto iter = list.begin(); iter != list.end(); ++iter) {
      data.push_back(*iter);
    }
  }
};

注意:std::initializer_list,它可以接收任意长度的初始化列表,但是里面必须是相同类型T,或者都可以转换为T

for循环

vector<int> vec {1, 2, 3};
for (vector<int>::iterator iter = vec.begin(); iter != vec.end(); iter++) {  // before c++11
  cout << "i: " << *iter << endl;
}
for (auto i : vec) {  // c++11基于范围的for循环
  cout << "i: " << i << endl;
}

nullptr

nullptr是c++ 11用来表示空指针新引入的常量值,在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针

void func(void *ptr) {
  cout << "func ptr" << endl;
}

void func(int i) {
  cout << "func i" << endl;
}

int main() {
  func(NULL); // 编译失败,会产生二义性
  func(nullptr); // 输出func ptr
  return 0;
}

final & override

c++11关于继承新增了两个关键字,final用于修饰一个类,表示禁止该类进一步派生和虚函数的进一步重载,override用于修饰派生类中的成员函数,标明该函数重写了基类函数,如果一个函数声明了override但父类却没有这个虚函数,编译报错,使用override关键字可以避免开发者在重写基类函数时无意产生的错误

struct Base {
  virtual void func() {
    cout << "base" << endl;
  }
};

struct Derived : public Base{
  void func() override { // 确保func被重写
    cout << "derived" << endl;
  }
  
  void fu() override { // error,基类没有fu(),不可以被重写
  
  }
};
struct Base final {
  virtual void func() {
    cout << "base" << endl;
  }
};

struct Derived : public Base{ // 编译失败,final修饰的类不可以被继承
  void func() override {
    cout << "derived" << endl;
  }
};

default & delete & explicit

default

c++11引入default特性,多数时候用于声明构造函数为默认构造函数,如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:

struct A {
  int a;
  A(int i) { a = i; }
};

int main() {
  A a; // 编译出错
  return 0;
}

上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过default,程序员只需在函数声明后加上=default;,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体,如下:

struct A {
  A() = default;
  int a;
  A(int i) { a = i; }
};

int main() {
  A a;
  return 0;
}

delete

c++中,如果开发人员没有定义特殊成员函数,那么编译器在需要特殊成员函数时候会隐式自动生成一个默认的特殊成员函数,例如拷贝构造函数或者拷贝赋值操作符,如下代码:

struct A {
  A() = default;
  int a;
  A(int i) { a = i; }
};

int main() {
  A a1;
  A a2 = a1;  // 正确,调用编译器隐式生成的默认拷贝构造函数
  A a3;
  a3 = a1;  // 正确,调用编译器隐式生成的默认拷贝赋值操作符
}

而我们有时候想禁止对象的拷贝与赋值,可以使用delete修饰,如下:

struct A {
  A() = default;
  A(const A&) = delete;
  A& operator=(const A&) = delete;
  int a;
  A(int i) { a = i; }
};

int main() {
  A a1;
  A a2 = a1;  // 错误,拷贝构造函数被禁用
  A a3;
  a3 = a1;  // 错误,拷贝赋值操作符被禁用
}

delele函数在c++11中很常用,std::unique_ptr就是通过delete修饰来禁止对象的拷贝的

explicit

explicit专用于修饰构造函数,表示只能显式构造,不可以被隐式转换,根据代码看explicit的作用:

struct A {
  A(int value) { // 没有explicit关键字
    cout << "value" << endl;
  }
};

int main() {
  A a = 1; // 可以隐式转换
  return 0;
}
struct A {
  explicit A(int value) {
    cout << "value" << endl;
  }
};

int main() {
  A a = 1; // error,不可以隐式转换
  A aa(2); // ok
  return 0;
}

constexpr

constexpr是c++11新引入的关键字,用于编译时的常量和常量函数,这里直接介绍constexpr和const的区别:
两者都代表可读,const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如果编译期间此函数不能被计算出来,那它就会当作一个普通函数被处理。如下代码:

#include<iostream>
using namespace std;

constexpr int func(int i) {
  return i + 1;
}

int main() {
  int i = 2;
  func(i);// 普通函数
  func(2);// 编译期间就会被计算出来
}

enum class

c++11新增有作用域的枚举类型

//传统enum,没有作用域概念(都在全局作用域)
enum AColor {
  kRed,
  kGreen,
  kBlue
};

enum BColor {
  kWhite,
  kBlack,
  kYellow
};

int main() {
  if (kRed == kWhite) {
    cout << "red == white" << endl;
  }
  return 0;
}

如上代码,不带作用域的枚举类型可以自动转换成整形,且不同的枚举可以相互比较,代码中的红色居然可以和白色比较,这都是潜在的难以调试的bug,而这种完全可以通过有作用域的枚举来规避。

enum class AColor {
  kRed,
  kGreen,
  kBlue
};

enum class BColor {
  kWhite,
  kBlack,
  kYellow
};

int main() {
  if (AColor::kRed == BColor::kWhite) { // 编译失败
    cout << "red == white" << endl;
  }
  return 0;
}

使用带有作用域的枚举类型后,对不同的枚举进行比较会导致编译失败,消除潜在bug,同时带作用域的枚举类型可以选择底层类型,默认是int,可以改成char等别的类型。

enum class AColor : char {
  kRed,
  kGreen,
  kBlue
};

std::function & lambda表达式

lambda表达式

它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式

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

其中各部分意义:

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

一个完整的lambda表达式:

auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; };
cout << func1(1) << " " << func2(2) << endl;
  • 允许省略返回值类型和 -> 符号
  • 如果lambda表达式不需要参数,允许省略参数列表
auto lambda = [] { printf("hello lambda"); };

lambda表达式允许捕获一定范围内的变量:

  • []不捕获任何变量
  • [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用
  • [=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用
  • [=, &a]值捕获外部作用域所有变量,按引用捕获a变量
  • [a]只值捕获a变量,不捕获其它变量
  • [this]捕获当前类中的this指针
int a = 0;
auto f1 = [=](){ return a; }; // 值捕获a
cout << f1() << endl;

auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
auto f3 = [=]() mutable { return a++; };

其实lambda表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。

std::function

什么是可调用对象:

  • 是一个函数指针
  • 是一个具有operator()成员函数的类对象(传说中的仿函数),lambda表达式
  • 是一个可被转换为函数指针的类对象
  • 是一个类成员(函数)指针
  • bind表达式或其它函数对象

std::function就是上面这种可调用对象的封装器,可以把std::function看做一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。

#include <functional>
#include <iostream>

using namespace std;
std::function<void(int)> f;  // 这里表示function的对象f的参数是int,返回值是void

void print_num(int i) { std::cout << i << '\n'; }

struct Foo {
Foo(int num) : num_(num) {}
  void print_add(int i) const { std::cout << num_ + i << '\n'; }
  int num_;
};

struct PrintNum {
	void operator()(int i) const { std::cout << i << '\n'; }
};

int main(int argc, char** argv) {
  // 存储自由函数
  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);
  return 0;
}

std::bind

使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何我们需要的时候。
std::bind通常有两大作用:

  • 将可调用对象与参数一起绑定为另一个std::function供调用
  • 将n元可调用对象转成m(m < n)元可调用对象,绑定一部分参数,这里需要使用std::placeholders
#include <functional>
#include <iostream>
#include <memory>

void f(int n1, int n2, int n3, const int& n4, int n5) {
  std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5
    << std::endl;
}

int g(int n1) { return n1; }

struct Foo {
void print_sum(int n1, int n2) { std::cout << n1 + n2 << std::endl; }
int data = 10;
};

int main() {
  using namespace std::placeholders;  // 针对 _1, _2, _3...

  // 演示参数重排序和按引用传递
  int n = 7;
  // ( _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数)
  auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
  n = 10;
  f1(1, 2, 1001);  // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
  // 进行到 f(2, 42, 1, n, 7) 的调用

  // 嵌套 bind 子表达式共享占位符
  auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
  f2(10, 11, 12);  // 进行到 f(12, g(12), 12, 4, 5); 的调用

  // 绑定指向成员函数指针
  Foo foo;
  auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
  f3(5);

  // 绑定指向数据成员指针
  auto f4 = std::bind(&Foo::data, _1);
  std::cout << f4(foo) << std::endl;

  // 智能指针亦能用于调用被引用对象的成员
  std::cout << f4(std::make_shared<Foo>(foo)) << std::endl;
  return 0;
}

rvalue & std::move & std::forward

左值、右值

值得一提的是,左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、“right value” 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 “read value”,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。

概念1:

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

概念2

  • 左值:可以取地址并且有名字的东西就是左值。
  • 右值:不能取地址的没有名字的东西就是右值。
int a = b + c; 

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

int a = 4; // a是左值,4作为普通字面量是右值

左值一般有:

  • 函数名和变量名
  • 返回左值引用的函数调用
  • 前置自增自减表达式++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&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。

左值引用、右值引用

左值引用就是对左值进行引用的类型,右值引用就是对右值进行引用的类型,他们都是引用,都是对象的一个别名,并不拥有所绑定对象的堆存,所以都必须立即初始化。

type &name = exp; // 左值引用
type &&name = exp; // 右值引用
  • 左值引用:对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const引用形式,但这样就只能通过引用来读取输出,不能修改数组,因为是常量引用。
int a = 5;
int &b = a; // b是左值引用
int &c = 10; // error,10无法取地址,无法进行引用
const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址
  • 右值引用:如果使用右值引用,那表达式等号右边的值需要时右值,可以使用std::move函数强制把左值转换为右值。
int a = 4;
int &&b = a; // error, a是左值
int &&c = std::move(a); // ok

std::move

需要了解深拷贝和浅拷贝,不再赘述
如果不使用std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提升程序性能,C++所有的STL都实现了移动语义,方便我们使用。例如:

std::vector<string> vecs;
...
std::vector<string> vecm = std::move(vecs); // 免去很多拷贝

注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。

std::forward

完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用std::forward

void PrintV(int &t) {
  cout << "lvalue" << endl;
}

void PrintV(int &&t) {
  cout << "rvalue" << endl;
}

template<typename T>
void Test(T &&t) {
  PrintV(t);
  PrintV(std::forward<T>(t));

  PrintV(std::move(t));
}

int main() {
  Test(1); // lvalue rvalue rvalue
  int a = 1;
  Test(a); // lvalue lvalue rvalue
  Test(std::forward<int>(a)); // lvalue rvalue rvalue
  Test(std::forward<int&>(a)); // lvalue lvalue rvalue
  Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
  return 0;
}
  • Test(1):1是右值,模板中T &&t这种为万能引用,右值1传到Test函数中变成了右值引用,但是调用PrintV()时候,t变成了左值,因为它变成了一个拥有名字的变量,所以打印lvalue,而PrintV(std::forward(t))时候,会进行完美转发,按照原来的类型转发,所以打印rvalue,PrintV(std::move(t))毫无疑问会打印rvalue。
  • Test(a):a是左值,模板中T &&这种为万能引用,左值a传到Test函数中变成了左值引用,所以有代码中打印。
  • Test(std::forward(a)):转发为左值还是右值,依赖于T,T是左值那就转发为左值,T是右值那就转发为右值。

返回值优化(RVO)

返回值优化(RVO)是一种C++ 编译优化技术,当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++ 标准允许省略调用这些复制构造函数。

什么时候编译器会进行返回值优化:

  • return的值类型与函数的返回值类型相同
  • return的是一个局部对象

示例1:

std::vector<int> return_vector(void) {
  std::vector<int> tmp {1,2,3,4,5};
  return tmp;
}
std::vector<int> &&rval_ref = return_vector();

不会触发RVO,拷贝构造了一个临时的对象,临时对象的生命周期和rval_ref绑定,等价于下面这段代码
const std::vector<int>& rval_ref = return_vector();

示例2:

std::vector<int>&& return_vector(void) {
  std::vector<int> tmp {1,2,3,4,5};
  return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();
这段代码会造成运行时错误,因为rval_ref引用了被析构的tmp

示例3:

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
和示例1类似,std::move一个临时对象是没有必要的,也会忽略掉返回值优化

示例4(触发RVO,不拷贝也不移动,不生成临时对象):

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

Q.E.D.


ALL WILL BE CLEAR