易用特性

auto

自动类型推断,就是编译器能够根据表达式的类型,自动决定变量的类型。

从 C++14 开始,还有函数的返回类型。

auto 并没有改变 C++ 是静态类型语言这一事实,使用 auto 的变量(或函数返回值)的类型仍然是编译时就确定了,只不过编译器能自动帮你填充。

遍历函数要求支持 C 数组,不用自动类型推断。


template <typename T, std::size_t N>
void foo(const T (&a)[N])
{
  typedef const T* ptr_t;
  for (ptr_t it = a, it_end = a + N;it != it_end; ++it) {
  }
}

template <typename T>
void foo(const T& c)
{
  for (typename T::const_iterator it = c.begin(),it_end = c.end();it != it_end; ++it) {
  }
}

对模板函数声明为⼀个指向数组的引⽤使得我们可以在模板函数中推导出数组的⼤小:

template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept{
	return N;
}

使用自动类型推断,再加上 C++11 提供的全局 begin 和 end 函数。

template <typename T>
void foo(const T& c)
{
  using std::begin;
  using std::end;
  for (auto it = begin(c),it_end = end(c);it != it_end; ++it) {
  }
}

auto 使用的规则类似于函数模板参数的推导规则,含 auto 的表达式时,等于 auto 替换为模板参数的结果。

  • auto a = expr:用 expr 去匹配一个假想的 template <typename T> f(T)函数模板,结果为值类型。
  • const auto& a = expr:用 expr 去匹配一个假想的 template <typename T> f(const T&) 函数模板,结果为常左值引用类型。
  • auto&& a = expr:用 expr 去匹配一个假想的 template <typename T> f(T&&) 函数模板,根据转发引用和引用坍缩规则,结果是一个跟 expr 值类别相同的引用类型。

decltype

decltype :获得一个表达式的类型,结果可以跟类型一样使用。

  • decltype(变量名)可以获得变量的精确类型。

  • decltype(表达式) (表达式不是变量名,但包括 decltype((变量名)) 的情况)

    可以获得表达式的引用类型;表达式的结果是个纯右值(prvalue)时结果仍然是值类型。

    decltype(a) 会获得 int(a 是 int)

    decltype((a)) 会获得 int&( a 是 lvalue)

    decltype(a + a) 会获得 int( a + a 是 prvalue)

根据类型推导规则,auto 是值类型,auto& 是左值引用类型,auto&& 是转发引用(可以是左值引用,也可以是右值引用)。使用 auto 不能通用地根据表达式类型来决定返回值的类型。

关于 auto&& a = expr;

左值得到左值引用,右值得到右值引用(但要注意右值引用是个左值)。

int x = 42;
int& a = x;
int&& b = 42;
auto&& c = a; // int&
auto&& d = b; // int&
auto&& e = std::move(b); // int&&

decltype(expr) 既可以是值类型,也可以是引用类型。可以这么写:

decltype(expr) a = expr;

C++14 引入了 decltype(auto) 语法,只需要像下面这样写:

decltype(auto) a = expr;

这种代码主要用在通用的转发函数模板中:你可能根本不知道你调用的函数是不是会返回一个引用。

函数返回值类型推断

从 C++14 开始,函数的返回值也可用 auto 或 decltype(auto) 来声明。

后置返回值类型声明

通常,在返回类型比较复杂、特别是返回类型跟参数类型有某种推导关系时会使用这种语法。

auto foo(参数) -> 返回值类型声明
{
  // 函数体
}

类模板的模板参数推导

pair<int, int> pr{1, 2};
auto pr = make_pair(1, 2);
pair pr{1, 2};// cpp17

int a1[] = {1, 2, 3};
array<int, 3> a2{1, 2, 3}; // OK
// array<int> a3{1, 2, 3}; // ERROR
array a{1, 2, 3};		   // cpp17

自动推导机制,可以是编译器根据构造函数来自动生成:

template <typename T>
struct MyObj {
  MyObj(T value);
};

MyObj obj1{string("hello")}; // MyObj<string>
MyObj obj2{"hello"};		 // MyObj<const char*>

自动推导机制,可以是手工提供一个推导向导,达到自己需要的效果:

template <typename T>
struct MyObj {
  MyObj(T value);
};

MyObj(const char*) -> MyObj<string>;

MyObj obj{"hello"}; // MyObj<string>

结构化绑定

multimap<string, string>::iterator m_map_begin, m_map_end;
tie(m_map_begin, m_map_end) = m_map.equal_range("A");

返回值是 pair,希望用两个变量来接收数值,就不得不声明了两个变量,然后使用 tie 来接收结果。

C++11/14 里,是没法使用 auto 的。

C++17 引入了一个新语法:(auto 声明变量来分别获取 pair 或 tuple 返回值里各个子项)

auto [m_map_begin, m_map_end] = m_map.equal_range("four");

统一初始化

C++ 标准委员会引入了列表初始化,允许以更简单的方式来初始化对象。初始化容器也可以和初始化数组一样了:

int a[] = {1, 2, 3};
vector<int> v{1, 2, 3};

从技术角度,编译器只是对 {1, 2, 3} 这样的表达式自动生成一个初始化列表,这里类型是 initializer_list

只需要声明一个接受 initializer_list 的构造函数即可使用。

C++11 引入的新语法,能够代替很多() 在变量初始化时使用,被称为统一初始化(uniform initialization)。

class utf8_to_wstring {
public:
  utf8_to_wstring(const char*);
  operator wchar_t*();
};

在 Windows 下想使用这个类来帮助转换文件名,打开文件:

