# 总结

最近需要整理 C++ 的安全编程规范,在线上的资料中,发现 SEI CERT C++ Coding Standard 2016 edition 是相对最全面、非常规范的资料。在本书的学习中,声明(第二章)和表达式(第三章)个人觉得是最重要的两章,其次是面向对象编程(第十章)。

本书不仅讨论了 C++ 规范和对应规范的安全编程实践,还阐述了很多编程实践。通过编程实践的学习,我发现了另外两本很有价值的书,作者都是 Scott Meyers ,书名分别是 Effective Modern C++ 11 and 14 Effective C++ 3rd edition。这三本书可以帮助读者了解 C++ 设计的思想和初衷,以及 C++ 各种特性的高效使用方法。

本文是在学习 SEI CERT C++ Coding Standard 2016 edition 时,对于一些问题的探索。

# 在构造或析构函数中不要调用虚函数

根据 C++ 规范,常见的错误如下:

struct B {
 B() { seize(); }
 virtual ~B() { release(); }
protected:
 virtual void seize();
 virtual void release();
};
struct D : B {
 virtual ~D() = default;
protected:
 void seize() override {
 B::seize();
 // Get derived resources...
 }
 void release() override {
 // Release derived resources...
 B::release();
 }
};

在实例化 D 类时,B 类的构造函数会先调用。B 类在构造函数中调用了 seize 虚构函数,此时的 D 的实例还没有初始化,B 类构造函数调用的将是 B::seize,而不会调用 D::seize。析构函数也同理,在 B 的析构函数中尝试调用 virtual 的 release,本意是调用 D::release 来释放,但实际调用的是 B::release。

这样的结果是 D 类实例在构造,资源没有获取 (seize),D 类实例在析构时,资源也没有释放(release)。

如果 seize 和 release 是纯虚函数,将会导致未定义行为。不过由于这种情况下程序不能编译通过(因为 B 类没有 seize 和 release 的实现),因此这种情况可以在程序运行前被处理掉。

不过 C++ 规范中还阐述了另一种未定义行为,其原文如下:

The C++ Standard, [class.cdtor], paragraph 4 [ISO/IEC 14882-2014], states the following:

Member functions, including virtual functions, can be called during construction or
destruction. When a virtual function is called directly or indirectly from a constructor or
from a destructor, including during the construction or destruction of the class’s non
static data members, and the object to which the call applies is the object (call it x)
under construction or destruction, the function called is the final overrider in the
constructor’s or destructor’s class and not one overriding it in a more-derived class. If the
virtual function call uses an explicit class member access and the object expression
refers to the complete object of x or one of that object’s base class subobjects but not x
or one of its base class subobjects, the behavior is undefined.

最后加粗的一句话读了很多遍都没理解,不过从 13 revs AndreyT 的回答中,理解了背后的原因。

详情可参考: C++ constructors: why is this virtual function call not safe?

从可视化的角度来解释,可参考如下的一张图

该图是一张继承图的大致关系,J 继承自 H 和 I,H 和 I 的基类都有 X 类。

简单来说,在 H 的构造函数中,可以调用椭圆以内的虚函数(比如 A、E、B 的虚函数),但不能调用椭圆以外的虚函数。

假如 X 有一个名叫 func 的纯虚函数,这里给 H 的构造函数传入了一个参数,该参数是指针,指向 G 类的实例,那么此时调用该参数的 func 函数将会导致 “未定义的行为”。

现在再来回味那句话:

  • "If the virtual function call uses an explicit class member access"

    这里的 virtual function call 是 func 纯虚函数,explicit class member 是 G 实例。

  • "the object expression refers to the complete object of x or one of that object’s base class subobjects"

    这里的 "object expression" 指的是 G 类的实例,complete object 指的是 J 实例。

  • "but not x or one of its base class subobjects"

    这里的 x 是 J 实例,one of its base class subobjects 是 H 实例。

关于这句话的描述可参考:https://stackoverflow.com/a/11377756。

# 不要在析构函数或内存释放函数拋异常

这一点的详情看参考书中的 2.8 节,这一小节遇到的疑点是如下的 C++ 规范:

class SomeClass {
 Bad bad_member;
public:
 ~SomeClass()
 try {
 // ...
 } catch(...) {
 // Handle the exception thrown from the Bad destructor.
 }
};

The currently handled exception is rethrown if control reaches the end of a handler of the function-try-block of a constructor or destructor.

为什么走到构造或析构函数的 catch 块末尾,会重新拋异常呢?

这篇文章中,详细阐述了为什么需要重新拋异常的原因,这里大致总结一下:

类实例的生命周期从构造函数成功执行、顺利返回时开始,从进入析构函数时结束。简单来说,如果在构造函数的 try 中拋异常,即没有正常返回,那么该实例对象是不存在的,那么为了避免调用者使用该实例(因为该实例不存在,声明周期都没有开始),所以就不能忽略抛出的异常,必须再次抛出。析构函数同理,由于进入析构函数时,类实例的声明周期就已经结束了,那么析构函数抛出异常时,该实例已经不存在了,因此只有拋异常。