c++11常用新特性:语言层面

强类型枚举

在c语言中,枚举类型是整型,但长度由编译器决定,枚举类型的成员的可见范围被提升至该枚举类型所在的作用域内。

在c++11中引入了称为强类型枚举的新类型,由关键字enum class或enum struct标识。
每一个enum class都是一个新的类型,不能与整型自动转换。
enum class成员的作用域起自其声明之处,终止于enum class定义结束之处。
可显式声明枚举类型(包括不限定作用域的enum)的长度。

enum struct Level : char
{
	first, second,
};

Level l = Level::second;
//l = 2;//error
cout << sizeof(l) << endl;//1

auto和decltype

定义迭代器类型的时候不用写很长的一串了,比如:

std::map<std::string, std::vector<int>> m;
for(auto it = std::begin(m); it != std::end(m); ++it) 
{
}

这段代码还展示了两个地方,一是>>中间不用加空格了,二是全局的begin和end函数(函数模板)。

auto先计算出表达式,再根据表达式的结果来推断类型。
auto会忽略掉直接修饰变量的const,但会保留指针指向变量的const。

int i=0;
const int ci=i, &cr = ci;
auto b=ci;//int
auto c=cr;//int
auto d=&i;//int*
auto e=&ci;//const int*
const auto f=ci;//const int
const auto g=&ci;//const int * const
auto &h=ci;//const int&

同一个赋值语句中,auto可用来声明多个变量的类型,编译器会用第一个变量去推导auto的类型,推导出来的数据类型被作用于其他变量(来自《深入理解c++11》4.2节):

auto x = 1, y = 2;
const int *g = &x, h = y; //h: const int
const auto *m = &x, n = y; //auto: int, n: int
auto o = 3, &p = o, *q = &p;

这基本符合我们的想象。

decltype

当用decltype(e)来获取类型时,编译器将依序判断一下四规则:
如果e是一个变量或类成员,那么decltype(e)就是e的类型
否则,假设e的类型是T,如果e是一个将亡值,那么decltype(e)为T&&
否则,假设e的类型是T,如果e是一个左值,那么decltype(e)为T&
否则,假设e的类型是T,则decltype(e)为T

以上内容和下面的例子摘自《深入了解c++11》4.3.3节:

int i = 4;
int arr[5] = {0};
int *ptr = arr;
struct S { double d; } s;
void Overloaded(int);
void Overloaded(char);
int && RvalRef();
const bool Func(int);

// 规则1: 单个标记符表达式以及访问类成员,推导为本类型
decltype(arr) var1;             // int[5], 标记符表达式
decltype(ptr) var2;             // int*, 标记符表达式
decltype(s.d) var4;             // double, 成员访问表达式
decltype(Overloaded) var5;      // 无法通过编译,是个重载的函数

// 规则2: 将亡值,推导为类型的右值引用
decltype(RvalRef()) var6 = 1;   // int&&

// 规则3: 左值,推导为类型的引用
decltype(true ? i : i) var7 = i;     // int&, 三元运算符,这里返回一个i的左值
decltype((i)) var8 = i;              // int&, 带圆括号的左值
decltype(++i) var9 = i;              // int&, ++i返回i的左值
decltype(arr[3]) var10 = i;          // int&, []操作返回左值
decltype(*ptr)  var11 = i;           // int&, *操作返回左值
decltype("lval") var12 = "lval";     // const char(&)[9], 字符串字面常量为左值

// 规则4:以上都不是,推导为本类型
decltype(1) var13;               // int, 除字符串外字面常量为右值
decltype(i++) var14;             // int, i++返回右值
decltype((Func(1))) var15;       // const bool, 圆括号可以忽略

以下是来自其他地方的一些类似描述:
如果decltype使用的表达式是一个变量,则decltype返回该变量的类型,包括直接修饰变量的const和引用。引用从来都是作为其所指对象的同义词出现,只有用在decltype处是一个例外。

const int ci=0, &cj=ci;
decltype(ci) x=0;//x与ci的类型完全相同,const int
decltype(cj) y=x;//y和cj的类型完全相同,const int &

