0%

C++11小结

在C++11正式标准发布7年后的今天,总算看到有比较多的公司开始普及使用C++11标准了,虽然编译器早已支持这个标准,但是愿意更新编译器的公司真是少啊,不知道C++14、C++17标准会在多远的未来才能正式出现在我们的代码里。一时兴起,把C++11标准的内容做个总结,一来是温故知新,二来是看看我还记得多少

右值引用和转移语义

C++11标准中添加了一种新的引用类型,右值引用(r-value reference)。所谓右值即出现在赋值等号右边的变量,而右值引用专为绑定临时变量而来,相应的C++11中STL的容器都新增了右值引用的构造和赋值操作符,其中构造被称为“转移构造函数”,比如std::string新增了

1
string (string&& str) noexcept;

其中string&&类型即为string的右值引用,右值引用有别于左值引用的单个“&”符号,写做“&&”以示区分。这些转移构造函数与普通复制构造函数(如 string(const string&) )的区别在于,转移构造函数并不真正复制构造一个对象,而是将该右值对象转移到新构造的对象上。比如上面的string转移构造,只将string&& str这个右值的内部实现的char*等内部实现传给新构造的对象,并将该右值本身的char*等内部实现置零,完成转移。由于转移构造一般会修改右值,所以转移构造的函数签名是string (string&&)而不是string(const string&&)。出现右值引用以及转移构造的原因在于,在C++11前的版本中按值传递对象的方法中容易出现因隐式转换而造成的不必要的拷贝,举个例子:

1
2
vector<Widget> vw;
vw.push_back(Widget());

在没有右值引用的C++老版本中,第二句话的push_back将造成两个Widget对象的构造,第一个为Widget()这个临时对象的默认空构的调用,由于push_back内需要在容器内新增一个Widget对象,在没有转移构造的老版本中,这将调用Widget的复制构造函数,造成一次本身并不需要的深拷贝。在C++11中这个情况得到改善,vector容器新增了右值引用的push_back重载:

1
void push_back (value_type&& val);

临时对象优先绑定到右值引用上,所以上面的push_back中在临时对象构造完成后,将调用Widget的转移构造(如果有)来构造容器上新增的对象,这将免去一次对象的复制操作。可能对于这个例子上你看不出这有什么意义,那么请设想下一些数据对象的拷贝动作。
需要注意的是,在C++11中所有有名字的变量都被认为是左值,即使这个变量的类型被声明为右值,即对于push_back(value_type&& val)内部来说val也仍然是个左值。那也许你看到这会想到这么个问题,这个函数push_back(value_type&&)内部是如何调用对应value_type的转移构造呢?答案就是C++11中新增的std::move。
C++11中在<utility>中新增了这么个方法:

1
2
template <class T>
typename remove_reference<T>::type&& move (T&& arg) noexcept;

这个方法返回一个指向arg的右值引用。所以我们可以像如下一样强制使用转移构造:

1
2
3
Widget tempWidget;
...
Widget widget = std::move(tempWidget);

这样tempWidget的内容就被“转移”到widget上了。

constexpr常量表达式关键字

C++11新增关键字constexpr用于表达常量表达式,在老版本标准中如下的定义无法通过编译:

1
2
3
4
5
6
int GetSize()
{
return 12 * 12;
}

int arraryInt[GetSize()];

所以出现了constexpr关键字来标识函数或者变量的值是个常量,当然你需要保证这个函数或者变量的值是常量

1
2
3
4
5
6
constexpr int GetSize()
{
return 12 * 12;
}

int arraryInt[GetSize()];

在c++11标准中以上代码就能通过编译。对于constexpr关键字修饰的函数,需要做几点保证否则依然无法通过编译:

  1. 函数返回值不能为void
  2. 函数返回值必须依照 return expr形式
  3. expr最后参数替换完必须是一个常量表达式
  4. 该expr表达式只能调用其他constexpr修饰的函数

初始化列表和统一初始化

C风格的数组一般可以通过以下方式初始化:

1
int arrayInt[10] = {1, 2, 3};

