1.auto
auto是旧关键字,在C++11之前,auto用来声明自动变量,表明变量存储在栈,很少使用。在C++11中被赋予了新的含义和作用,用于类型推断。auto关键字主要有两种用途:一是在变量定义时根据初始化表达式自动推断该变量的类型,二是在声明或定义函数时作为函数返回值的占位符,此时需要与关键字decltype连用。
1.1用法示例
(1)auto用于推断变量类型示例。auto i = 42; //i is an int
auto l = 42LL; //l is an long long
auto p = new foo(); //p is a foo*
1 2 3 4 5 (2)声明或定义函数时作为函数返回值的占位符。 auto不能用来声明函数的返回值。但如果函数有一个尾随的返回类型时,auto是可以出现在函数声明中返回值位置。这种情况下,auto并不是告诉编译器去推断返回类型,而是指引编译器去函数的末端寻找返回值类型。在下面这个例子中,函数返回值类型是operator+操作符作用在T、U类型变量上的返回值类型。template<class T, class U> auto add(T t, U u) -> decltype(t + u){
return t + u; } 1 2 3 2.decltype decltype与auto关键字一样,用于进行编译时类型推导,不过它与auto还是有一些区别的。decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,而是总是以一个普通表达式作为参数,返回该表达式的类型,而且decltype并不会对表达式进行求值[2]。2.1decltype推导规则
(1)如果e是一个变量或者类成员访问表达式,假设e的类型是T,那么的decltype(e)为T,decltype((e))为T&。 (2)如果e是一个解引用操作,那么decltype(e)和decltype((e))均为T&。 (3)否则decltype(e)与decltype((e))均为T。2.2用法示例
(1)推导出表达式类型。struct A { double x; };
const A* a = new A{0};//第一种情况
decltype(a->x) y; // type of y is double decltype((a->x)) z = y; // type of z is const double&,因为a一个常量对象指针//第二种情况
int* aa=new int; decltype(aa) y=aa; //type of y is int&,解引用操作//第三种情况
decltype(5) y; //type of y is int decltype((5)) y; //type of y is int const int&& RvalRef() { return 1; } decltype ((RvalRef())) var = 1; //type of var is const int&& 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 (2)与using/typedef合用,用于定义类型。using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型
using ptrdiff_t = decltype((int)0 - (int)0); using nullptr_t = decltype(nullptr);vectorvec;
typedef decltype(vec.begin()) vectype; for (vectype i = vec.begin; i != vec.end(); i++){ //... } 1 2 3 4 5 6 7 8 9 显而易见,与auto一样,也提高了代码的可读性。(3)泛型编程中结合auto,用于追踪函数的返回值类型,这也是decltype的最大用途。
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(xy) { return xy; } 1 2 3 4 5 3.nullptr 以前都是用0来表示空指针的,但由于0可以被隐式类型转换为整形,这就会存在一些问题。关键字nullptr是std::nullptr_t类型的值,用来指代空指针。nullptr和任何指针类型以及类成员指针类型的空值之间可以发生隐式类型转换,同样也可以隐式转换为bool型(取值为false)。但是不存在到整形的隐式类型转换[3]。int* p1 = NULL;
//或 int* p2 = nullptr; 1 2 3 4.constexpr constexpr再C++11中用于申明常量表达式(const expression)。常量表达式是指值不会改变并且在编译过程中就得到计算结果的表达式[4]。const int i=3; //i是一个常量变量
const int j=i+1; //j是一个常变量,i+1是一个常量表达式
int k=23; //k的值可以改变,从而不是一个常变量
const int m=f(); //m不是常变量,m的值只有在运行时才会获取。
1 2 3 4 5 6 7 一般来说,若果一旦认定变量是一个常量表达式,那就把它声明为constexpr类型。必须明确一点,在constexpr声明中,如果定义了一个指针,限定符号constexpr仅仅对指针有效,与指针所指对象无关。
const int p=nullptr; //p是一个指向整型常量的指针(pointer to const) constexpr int p1=nullptr; //p1是一个常量指针(const pointer)
1 2 5.noexcept 在C++11标准之前,C++在函数声明中有exception specification(异常声明)的功能,用来指定函数可能抛出的异常类型[5]。voidFunc0() throw(runtime_error);
voidFunc1() throw(); voidFunc2(); 1 2 3 函数Func0可能抛出runtime_error类型的异常;函数Func1不会抛出任何异常;函数Func2没有异常说明,则该函数可以抛出任何类型的异常。如果函数抛出了没有在异常说明中列出的异常,则编译器会调用标准库函数unexpected。默认情况下,unexpected函数会调用terminate函数终止程序。
这种异常声明的功能很少使用,因此在C++11中被弃用(实际仍可使用)。C++11引入noexcept,具有两层含义,一个是修饰符,而是操作符。具体用法如下。
(1)修饰符示例。voidFunc3() noexcept;
1 noexcept的功能相当于上面的throw(),表示函数不会抛出异常。如果noexcept修饰的函数抛出了异常,编译器可以选择直接调用std::terminate()终止程序运行。noexcept比throw()效率高一些。voidFunc4() noexcept(常量表达式);
1 如果常量表达式的结果为true,表示该函数不会抛出异常,反之则有可能抛出异常。不带常量表达式的noexcept相当于noexcept(true)。(2)操作符示例。
上面noexcept的用法是其作为修饰符时的用法,实际上noexcept还可以作为操作符,常用于模板中。template void func5() noexcept( noexcept(T()) ) {}
1 第2个noexcept就是一个操作符,如果其参数是一个有可能抛出异常的表达式,则返回值为false,那么func5有可能会抛出异常,否则返回值为true,func5为noexcept(true),不会抛出异常。这样函数是否会抛出异常,可以由表达式进行推导,使得c++11更好的支持泛型编程。
6.final和override
2012 年 3 月 22 日,GCC 4.7.0 正式发布。从这个版本开始,GCC 增加了许多新的 C++ 11 的特性。今天我们要介绍的是其中的一个特性:显式地使用 final和override关键字[6]。6.1final
(1)final用于修饰类。 final修饰类,可用于申明终结类。从此C++终于有申明终结类的关键字了。struct B1 final { };
struct D1 : B1 { }; // 错误!不能从 final 类继承!
1 2 3 上面的代码是错误的,因为 D1 试图继承 B1,而 B1 被 final声明为终结类,类似于Java的关键字的作用。(2)final用于修饰虚函数。
final用于修饰虚函数,表明子类不能重写该虚函数,为”终结虚函数“。例如:struct B2
{ virtual void f() final {} // final 函数 };struct D2 : B2
{ virtual void f() {} }; 1 2 3 4 5 6 7 8 9 这段代码会出错,因为D2::f重写了B2::f,但是B2::f却被声明为 final 。6.2override
假如我们继承基类的虚函数,在重写虚函数时写错了,参数类型不对或个数不对,但是编译没问题,造成了对基类同名函数的隐藏,运行时候和设计的不一样,override就是辅助检查是否正真重写了继承的虚函数。例如:struct B3
{ virtual void f() {} };struct D3 : B3
{ void f(int a) {} //未重写,发生隐藏,但不会报编译错误 }; 1 2 3 4 5 6 7 8 9 开发 D3 的程序员真的想重写B3::f函数吗?还是说,他只是不小心写了个与父类同名的函数,却在不经意间导致了隐藏?为了避免这种错误,C++ 11 引入了override关键字。于是,我们会发现,下面的一段代码是会出错的:struct B4
{ virtual void g(int) {} };struct D4 : B4
{ virtual void g(int) override {} // OK virtual void g(double) override {} // Error }; 1 2 3 4 5 6 7 8 9 10 多亏了override关键字,我们可以让编译器帮我们检测到这个很难发现的程序错误。这段代码的错误在于,override关键字表明,g(double)虽然想要进行override的操作,但实际父类并没有这么个函数。在实际开发中,建议大家重写继承而来的虚函数时,加上关键字virtual表明当前函数式虚函数,C++编译器的”放纵“降低了代码的可读性。值得注意的是,这些并不是一些语法糖,而是能确确实实地避免很多程序错误,并且暗示编译器可以作出一些优化。调用标记了final的virtual函数,例如上面的B2::f,GNU C++ 前端会识别出,这个函数不能被覆盖,因此会将其从类的虚表中删除。而标记为final的类,例如上面的 B1,编译器则根本不会生成虚表。这样的代码显然更有效率。
7.sizeof…运算符
sizeof…运算符的作用是获取C++11中可变参数模板中参数包中元素个数。类似sizeof,sizeof…返回一个常量表达式,而且不会对模板的实参求值[7]。例如:template<typename... Args> void g(Args... args){
cout<<sizeof...(Args)<<endl; //类型参数的数目 cout<<sizeof...(args)<<endl; //函数参数的数目 } 1 2 3 4 8.default和delete[8] 8.1default 我们知道,C++98和C++03编译器在类中会隐式地产生四个函数:默认构造函数、拷贝构造函数、析构函数和赋值运算符函数,它们被称为特殊成员函数。在 C++11 中,被称为 “特殊成员函数” 的还有两个:移动构造函数和移动赋值运算符函数。如果用户申明了上面六种函数,编译器则不会隐式产生。C++引入的default关键字,可显示地、强制地要求编译器为我们生成默认版本。class DataOnly{
public: DataOnly()=default; //default constructor ~DataOnly()=default; //destructorDataOnly(const DataOnly& rhs)=default; //copy constructorDataOnly& operator=(const DataOnly & rhs)=default; //copy assignment operatorDataOnly(const DataOnly && rhs)=default; //C++11,move constructorDataOnly& operator=(DataOnly && rhs)=default; //C++11,move assignment operator
};
1 2 3 4 5 6 7 8 9 10 11 上面的代码,就可以让编译器生成上面六个函数的默认版本。8.2delete
delete关键在C++11之前是对象释放运算符,但在C++11中,被赋予了新的功能,主要有如下几种作用: (1)禁止编译器生成上面六种函数的默认版本。class DataOnly{
public: DataOnly()=delete; //default constructor ~DataOnly()=delete; //destructorDataOnly(const DataOnly& rhs)=delete; //copy constructorDataOnly& operator=(const DataOnly & rhs)=delete; //copy assignment operatorDataOnly(const DataOnly && rhs)=delete; //C++11,move constructorDataOnly& operator=(DataOnly && rhs)=delete; //C++11,move assignment operator
};
1 2 3 4 5 6 7 8 9 10 11 (2)C++11 中,delete 关键字可用于任何函数,不仅仅局限于类成员函数。在函数重载中,可用delete来滤掉一些函数的形参类型,如下:bool isLucky(int number); // original function
bool isLucky(char) = delete; // reject chars bool isLucky(bool) = delete; // reject bools bool isLucky(double) = delete; // reject doubles and floats 1 2 3 4 这样在调用 isLucky 函数时,如果参数类型不对,则会出现错误提示if (isLucky('a'))... // error! call to deleted function
if (isLucky(true))... // error! if (isLucky(3.5))... // error! 1 2 3 (3)在模板特例化中,也可以用 delete 来过滤一些特定的形参类型。例如,Widget 类中声明了一个函数模板,当进行模板特化时,要求禁止参数为 void* 的函数调用。class Widget {
public: template void processPointer(T* ptr){} }; template<> void Widget::processPointer(void*)=delete; //deleted function template 1 2 3 4 5 9.static_assert 这个宏用于检测和诊断编译时错误,与assert(运行时断言宏)相反。static_assert用于检测编译时程序的不变量。一个表达式可以被计算为 bool 或 string (字符串),如果这个表达式的值为 false ,那么编译器会出现一个包含特定字符串的错误,同时编译失败。如果为 true 那么没有任何影响。例如:
static_assert(sizeof(void*) == 8,"not supported");
1 static_assert和type traits一起使用能发挥更大的威力。type traits是一些class template,在编译时提供关于类型的信息。在头文件中可以找到它们。这个头文件中有好几种class template,有helper class,用来产生编译时常量,有type traits class,用来在编译时获取类型信息,还有就是type transformation class,他们可以将已存在的类型变换为新的类型。下面这段代码原本期望只做用于整数类型。
template <typename T1, typename T2> auto add(T1 t1, T2 t2)
{ return t1 + t2; } 1 2 3 4 但是如果有人写出如下代码,编译器并不会报错std::cout << add(1, 3.14) << std::endl;
std::cout << add("one", 2) << std::endl; 1 2 程序会打印出4.14和”e”。但是如果我们加上编译时断言,那么以上两行将产生编译错误。template <typename T1, typename T2>
auto add(T1 t1, T2 t2) { static_assert(std::is_integral::value, "Type T1 must be integral"); static_assert(std::is_integral::value, "Type T2 must be integral"); return t1 + t2; } 1 2 3 4 5 6 7 参考文献 [1]【C++11新特性】auto关键字 [2]C++11特性:decltype关键字 [3]C++开发者都应该使用的10个C++11特性 [4]constexpr与常量表达式(c++11标准) [5][了解C++11(五)—— noexcept]{ } [6]C++11 新特性:显式 override 和 final [7]C++ primer中文版第五版:619-619 [8]C++11 之 delete 和 default [9]< type_traits>