如果decltype使用的表达式不是一个变量,则返回表达式结果对应的类型。
但如果表达式可以作为左值,产生的就是引用。

auto和decltype组合起来用能实现函数返回类型后置:

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

新的区间迭代

由于auto的出现,上节例子中的区间迭代简化了一些,然而c++11提供了更简洁的区间迭代:

std::map<std::string, std::vector<int>> m;
for(auto e:m) 
{
}
为了使数据结构可迭代:
这个数据结构必须要有begin和end方法,成员方法和独立函数都行,这两个方法分别返回开始和结束的迭代器
迭代器支持*操作符、!=操作符、++方法(前缀形式,成员函数和独立函数都行)

新的区间迭代其实是调用了std::begin和std::end,所以自定义的数据结构实现begin和end成员方法就可以了,当然也可以对std::begin和std::end进行特化。

nullptr

增强的安全的空指针类型,是nullptr_t类型的常量,nullptr_t是由nullptr定义的:

typedef decltype(nullptr) nullptr_t;

另外,c语言标准中的void *指针是可以隐式转换为任意指针的,c++不行。所以一般NULL在c里被定义为(void *)0,在c++里被定义为0。

统一的初始化列表

在引入c++11之前,只有数组和结构体能使用初始化列表,而且初始化列表还得放在等号后边,其他容器想要使用初始化列表,只能用以下方法:

int arr[3] = {1, 2, 3}  
vector<int> v(arr, arr + 3); 

在c++11中,可以使用以下语法来进行初始化:

int arr[3]{1, 2, 3};  
vector<int> iv{1, 2, 3};  
map<int, string>{{1, "a"}, {2, "b"}};  
string str{"Hello World"}; 

lambda

[]内放的是变量捕获。变量捕获的魔法是如何运作的?其实lambda实现的方法是创建一个简略的类。这个类重载了operator(),所以表现的像个普通函数。一个lambda函数是这个类的实例。当这个类构造的时候,所有捕获的变量被传送到类中并保存为成员变量。c++11的优势是这一切都变得非常简单。你可以在任意时候使用它,而不仅仅是极少的特殊场合去写一个类。
c++为性能计,实际上提供了好几种灵活的捕捉变量的方式。[]中什么也没有则不捕获变量,如果你创建了一个空[]的lambda函数,c++将创建一个普通的函数而不是类。这里有完整的捕获选项:

[]  不捕获任何变量
[&] 以引用方式捕获所有变量(包括this)
[=] 用值的方式捕获所有变量(包括this)
[=, &foo] 以引用捕获foo, 但其余变量都靠值捕获
[bar] 以值方式捕获bar, 不捕获其它变量
[this] 捕获所在类的this指针

捕捉列表仅能捕捉父作用域的自动变量,而对超出这个范围的变量,是不能被捕捉的。全局变量不用捕获便能访问。
忽略返回类型的话,编译器会自动推断出返回类型,如果需要指定,参考:

auto add = [](int a, int b) -> int { return a + b; };

如果参数为空()可以省略,所以最简单的lambda函数是[]{}

默认情况下lambda函数是一个const函数,可以在参数列表后面添加mutable修饰符来取消其常量性,在使用mutable时,参数列表不可省略,即使参数为空。这里的const指的是捕获的变量是const的,和参数没关系。相当于对应的函数对象的operator()是const的,即函数对象本身是const的,所以它的成员变量都不可变。

新的std::function可用来传递lambda函数。
维基百科说,闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以我认为c++11的lambda函数并不是真正的闭包。

显式缺省函数/显式删除函数

c++11标准称"=default"修饰的函数为显式缺省函数,而称"=delete"修饰的函数为显式删除函数。
参考《C++ default 和delete的新用法》

override和final

override确保在派生类中声明的重载函数跟基类的虚函数有相同的签名。
final有两个用途。第一,它可以阻止从类派生;第二,它可以阻止一个虚函数的重载。

class TaskManager {} final; 
class PrioritizedTaskManager: public TaskManager {
}; //compilation error: base class TaskManager is final