这个初始化列表的概念从C中被带至了C++中,而老版本的C++中除了POD(Plain old data structure)类型以外并不能使用该初始化列表,甚至是以数组作为实现的std::vector也不能使用初始化列表。这个问题在C++11中得到解决,C++11新增了一个类型模板std::initializer_list,其位于新头文件<initializer_list>中,这个模板的出现使得stl中各容器的初始化列表方法成为可能。相应的stl中的容器都增加了intializer_list的构造函数:

1
2
3
4
5
6
vector (initializer_list<value_type> il, const allocator_type& alloc = allocator_type());

list (initializer_list<value_type> il, const allocator_type& alloc = allocator_type());

map (initializer_list<value_type> il, const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());

初始化列表{ … }可以被编译器隐式转换成initializer_list<value_type<(虽然你去查initializer_list的构造,将查不到非空的构造),这使得在C++11中我们可以如下初始化容器:

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
#include <vector>
#include <list>
#include <map>
#include <initializer_list>
#include <utility>
class CTest
{
public:
CTest(int i, float f) : m_int(i), m_float(f) {}
int get_int()
{
return m_int;
}
int get_float()
{
return m_float;
}
private:
int m_int;
float m_float;
};
int main()
{
using std::vector;
using std::list;
using std::map;
using std::make_pair;
vector<int> vecTestInt = { 1, 2, 3 };
vector<CTest> vecTestClass = { CTest(1, 1.2f), CTest{2, 2.3f} };

list<CTest> listTest = { CTest(1, 1.2f), CTest{2, 2.3f} };
map<int, CTest> mapTest = { make_pair(1, CTest{1, 1.2f}) };
}

需要注意得是initializer_list并不是一个标准容器,不能使用其作为容器使用,仅仅是初始化列表使用,当然你也可以使用该类模板作为函数参数使用。
如果你细看了上面这份代码,你会发现在构造CTest时我使用了花括号类似初始化列表的方式,这里也是C++11的一个新特性,统一初始化。在C风格的代码中,我们会这样建立一个struct:

1
2
3
4
5
struct STest {
int m_int;
float m_float;
};
STest s{ 1, 2 };

而这个初始化在老版本c++中对于非POD类型并不能使用,所以C++11中为了统一,现在你也可以使用这种方式构造类型,对于上面的CTest{2, 2.3f}的调用实际上是调用了CTest的CTest(int i, float f)构造函数。

类型推导

C++11版本新增两个型别关键字auto和decltype,主要为了解决模板泛型编程中类型不明确类型的定义,而auto自动推导型别关键字的出现也大大减少了冗赘的代码量。所谓auto自动推导型别,故名思意,一个变量的类型可以由编译器推导出,而不需要书写时指明,例如:

1
2
3
4
auto a = 1;
vector<vector<vector<int>>> vecTest;
auto itBegin = vecTest.begin();
decltype(itBegin) itEnd = vecTest.end();

需要注意的是auto必须使用在有被明确初始化参数的情况下,语义上来说,当你大概知道这个变量是什么类型时你可以使用auto,如果你书写时完全不知道这个变量是什么类型那么你最好别使用auto,以及auto在使用上有着和弱类型的脚本语言同样的问题,你需要使用匈牙利命名法给变量名前加上类型,否则到后面你可能自己都忘了这个变量是什么类型。decltype的使用在上面也演示了,关于type traits的内容将在后文解释。

基于范围的for循环

容器手工for循环一直是C++常见的代码,而初始迭代器的定义、跳出循环条件、步长一直像是定式一般的存在,C++标准委员会为了简化这种常见循环,在C++11中简化了这种基于范围的for循环。在新标准中对于数组或者拥有begin及end方法的容器,可以这样写for循环:

1
2
3
4
5
vector<int> vecTest = { 1,2,3 };
for (auto &i : vecTest)
{
i = i * 2;
}

这里使用的是传递引用的方式来改变vector里的元素值,也可以使用按值传递的方式(即去掉&),当然前提是不需要修改原容器内容。

Lambda表达式

