C++11(一)assert 与 noexcept
静态断言 static_assert
断言 assert 宏只有在程序运行时才能起作用,而#error 只在编译器预处理时才能起作用。有的时候,我们希望在编译时能做一些断言。读者也可以尝试一下 Boost 库内置的 BOOST_STATIC_ASSERT,其缺陷都是很明显的:诊断信息不够充分,从而难以准确定位错误的根源。
在 C++11 标准中,引人了 static_assert 断言来解决这个问题。 static_assert 使用起来非常简单,它接收两个参数,一个是断言表达式,这个表达式通常需要返回一个 bool 值;一个则是警告信息,它通常也就是一段字符串,在出错时会输出该字符信息,这样出错位置就是非常明确的。
noexcept
noexcept 形如其名,表示其修饰的函数不会抛出异常。在 C++11 中如果 noexcept 修饰的函数抛出了异常,编译器可以选择直接调用 std::terminate 函数来终止程序的运行,这比基于异常机制的 throw 在效率上会高一些。这是因为异常机制会带来一些额外开销,比如函数抛出异常,会导致函数栈被依次地展开( unwind),并依帧调用在本帧中已构造的自动变量的析构函数等。
当然, noexcept 更大的作用是保证应用程序的安全。比如一个类析构函数不应该抛出异常,那么对于常被析构函数调用的 delete 函数来说,C++默认将 delete 函数设置成 noexcept,就可以提高应用程序的安全性。而同样出于安全考虑,C++11 标准中让类的析构函数默认也是 noexcept(true) 的。当然,如果程序员显式地为析构函数指定了 noexcept,或者类的基类或成员有 noexcept(false) 的析构函数,析构函数就不会再保持默认值。
非静态成员的 sizeof
从 C 语言被发明开始, sizeof 就是一个运算符,也是 C 语言中除了加减乘除以外为数不多的特殊运算符之一。而在 C++引入类( class)类型之后, sizeof 的定义也随之进行了拓展。不过在 C++98 标准中,对非静态成员变量使用 sizeof 是不能够通过编译的。我们可以看看下面的例子。
1 |
|
注意最后一个 sizeof 操作。在 C++11 中,对非静态成员变量使用 sizeof 操作是合法的。而在 C++98 中,只有静态成员,或者对象的实例才能对其成员进行 sizeof 操作。因此如果读者只有一个支持 C++98 标准的编译器,在没有定义类实例的时候,要获得类成员的大小,我们通常会采用以下的代码:
1 | sizeof (((People*)0)->hand) |
这里我们强制转换 0 为一个 People 类的指针,继而通过指针的解引用获得其成员变量并用 sizeof 求得该成员变量的大小。而在 C++11 中,我们无需这样的技巧,因为 sizeof 可以作用的表达式包括了类成员表达式。
1 | sizeof(People::hand); |
可以看到,无论从代码的可读性还是编写的便利性,C++11 的规则都比强制指针转换的方案更胜一筹。
扩展的 friend 语法
friend 关键字用于声明类的友元,友元可以无视类中成员的属性。无论成员是 public、 protected 或是 private 的,友元类或友元函数都可以访问,这就完全破坏了面向对象编程中封装性的概念。因此,使用 friend 关键字充满了争议性。在通常情况下,面向对象程序开发的专家会建议程序员使用 Get/Set 接口来访问类的成员,但有的时候, friend 关键字确实会让程序员少写很多代码。因此即使存在争论, friend 还是在很多程序中被使用到。而 C++11 对 friend 关键字进行了一些改进,以保证其更加好用,我们可以看看下面的例子。
1 | class Poly; |
虽然在 C++11 中这是一个小的改进,却会带来一点应用的变化一程序员可以为类模板声明友元了。这在 C++98 中是无法做到的。比如下面这个例子,如代码清单 2-20 所示。
1 | class P; |
从代码中我们看到,对于 People 这个模板类,在使用类 P 为模板参数时,P 是 People< P > 的一个 friend 类。而在使用内置类型 int 作为模板参数的时候, People< int >会被实例化为一个普通的没有友元定义的类型。这样一来,我们就可以在模板实例化时才确定一个模板类是否有友元,以及谁是这个模板类的友元。这是一个非常有趣的小特性,在编写一些测试用例的时候,使用该特性是很有好处的。
final/override 控制
final 关键字的作用是使派生类不可覆盖它所修饰的虚函数:
1 | struct object{ |
派生于 Object 的 Base 类重载了 Object 的 fun 接口,并将本类中的 fun 函数声明为 final 的。那么派生于 Base 的 Derived 类对接口 fun 的重载则会导致编译时的错误。
在 C++11 中为了帮助程序员写继承结构复杂的类型,引入了虚函数描述符 override,如果派生类在虚函数声明时使用了 override 描述符,那么该函数必须重载其基类中的同名函数否则代码将无法通过编译。
外部模板
为什么需要外部模板
1 | template <typename T> |
在第一个 test1.pp 文件中,我们定义了以下代码
1 |
而在另一个 test2.cpp 文件中,我们定义了以下代码
1 |
|
由于两个源代码使用的模板函数的参数类型一致,所以在编译 test1.cpp 的时候,编译器实例化出了函数 fun
不过读者也注意到了,对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。会极大地增加编译器的编译时间和链接时间。解决这个问题的方法就是使用“外部的”模板。
显式的实例化与外部模板的声明
外部模板的使用实际依赖于 C++98 中一个已有的特性,即显式实例化。显式实例化的语法很简单,比如对于以下模板:
1 | template <typename T> |
我们只需要声明:
1 | template void fun<int>(int); |
这就可以使编译器在本编译单元中实例化出一个 fun
1 | extern template void fun<int>(int) |
首先,在 test1.cpp 做显式地实例化:
1 |
|
接下来,在 test2.cpp 中做外部模板的声明:
1 |
|
这样一来,在 test2.o 中不会再生成 fun
可以看到,由于 test2.o 不再包含 fun
参考文献
《深入理解 C++11:C++11 新特性解析与应用》