ifstream ifs(utf8_to_wstring(filename));

ifs 的行为无论如何都不正,上面这个写法会被编译器认为是和下面的写法等价的:

ifstream ifs(utf8_to_wstring filename);

编译器认为是声明了一个叫 ifs 的函数,而不是对象!

把任何一对小括号替换成大括号(或者都替换),则可以避免此类问题:

ifstream ifs{utf8_to_wstring{filename}};

几乎可以在所有初始化对象的地方使用大括号而不是小括号。它还有一个附带的特点:当一个构造函数没有标成 explicit 时,如果调用上下文要求那类对象的话,你可以使用大括号不写类名来进行构造。

class Obj{
public:
    double a;
    Obj(double _a): a(_a){}
};

Obj f(double _a)
{
    return {_a};
}

int main()
{   
    cout<< f(1.0).a << endl;
    return 0;
}

如果 Obj 类可以使用浮点数进行构造的话,上面的写法就是合法的。如果有无参数、多参数的构造函数,也可以使用这个形式。除了形式上的区别,它跟 Obj(1.0) 的主要区别是,后者可以用来调用 Obj(int),而使用大括号时编译器会拒绝窄转换,不接受以 {1.0} 或 Obj{1.0} 的形式调用构造函数 Obj(int)。

类数据成员的默认初始化

C++11 增加了一个语法,允许在声明数据成员时直接给予一个初始化表达式。

class Complex {
public:
  Complex() {}
  Complex(float _re) : re(_re) {}
  Complex(float _re, float _im): re(_re) , im(_im) {}

private:
  float re{0};
  float im{0};
};

第一个构造函数没有任何初始化列表,所以类数据成员的初始化全部由默认初始化完成,re和 im都是 0。

第二个构造函数提供了 re 的初始化,im 仍由默认初始化完成。

第三个构造函数则完全不使用默认初始化。

自定义字面量

字面量(literal)是指在源代码中写出的固定常量。

在 C++98 里只能是原生类型,如"hello",字符串字面量,类型是 const char[6]。

C++11 引入了自定义字面量,可以使用 operator"" 后缀 来将用户提供的字面量转换成实际的类型。

C++14 则在标准库中加入了不少标准字面量。

自己的类里支持字面量也相当容易,唯一的限制是非标准的字面量后缀必须以下划线 _ 打头。

namespace m_t{
static const double ratios[3] = {1.0, 60.0, 3600.0};

class m_time{
public:
    enum units{second, minute, hour};

    explicit m_time(double _val, units _unit = second)
    {
        val = _val * ratios[_unit];
    }
    
    double get() const noexcept
    {
        return val;
    }

private:
    double val;
};

m_time operator+(m_time A, m_time B)
{
    return m_time(A.get() + B.get());
}

m_time operator"" _s(long double _val)
{ 
    return m_time(_val, m_time::second);
}

m_time operator"" _m(long double _val)
{ 
    return m_time(_val, m_time::minute);
}

m_time operator"" _h(long double _val)
{ 
    return m_time(_val, m_time::hour);
}

}

测试

using namespace m_t;
cout << (1.0_s + 1.0_m + 1.0_h).get() << endl; //3661

静态断言

C++98 的 assert 允许在运行时检查一个函数的前置条件是否成立。

C++11 直接从语言层面提供了静态断言机制,可以直接放在类的定义中。

static_assert(编译期条件表达式, 可选输出信息);

=delete 和 =default

编译器默认为一个类生成的默认函数

  • 默认构造函数
  • 默认析构函数
  • 默认拷贝构造函数
  • 默认赋值函数
  • 移动构造函数
  • 移动拷贝函数
class Obj {
public:
    Obj ()                  			// default constructor
    ~Obj ()                 			// destructor

    Obj (const Obj & rhs)              // copy constructor
    Obj & operator=(const Obj & rhs)   	// copy assignment operator

    Obj (const Obj && rhs)         		// C++11, move constructor
    Obj & operator=(Obj && rhs)    		// C++11, move assignment operator
};

=delete 禁止使用编译器默认生成的函数

不想使用其中某个,可以使用=delete

delete 关键字可用于任何函数,不仅仅局限于类的成员函数。

在模板特例化中,可以用delete来过滤一些特定的形参类型

= defaule 要求编译器生成一个默认函数。

override 和 final

override 和 final 是两个 C++11 引入的新说明符。

仅在出现在函数声明尾部时起作用,不影响我们使用这两个词作变量名等其他用途。

这两个说明符可以单个或组合使用,都是加在类成员函数声明的尾部。

override 显式声明了成员函数是一个虚函数且覆盖了基类中的该函数。如果有 override 声明的函数不是虚函数,或基类中不存在这个虚函数,编译器会报告错误。

  • 给开发人员更明确的提示,这个函数覆写了基类的成员函数;
  • 让编译器进行额外的检查,防止程序员拼写错误或代码改动没有让基类和派生类中的成员函数名称完全一致。

final 声明了成员函数是一个虚函数,且该虚函数不可在派生类中被覆盖。

final 标志某个类或结构不可被派生。这时应将其放在被定义的类或结构名后面。

class A {
public:
  virtual void foo();
  virtual void bar();
  void foobar();
};

class B : public A {
public:
  void foo() override; // OK
  void bar() override final; // OK
  //void foobar() override;
  // 非虚函数不能 override
};

class C final : public B {
public:
  void foo() override; // OK
  //void bar() override;
  // final 函数不可 override
};

class D : public C {
  // 错误:final 类不可派生
};