class A
{
pulic:
  virtual void func() const;
};
class  B: A
{
pulic:
  void func() const override final; //OK
};
class C: B
{
pulic:
 void func() const; //error, B::func is final
};

例子来自:https://www.kancloud.cn/wangshubo1989/new-characteristics/99708

委托构造

一个类的构造函数可以调用这个类的其他构造函数了。
还可以复用基类的构造函数,通过using Base::Base;来声明使用基类的构造函数,这样就避免了定义相同的构造函数来保持和基类一样的初始化行为。

noexcept

noexcept 运算符
noexcept( expression )
noexcept 运算符进行编译时检查,若表达式声明为不抛出任何异常则返回 true。

noexcept 限定符
noexcept( constant expression )
在函数的后面加上 noexcept,指定函数是否抛出异常。noexcept 等价于 noexcept(true),代表这个函数不会抛出异常,如果抛出异常程序就会终止。

alignof和alignas

alignof 运算符
alignof( 类型标识 )
查询类型的对齐要求。返回 std::size_t 类型值。
返回由类型标识所指示的类型的任何实例所要求的对齐字节数,该类型可以为完整类型、数组类型或者引用类型。
若类型为引用类型,则运算符返回被引用类型的对齐;若类型为数组类型,则返回元素类型的对齐要求。

alignas 指定符
指定类型或对象的对齐要求。
alignas(int) 等价于 alignas(alignof(int))

// 每个 sse_t 类型对象将对齐到 16 字节边界
struct alignas(16) sse_t
{
	float sse_data[4];
};
 
// 数组 "cacheline" 将对齐到 128字节边界
alignas(128) char cacheline[128];

简而言之,alignof和alignas分别用来获取和设置《结构体和类的大小问题》中提到的对齐参数。

另外标准库还有与此相关的概念:
max_align_t,定义于<cstddef>,是一个平凡的标准内存布局类型,它的对齐参数至少和所有标量类型一样严格,也就是说它的对齐参数大于等于所有标量类型的对齐参数。(标量类型:非数组、非class类型)
内存分配函数返回的指针对齐至少和max_align_t一样严格,也就是说其对齐参数大于等于alignof(std::max_align_t)。
max_align_t通常被实现为最大标量类型的同义词,一般是long double类型。所以一般malloc和new返回的指针至少是sizeof(long double)对齐的。
align、aligned_storage、aligned_union,多数编译器并未实现align函数,后两者是类模板,不多介绍了。

constexpr

constexpr 限定符声明可以在编译时求得函数或变量的值,前提是它所依赖的东西也是可以在编译期求得,但实际上它可能并不能在编译期算出来。
若函数或函数模板的任何声明拥有 constexpr 限定符,则每个声明必须都含有该限定符。
constexpr 修饰的函数,如果其返回值可以在编译时期计算出来,那么这个函数就会产生编译时期的值,否则就和普通函数一样。
常量表达式函数的要求有:
函数体只有单一的return返回语句
函数必须有返回值
在使用前必须已有定义
return返回语句表达式中不能使用非常量表达式的函数、全局数据

用户自定义字面量

可以通过指定一个后缀标识的操作符,将声明了该后缀标识的字面量转化为需要的类型,这里是一个简单的例子:

struct Watt{ unsigned int v; };
Watt operator "" _W(unsigned long long v) {
	return {(unsigned int)v};
}
void operator "" _W(const char *, size_t) { }

int main() {
	Watt capacity = 1024_W;
}

C++11中具体规则如下:

  • 字面量操作符函数的参数只能接受4种:“unsigned long long”,“long double”,“const char *, size_t”,“char”。
  • 如果字面量为整型数,那么字面量操作符函数只可接受unsigned long long或者const char*为参数。当unsigned long long无法容纳该字面量的时候,编译器会自动将该字面量转化为以\0为结束符的字符串,并调用以const char*为参数的版本进行处理。
  • 如果字面量为浮点型数,则字面量操作符函数只可接受long double或者const char*为参数。const char*版本的规则同整型的一样(过长则使用const char*版本)。
  • 如果字面量为字符串,则字面量操作符函数函数只可接受const char*,size_t为参数。
  • 如果字面量为字符,则字面量操作符函数只可接受一个char为参数。

