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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std:
struct People
{
public:
int hand;
static People * all;
};

int main()
{
People p;
cout << sizeof(p.hand) << endl; //C++98 中通过,C++11 中通过
cout << sizeof(People::all)<<endl; //c++98 中通过,C+11 中通过
cout << sizeof(People::hand)<< end1; //C++98 中错误,C++11 中通过
}

注意最后一个 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
2
3
4
5
6
7
8
9
10
11
12
class Poly;
typedef Poly P;

class Lilei {
friend class Poly; //C++98 通过,C++11 通过
};
class Jim {
friend Poly; //C++98 失败,C++11 通过
};
class Hanmeimei {
friend p; //C++98 失败,C++11 通过
};

虽然在 C++11 中这是一个小的改进,却会带来一点应用的变化一程序员可以为类模板声明友元了。这在 C++98 中是无法做到的。比如下面这个例子,如代码清单 2-20 所示。

1
2
3
4
5
6
7
8
9
class P;

template <typename T>
class People{
friend T;
};

People<P> PP; //类型 P 在这里是 People 类型的友元
People<int> P; //对于 int 类型模板参数,友元声明被忽略

从代码中我们看到,对于 People 这个模板类,在使用类 P 为模板参数时,P 是 People< P > 的一个 friend 类。而在使用内置类型 int 作为模板参数的时候, People< int >会被实例化为一个普通的没有友元定义的类型。这样一来,我们就可以在模板实例化时才确定一个模板类是否有友元,以及谁是这个模板类的友元。这是一个非常有趣的小特性,在编写一些测试用例的时候,使用该特性是很有好处的。

final/override 控制

final 关键字的作用是使派生类不可覆盖它所修饰的虚函数:

1
2
3
4
5
6
7
8
9
10
struct object{
virtual void fun()=0;
};

struct Base: public object {
void fun() final; //声明为 fina1
};
struct Derived: public Base {
void fun(); //无法通过编译
};

派生于 Object 的 Base 类重载了 Object 的 fun 接口,并将本类中的 fun 函数声明为 final 的。那么派生于 Base 的 Derived 类对接口 fun 的重载则会导致编译时的错误。

在 C++11 中为了帮助程序员写继承结构复杂的类型,引入了虚函数描述符 override,如果派生类在虚函数声明时使用了 override 描述符,那么该函数必须重载其基类中的同名函数否则代码将无法通过编译。

外部模板

为什么需要外部模板

1
2
template <typename T>
void fun(T) {}

在第一个 test1.pp 文件中,我们定义了以下代码

1
2
#include test. h"
void test1() {fun(3)};

而在另一个 test2.cpp 文件中,我们定义了以下代码

1
2
#include "test.h"
void test2() {fun(4)};

由于两个源代码使用的模板函数的参数类型一致,所以在编译 test1.cpp 的时候,编译器实例化出了函数 fun(int),而当编译 test2.cpp 的时候,编译器又再一次实例化出了函数 fun(int)。那么可以想象,在 test1.o 目标文件和 test2.o 目标文件中,会有两份一模一样的函数 fun(int) 代码。而代码重复,为了节省空间,保留其中之一就可以了。事实上,大部分链接器也是这样做的,在链接的时候,链接器通过一些编译器辅助的手段将重复的模板函数代码 fun(int) 删除掉,只保留了单个副本。我们可以看看下图中的模板函数的编译与链接的过程示意。

不过读者也注意到了,对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。会极大地增加编译器的编译时间和链接时间。解决这个问题的方法就是使用“外部的”模板。

显式的实例化与外部模板的声明

外部模板的使用实际依赖于 C++98 中一个已有的特性,即显式实例化。显式实例化的语法很简单,比如对于以下模板:

1
2
template <typename T>
void fun(T){}

我们只需要声明:

1
template void fun<int>(int);

这就可以使编译器在本编译单元中实例化出一个 fun(int) 版本的函数(这种做法也被称为强制实例化)。而在 C++11 标准中,又加入了外部模板( Extern Template)的声明。语法上,外部模板的声明跟显式的实例化差不多,只是多了一个关键字 extern。对于上面的例子,我们可以通过:

1
extern template void fun<int>(int)

首先,在 test1.cpp 做显式地实例化:

1
2
3
#include "test.h"
template void fun<int>(int); //显示地实例化
void test1(){fun(3)};

接下来,在 test2.cpp 中做外部模板的声明:

1
2
3
#incude "test.h"
extern template void fun<int>(int); //外部模板的声明
void test1() {fun(3)};

这样一来,在 test2.o 中不会再生成 fun(int) 的实例代码。整个模板的实例化流程如下图所示。

可以看到,由于 test2.o 不再包含 fun(int) 的实例,因此链接器的工作很轻松,基本跟外部变量的做法是一样的,即只需要保证让 test1.cpp 和 test2.cpp 共享一份代码位置即可。而同时,编译器也不用每次都产生一份 fun(int) 的代码,所以可以减少编译时间。这里也可以把外部模板声明放在头文件中,这样所有包含 test.h 的头文件就可以共享这个外部模板声明了,这一点跟使用外部变量声明是完全一致的。

参考文献

《深入理解 C++11:C++11 新特性解析与应用》