0%

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新特性解析与应用》