另外还应注意以下几点:

  • 在字面量操作符函数的声明中,operator""与用户自定义后缀之间必须有空白。
  • 后缀建议以下划线开始,但其实只要不以数字开头就是合法的,如果不以下划线开始,编译器会提醒“不以下划线开始的字面量操作符后缀保留给将来用”。

以上主要参考自《深入了解c++11》3.8节。

右值引用和move语义

如果一个变量有名字,它就是左值,否则,它就是右值。右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
右值引用就是必须绑定到右值的引用。右值引用只能绑定到一个将要销毁的对象,那么使用右值引用的代码可以自由地接管所引用的对象的资源。
考虑一个右值作为拷贝构造函数或赋值操作符函数的参数时,没有必要复制一份资源,直接将右值的资源转移给新对象多好,反正右值也将要销毁掉。右值引用使得拷贝构造函数和赋值操作符函数能识别出右值来,进行更高效的复制和赋值操作。
参数为右值引用的拷贝构造函数和赋值操作符函数称为move构造函数和move赋值函数,所有的标准库容器和自定义容器都可以通过添加move赋值函数和move构造函数来避免使用临时对象所带来的性能损失。

有时候将一个左值在做为拷贝构造函数的参数被拷贝之后便不再使用,此时是可以直接转移资源所有权的,这里需要把这个左值转换为右值,std::move就用来将一个左值引用或右值引用转换为右值引用。

// g++: bits/move.h
  /// remove_reference
  template<typename _Tp>
    struct remove_reference
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&>
    { typedef _Tp   type; };

  template<typename _Tp>
    struct remove_reference<_Tp&&>
    { typedef _Tp   type; };

  /**
   *  @brief  Convert a value to an rvalue.
   *  @param  __t  A thing of arbitrary type.
   *  @return The parameter cast to an rvalue-reference to allow moving it.
  */
  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

在参数类型推导上,c++11加入了两个原则,其一是:
引用折叠原则 (reference collapsing rule),注意,以下条目中的 T 为具体类型,不是推导类型。

  1. T& & (引用的引用) 被转化成 T&
  2. T&& & (右值引用的引用)被传化成 T&
  3. T& && (引用的右值引用) 被转化成 T&
  4. T&& && 被转化成 T&&

另一个特殊的参数类型推导原则是,如果左值引用T &arg做为move(这里以move模板函数为例)的实参,_Tp被推导为T &。根据引用折叠原则,T& &&被转化为T&,所以move被推导为move(T &arg)。返回值typename std::remove_reference<T &>::type&&的类型是T&&,所以就实现了左值引用到右值引用的转换。

std::move在提高swap函数的性能上非常有帮助,有了std::move,swap函数的定义变为:

// g++: bits/move.h
template <class T> swap(T& a, T& b)
{
	T tmp(std::move(a)); // move a to tmp
	a = std::move(b);    // move b to a
	b = std::move(tmp);  // move tmp to b
}

当然,这得需要class T定义有move构造函数和move赋值函数才会有性能提升。

perfect forwarding

我们来考虑下面的情景:

void doWork(TYPE&& param) {
	// ops and expressions using std::move(param)
}

这个代码是从Scott Meyers的演讲当中摘取的。现在的问题是:param是右值吗?不,因为param有名字,所以它是左值。
我们希望在

template <typename T>
void relay(T&& t) {
	foo(t);
}

中,当实参是左值时,传给foo的是左值,当实参是右值时,传给foo的是右值,perfect forwarding。

// g++: bits/move.h
  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
            " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

不能根据forward的实参推导出_Tp来,所以使用forward是要显式实例化,刚才的那段代码变为:

template <typename _T>
void relay(_T&& t) {
	foo(std::forward<_T>(t));
}

就能实现perfect forwarding了:

void foo(int & i) { cout << "int & " << i << endl; }
void foo(int && i) { cout << "int && " << i << endl; }
int main() {
    int m = 66;
    relay(m);
    relay(88);
    return 0;
}

int & 66
int && 88