现在大多数高级语言和脚本语言都早早支持了lambda表达式,而lambda表达式对于C++和STL来说可以说有着十分重要的意义。STL中的算法通常都提供自定义判断函数参数,比如std::find_if,而在C++11前的版本里如果你想使用find_if并且可能仅仅是一个简单比较,你都必须再定义一个函数甚至函数对象(当然你也可以用std::greater),这通常是十分麻烦并且冗赘的,这也是我认为在C++11版本前algorithm中的方法使用率低的一个原因。C++11为了解决这个问题,引入lambda表达式,有了lambda表达式一个find_if的查找看上去就像是:

1
2
3
list<int> vecTest = { 1,2,3,4,5 };
int iTarget = 3;
auto itTest = find_if(vecTest.begin(), vecTest.end(), [iTarget](int i) { return i > iTarget; });

这里简单的介绍下C++中的lambda表达式,如果接触过其他语言的lambda表达式,你应该对这种匿名函数式不会陌生,C++11中的lambda表达式可分为下面几个部分:

1
2
3
4
[/*捕获子句*/] (/*参数列表*/) /*可变声明mutable*/ /*异常规范*/ -> /*返回值声明*/
{
//函数体
}

其中捕获子句一般分为以下几种格式:

1
2
3
4
5
6
[&a, b]
[a, &b]
[&, b]
[a, &]
[=, &b]
[&a, =]

所谓捕获子句是由于该匿名函数内可能需要用到外部的变量所需要做的一个声明,声明需要用到外部的何种变量,以何种方式。以上几个格式一一说明,[&a, b]代表外部变量a以引用传递到lambda表达式内部,b以按值传递方式传递,[a,&b]则反过来;[&,b]表示变量b以按值传递方式,其余变量按引用传递,[a,&]类似;[=,&b]表示变量b以按引用传递,其余变量按值传递,[&a,=]类似。
参数列表好理解,这个lambda匿名函数的入参列表。mutable可变声明,如果加上mutable表示之前按值捕获的变量其值可以在lambda函数体内修改。异常规范声明,标识该lambda表达式是否抛异常,不抛异常可以写throw()。返回值声明即lambda匿名函数的返回值类型,这里就不要写auto了,因为本身这个声明就是可以省略的。注意,以上可变声明、异常规范、返回值声明甚至参数列表都是可省略的,参数列表省略前提是这个函数并没有入参。所以上面的find_if表达式完整版本是:

1
2
auto itTest = find_if(vecTest.begin(), vecTest.end(),
[iTarget](int i) /*mutable*/ throw() -> bool{ return i > iTarget; });

如果希望将lambda表达式作为变量参数传递可以利用同是C++11引进的新模板std::function

1
2
function<bool(int)> funCompare = [iTarget](int i) /*mutable*/ throw() -> bool { return i > iTarget; };
auto it = find_if(vecTest.begin(), vecTest.end(), funCompare);

回返类型后置的函数声明

在C++11版本中添加了decltype和auto关键字后,模板编程有了更多的可能,但是对于返回值类型来说存在这样的问题:

1
2
3
4
5
template<typename LHS, typename RHS>
auto Add(const LHS &lhs, const RHS &rhs)
{
return lhs + rhs;
}

上述代码将编译出错,auto并不能直接作为函数或模板返回值声明使用,那么decltype呢

1
2
3
4
5
template<typename LHS, typename RHS>
decltype(lhs + rhs) Add(const LHS &lhs, const RHS &rhs)
{
return lhs + rhs;
}

仍然有问题,lhs和rhs只在函数体内有效。所以在C++11中为了解决模板编程这一问题规定了新的后置式返回值声明形式:

1
2
3
4
5
template<typename LHS, typename RHS>
auto Add(const LHS &lhs, const RHS &rhs) -> decltype(lhs + rhs)
{
return lhs + rhs;
}

注意这里的auto仅仅是后置返回值声明的形式,也就是如果使用后置式返回值声明该函数的前置的声明必须是auto。当然除了模板,普通函数也可以使用后置声明:

1
auto GetFunc() ->int(*)(int);

构造函数改良

在C++03版本中不允许委托构造(delegating constructor),同时不允许子类使用基类的构造函数作为构造,而这两个较为方便的特性在Java和C#中都已支持,所以在C++11把这两个特性加入标准中。现在你可以这样书写你的类型的构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CBase {
public:
CBase() : CBase(0) {}
CBase(int i) : m_int(i) {}
CBase(int i, int j) { CBase(i + j); }
protected:
int m_int = 1;
};
class CDerived : public CBase {
public:
using CBase::CBase;
int GetInt() { return m_int; }
};
int main()
{
CDerived d(1,2);
return 0;
}

CBase中空构CBase()使用CBase(int)作为委托构造函数,CBase(int,int)同理使用CBase(int)作为委托构造函数;子类CDerived公共继承CBase类,"using CBase::CBase"的声明告诉编译器CDerived使用CBase的构造函数来作为CDerived的构造函数。需要注意的是这种委托构造写法必须在初始化列表里,也就是冒号后,而不能出现{ }函数体里。这里上面还用到另一个新特性,在C++03版本中声明变量不能对其进行初始化声明,而C++11中将允许这种声明方式(上面代码中的int m_int = 1),如果构造函数中没有指明成员变量的初始化值将使用这个变量的初始化声明值来赋值。

override和final显式虚函数重载

override表示重载基类虚函数,final表示该虚函数不能再被重载,如:

1
2
3
4
5
6
struct Base {
virtual void Func();
};
struct Derived : public Base {
void Func() override;
};

override的主要用意在于显示表达虚函数重载,当然上例中override不写也是可以的,如果使用了override但是本身不是虚函数重载,编译器将报错。final的使用也是类似:

1
2
3
4
5
6
struct Base {
virtual void Func() final;
};
struct Derived : public Base {
void Func();
};

该代码编译器将报错,因为Func在基类中已经声称是final,不能再被重载。
需要注意的是override和final并不是语言关键字,所以在代码中命名override和final名称的变量并不会出现问题,final和override只在函数声明时生效。

空指针关键字nullptr

C中习惯使用NULL这个预编译宏来表示空指针,但是NULL本身的定义就是整数0,这在函数调用时可能会造成歧义,如一个同名函数如果提供(int)和(int(*)(void))两种函数签名的函数时,对于(NULL)的函数调用将直接导致编译器调用(int)而非是(int(*)(void))。所以在C++11版本中加入了新语言关键字nullptr来表示空指针这一含义,nullptr可以隐式转换成任意指针,方便在引入了std::function的c++11中使用,现在(nullptr)的调用将调用(int(*)(void))而非(int)。

强类型枚举

C++03版本中枚举的使用存在比较难用的地方,比如在同一命名空间中不同的枚举内名称不能相同,所以通常的做法是去给枚举内部值命名个很长的名字(如:ENUM_ABM_DEDUCT_RATING_DEDUCT_SUCCESS)来加以区分,虽然这看上去真的很难看。在使用上老版本的枚举类型大部分人的使用习惯还是以C风格的整形习惯来操作,而不是单独作为一种类型。在C++11中增加了一种强类型枚举,为了和向下兼容和与老的枚举类型进行区分,新枚举使用enum class形式进行定义:

1
2
3
4
5
6
7
8
9
enum TEnum
{
Right
};
enum class TEnumClass : unsigned int
{
Right

};

新枚举使用 : unsigned int 类似的形式来显示说明枚举值的类型,如果不写默认类型为int整形。新枚举使用新的强类型枚举没有命名上的尴尬,不需要在同一个命名空间里枚举值名称不同,所以上面Right可以重名。也因此新枚举使用上不能抛开枚举名单独使用,也就是TEnumClass::Right才是一个有效的枚举值,而不能是单独的Right(由于新老枚举共存,在上面的场景下单独使用Right将意味着TEnum::Right);新枚举同时是类型安全的,类型安全意味着他不能直接与整数进行比较,不能隐式转换成整数。此外新枚举可以进行forward declaration。

