简介
不要在构造和析构期间调用 virtual
函数,因为调用时只会调用该层次(基类)的定义而不会下降至调用派生类的定义。
基本示例
假设我们有如下类:Transaction
类作为基础交易类,其包含了一个 virtual
函数 logTransaction
用来记录交易。其子类 BuyTransaction
和 SellTransaction
分别有各自的实现。
class Transaction {
public:
Transaction();
virtual void logTransaction() const = 0;
};
class BuyTransaction : public Transaction {
public:
virtual void logTransaction() const;
};
class SellTransaction: public Transaction {
public:
virtual void logTransaction() const;
};
目前看上去没什么问题,假设我们在 Transaction
的构造函数中调用 logTransaction()
表示我们希望每一次交易初始化时就进行记录。为了方便演示我们补充一下 BuyTransaction
和 SellTransaction
的相关函数进行一些有意义的输出,如下所示 ( 完整代码见 Ex1 ):
Transaction::Transaction() {
logTransaction();
}
void BuyTransaction::logTransaction() const {
std::cout << "Logged a buy transaction" << std::endl;
}
void SellTransaction::logTransaction() const {
std::cout << "Logged a sell transaction" << std::endl;
}
我们初始化一个 BuyTransaction
对象,编译运行后输出如下信息:
BuyTransaction b;
Transaction.cpp: In constructor ‘Transaction::Transaction()’:
Transaction.cpp:21:20: warning: pure virtual ‘virtual void Transaction::logTransaction() const’ called from constructor
logTransaction();
^
/tmp/ccMUkjK5.o: In function `Transaction::Transaction()':
Transaction.cpp:(.text+0x22): undefined reference to `Transaction::logTransaction() const'
collect2: error: ld returned 1 exit status
程序提示我们找不到 Transaction::logTransaction()
的定义,并警告一个纯虚函数被调用了。这意味着,**当一个派生类 (derived class) 中的基类 (base class) 成分被构造时,其调用的所有 virtual
函数都只会调用基类中的定义,而非派生类中的定义!**这其实并不难理解:
- 从安全性来说,由于基类的构造函数先于派生类中的构造函数被调用,因此在基类的构造函数被调用时,派生类中独有的成员变量还未被初始化,假如这个时候调用派生类的虚函数定义,有可能会因为调用未初始化的成员变量而导致程序发生未定义行为。因此 C++ 禁止了此类行为,规定在基类的构造函数中调用虚函数中只会调用基类的定义,如果未定义(例如纯虚函数的情况)就会在链接阶段由于找不到定义而失败。
- 从远离来说,在基类的构造函数调用阶段,对象的类型实际上时基类的类型(在上例情况下是
Transaction
)。因此,不只是调用虚函数时会被编译器认为是基类,如果我们对其使用运行期类型信息(runtime type information,例如 dynamic_cast 和 typeid),也会将对象视为基类类型。
对于析构函数也是类似的情况。因此为了避免这种情况以及防止出错,我们不应该在构造和析构过程中调用 virtual
函数。
示例 2
通常情况下,不在构造和析构过程中调用 virtual
函数看似很简单,但是我们也需要其内调用的所有函数也保证不调用虚函数,否则也会出现类似问题,如下所示 ( 完整代码见 Ex2 ):
class Transaction {
public:
Transaction() {
init();
}
virtual void logTransaction() const = 0;
private:
void init() {
logTransaction();
}
};
class BuyTransaction : public Transaction {
// ...
};
class SellTransaction: public Transaction {
// ...
};
// ...
BuyTransaction b;
//...
pure virtual method called
terminate called without an active exception
Aborted (core dumped)
可以发现,上述代码逻辑上和第一个例子比较类似,但是用了一个 init()
函数来进行对象初始化(我们经常会使用这个方法)。但是在它内部同样也调用了virtual
函数。并且,此段代码可以顺利通过编译(不会引起编译器和链接器的错误)!而只是在运行时由于纯虚函数被调用而自动终止。假设我们将纯虚函数改为基类的实现版本:
class Transaction {
//...
// virtual void logTransaction() const = 0;
virtual void logTransaction () {
std::cout << "Logged a base transaction" << std::endl;
}
// ...
};
//...
Logged a base transaction
程序会直接调用基类的版本而不报任何错误!但实际上我们期待的是调用派生类的定义,这会给我们的调试带来极大的困扰。而唯一能避免此种情况的方法是,我们确保构造函数及其所有调用函数中都不调用 virtual
函数。
替代方法
针对上面例子,我们有没有什么办法可以既实现我们想要的效果(对不同的 Transaction
派生类对象的构造过程,进行独有的 logTransaction()
调用)并且不会出现上述问题呢?答案是有的,其中一种比较简单的方法是将 logTransaction()
改为 non-virtual
并要求派生类中的构造函数传递必要信息进而传递 logTransaction()
来调用,如下所示 ( 完整代码见 Ex3 ):
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;
};
class BuyTransaction : public Transaction {
public:
BuyTransaction() : Transaction(createLogString()) {}
private:
static std::string createLogString();
};
class SellTransaction: public Transaction {
public:
SellTransaction() : Transaction(createLogString()) {}
private:
static std::string createLogString();
};
Transaction::Transaction(const std::string& logInfo) {
std::cout << logInfo << std::endl;
}
std::string BuyTransaction::createLogString() {
return "Logged a buy transaction";
}
std::string SellTransaction::createLogString() {
return "Logged a sell transaction";
}
既然我们不能从基类构造函数中往下调用,我们可以先在派生类中将需要的信息生成并传递至基类构造函数中。这里使用了 private static
函数来创建信息,因此信息不会受成员变量影响,避免了可能会出现读取未定义成员变量的问题。
初始化两个派生类,编译运行结果如下所示,可以发现运行效果和我们的预期的一样。
// ...
BuyTransaction b;
SellTransaction s;
//...
Logged a buy transaction
Logged a sell transaction
结论
不要再构造和析构期间调用 virtual
函数,因为调用时只会调用该层次(基类)的定义而不会下降至调用派生类的定义。
完整可运行代码地址:Effective-Cpp-Reading-Note Efftive C++ 阅读笔记:Effective C++