relay的实参无论是左值引用还是右值引用,t都是左值引用,都会命中forward的第一个版本。(可以自己写一个forward,只保留第一个版本试试)
当relay的实参为左值引用时,同上一节所说,_T被推导为T&,std::forward<T&>(t),在forward内部T & &&被折叠为T&,t被转换为左值引用。
当relay的实参为右值引用时,_T被推导为T,t被转换为右值引用。

如果将foo(std::forward<_T>(t));改为foo(std::forward<_T&>(t));,当relay的实参为左值引用时,_T被推导为T&,T& &被折叠为T&,同上;当relay的实参为右值引用时,_T被推导为T,仍然是std::forward<T&>(t),t被转换为左值引用。

如果改为foo(std::forward<_T&&>(t));,当relay的实参为左值引用时,_T被推导为T&,T& &&被折叠为T&,同上;当relay的实参为右值引用时,_T被推导为T,std::forward<T&&>(t),在forward内部,T&& &&被折叠为T&&,t被转换为右值引用。

那forward的第二个版本干啥的,可以这样forward:relay(std::forward<int>(88));。再说只forward左值,不forward右值,也不perfect。


更多详细内容可参考《Effective Modern C++》Item28:理解引用折叠


变长参数模板

看一个典型的变长模版参数的定义:

template <typename ... Args>
void f(Args ... args)
{
cout << sizeof...(Args);
cout << sizeof...(args);
}

这里涉及两个概念:
1, 模板参数包(template parameter pack):
它指模板参数位置上的变长参数(可以是类型参数,也可以是非类型参数),例如上面的Args。
2, 函数参数包(function parameter pack):
它指函数参数位置上的变长参数,例如上面的args。
sizeof…(Args)可以用来获知参数包中打包了几个参数。

还有一个解包规则,解包是把参数包展开为它所表示的具体内容的动作,语法是包名加上…,如"Args…, args…"。
参数包的展开不能无条件地在任何地方使用,标准规定可以进行参数包展开的有7种情况:1,表达式;2,初始化列表;3,基类描述列表;4,类成员初始化;5,模板参数列表;6,通用属性列表;7,lambda函数的捕获列表。

由以上概念和规则,与c++已有规则组合,就能演化出极其复杂多样的用法,下面举两个例子。

递归方式展开参数包

void print() { }
template <class T, class ... Args>
void print(T head, Args ... rest)
{
	cout << head << endl;
	print(rest...);
}

print(1,2,3,4);

表达式方式展开参数包

template <class T>
void print(T t)
{
	cout << t << endl;
}
template <class ... Args>
void print(Args ... args)
{
	int dummy[] = {(print(args), 0)...};
}

print(1,2,3,4);

参考
https://blog.csdn.net/yanxiangtianji/article/details/21045525
https://www.cnblogs.com/qicosmos/p/4325949.html

其他

类的非static成员变量可以在类定义的时候直接初始化。

sizeof运算符可以在类的数据成员上使用,无需明确对象,但该数据成员应是public的。

class A {
public:
	int m = 5;
};
int main() {
	A a;
	cout << sizeof(A::m) << sizeof(a.m);
	return 0;
}

增强的定义别名的能力:

using pf = void(*)();
template <typename First, typename Seconde, typename Third>
class Some
{
	//...
};
template <typename Second>
using Spec = Some<int, Second, int>;

通过thread_local修饰符声明的变量是线程局部变量。

quick_exit函数功能类似exit函数,它们都会正常结束程序,但quick_exit不会清理资源。类似at_exit函数,同样可以通过at_quick_exit函数注册quick_exit时需要调用的函数。

static_assert
编译时期的断言。

可以try整个函数体,甚至可以包含构造函数初始化列表,这不是c++11的新内容,但我是现在才知道:

class entry
{
	int num;
public:
	entry()try:num(36){
		printf("%s\n", __func__);
	}catch(...){}
	void fun(int n)try{
		printf("%s\n", __func__);
	}catch(...){}
};

参考

行者
C++11系列
cppreference
从4行代码看右值引用
c++11 中的 move 与 forward
C++开发者都应该使用的10个C++11特性
深入理解c++11

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值