角括号

老版本C++中声明类似vector<vector<int>>的类型会报错,这是由于老版本编译器将>>符号一律视为右移运算符,所以老版本中只能vector<vector<int> >来书写。在C++11版本中修改了语法剖析器规则,>>符号会在合适的时机被正确解读,例如刚才说的vector<vector<int>>现在能正确被编译。

explicit关键字增强

C++11版本中explicit关键字可以修饰operator()函数,为了避免对象不必要的隐式转换。

模板别名与using新语义

在老版本C++标准中以下这种模板别名无法通过编译:

1
2
3
4
5
6
7
8
9
template<typename T1, typename T2>
class Widget;


template<typename T>
typedef Widget<int, T> SpecialWidget;

template<typename T>
typedef map<int, T> SpecialMap;

在C++11对using语法增加了新的用法,现在可以使用using来做类似typedef的别名声明:

1
2
3
4
5
6
7
8
template<typename T>
using SpecialWidget = Widget<int, T>;


template<typename T>
using SpecialMap = map<int, T>;

using MyMap = map<int, int>;

对于一般类型的别名如上面MyMap的声明和typedef关键字等效。

union限制放宽

在老版本c++中,union不能拥有非平凡(non-trivial)成员,否则将不能通过编译。C++11中放宽该限制,union可以拥有non-trivial成员,但是如果拥有了则必须手工定义构造函数。

可变参数模板

C++11中允许声明任意个数、任意类别的模板参数,其声明方式类似于c++11新增的std::tuple:

1
template <class... Types> class tuple;

与可变参数函数类似,可变参数模板以<class…>或<typename…>来表示可变参数,代表该模板类型参数可以有0到任意个。虽然C++11标准里提供了可变参数模板的书写标准,但是并没有提供解开这个不定长类型参数的方法或机制,这里要想解开这个不定长类型参数需要一定的技巧,通常是通过递归来实现可变参数模板的书写,比如以下这个C++11中的printf实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void printf(const char* s)
{
while (*s)
{
if (*s == '%' && *(++s) != '%')
throw std::runtime_error("invalid format string: missing arguments");
std::cout << *s++;
}
}
template<typename T, typename... Args>
void printf(const char* s, T value, Args... args)
{
while (*s)
{
if (*s == '%' && *(++s) != '%')
{
std::cout << value;
printf(*s ? ++s : s, args...);
return;
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}

printf在确定调用形式后,进行实例化函数模板,反复递归调用printf(const char*, T, Args…)直到调用printf(const char*)完成实例化。C++11还新增了对这个类型参数数量的获取方式:

1
2
3
4
5
template<typename... T>
void Func(T... t)
{
int size = sizeof...(T);
}

新的字符串字面值

C++11标准中添加了对UTF-8、UTF-16和UTF-32的支持,在新标准中你可以使用u8、u、U三种前缀来分表表示UTF-8、UTF-16、UTF-32:

1
2
3
u8"UTF-8"
u"UTF-16"
U"UTF-32"

以上三个字符串类型分别为const char[]、const char16_t[]、const char32_t[],在C++11标准中char已经被修改为可以容下UTF-8的8位编码,另外char16_t和char32_t类型被新增用来支持UTF-16和UTF-32,在<string>中也新增了std::u16string和std::u32string对应UTF-16和UTF-32字符串。
另外C++11标准也提供了避免字符串内容被转义的方法:

1
u8R"delimiter(windows的换行符是"\r\n")delimiter"

上面这个字符串使用了u8这个UTF-8前缀,同时使用了R这个前缀,R代表字符串内容按字面义获取(RAW),字符串中圆括号内的字符不会被转义,delimiter为界定符标识表示在"delimiter(“和”)delimiter"之间的字符串,这个界定符可以是人为约定的任意最大16个字符的字符串,当然也可以省略,以下两个字符串和上面的等义:

1
2
u8R"aaa(windows的换行符是"\r\n")aaa"
u8R"(windows的换行符是"\r\n")"

用户自定义字面量

当我们定义一个float时一般书写1.23f,这个f后置表明该1.23字面量值为一个float而非一个double,这个f称为字面值的修饰符(literal modifier)。在老版本c++中用户并不能自定义这种修饰符,而在C++11中开放该修饰符的定义,增加了字面量操作符函数(literal operator),字面量操作符函数使用户自定义字面量成为可能:

1
long double operator"" _mm(long double x) { return x / 1000; }

上面这个字面量操作符函数表示后续如果使用如 123.4_mm 这样的变量,这个变量的值将等于0.1234这个long double类型值。字面量操作符函数以operator""标识,_mm为后缀名,注意这里用户自定义字面量操作符函数名必须以下划线“_”开头,非下划线开头的函数预留给C++标准使用。同时要注意字面量操作符函数仅允许以下这11个入参函数签名,其余入参签名函数无法通过编译

1
2
3
4
5
6
7
8
9
10
11
(const char *)
(unsigned long long int)
(long double)
(char)
(wchar_t)
(char16_t)
(char32_t)
(const char *, std::size_t)
(const wchar_t *, std::size_t)
(const char16_t *, std::size_t)
(const char32_t *, std::size_t)

使用或禁用对象的默认函数

一个自定义类型如果用户未定义,一般编译器会自动生成默认空构(default constructor)、复制构造函数(copy constructor)、赋值运算符(copy assignment)和析构(destructor),以及operator new和opeartor delete。老版本c++中通常很难精确控制这些默认函数的是否自动生成,比如singleton如果要控制类型不可外部构造,需要将构造、复制构造、赋值运算符都声明为private,虽然本身其实不需要复制构造和赋值运算符;再比如一个类型如果声明了不是空构的构造函数,那他的空构函数编译器将不会再自动生成,如果空构仍需要那么只能再定义一遍,即使你可能需要的空构就是编译器自动生成的。所以在C++11中添加了以下语法来解决这些问题:

1
2
3
4
5
6
7
8
9
class Widget
{
public:
Widget() = default;
Widget(int i) {}
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;
void* operator new(std::size_t) = delete;
};

= default表示使用编译器默认生成的版本,= delete表示函数被禁用,编译不再自动生成对应函数。
这里=delete还有另外一个用法,用于禁止隐式转换造成的函数调用:

1
2
3
4
struct SType
{
void func(int i) {}
};

SType的func接受一个int类型的入参,所以所有可以一次隐式转换成int的类型都能在func时调用成功,如果想禁止这种隐式转换造成的函数调用可以禁用掉:

1
2
3
4
5
6
struct SType
{
void func(int i) {}
template<typename T>
void func(T) = delete;
};

静态断言static_assert

老版本的c++只有运行期断言assert和预处理期测试指令#error,但是没有编译期的测试断言方法,而模板的实例化发生在编译期,如果想在这时做断言老版本C++并没有这个能力。
在C++11中加入新关键字static_assert,用于在编译器做断言测试,语法为:

1
static_assert (bool_constexpr, message)

使用方法如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <type_traits>
template <class T>
void swap(T& a, T& b)
{
static_assert(std::is_copy_constructible<T>::value,
"Swap requires copying");
static_assert(std::is_nothrow_copy_constructible<T>::value
&& std::is_nothrow_copy_assignable<T>::value,
"Swap requires nothrow copy/assign");
auto c = b;
b = a;
a = c;
}

<type_traits>同样是C++11版本加入stl的成员,主要用于编译期的一些类型判断,std::is_copy_constructible用于判断类型是否可拷贝构造,std::is_nothrow_copy_construcible用于判断类型的拷贝构造是否不抛异常,std::is_nothrow_copy_assignable用于判断类型的赋值操作符函数是否不抛异常。上述模板定义在编译器发生断言判断,如果类型不可复制,复制构造和赋值操作符抛异常将在编译期出错。

sizeof操作符可作用于成员类型上

c++11标准中sizeof操作符可以作用于类型的成员类型上,例如:

1
2
struct SomeType { OtherType member; }
sizeof(SomeType::member);