C++ / a working model

27 / 80   ·   C++14   ·   约 9 分钟

为什么没有 virtual constructor:工厂与 clone

先记住这句话

构造函数不能声明为虚函数;创建表达式已经决定要建立的具体类型,不能靠尚未完成的对象反向选择构造类型。运行期选型应使用工厂,按现有动态类型复制则用虚 clone,构造期间的虚调用也有阶段限制。

本篇内容
  1. 创建类型在初始化之前确定
  2. 运行期选择与多态复制分开设计
  3. 构造期间虚分派止于当前层
  4. 运行示例
  5. 动手练习

创建类型在初始化之前确定

C++ 明确不允许构造函数带 virtual。调用构造的语法已经指定需要初始化的类,空间需求和基类成员结构必须由所创建类型决定;虚调用则是在已有对象接口上选择实现,两者承担的工作不同。

“构造前虚表不存在”可以帮助理解某些 ABI 的实现过程,却不是语言禁止虚构造函数的正式依据,更不能推导出构造函数里一律禁止调用虚函数。解释规则时先说明类型创建与动态分派的区别,再把虚表作为实现细节补充。

运行期选择与多态复制分开设计

如果输入决定创建哪种类型,写返回 unique_ptr<Base> 的工厂,由分支构造具体派生对象,再把所有权统一交给调用者。如果已有多态对象,需要复制其实际派生状态,可以定义 virtual unique_ptr<Base> clone() const,让派生实现复制自身。

clone 是普通虚成员函数,不是真正的虚构造函数。它返回已完成构造的新对象。智能指针返回类型不支持语言层面的协变,因此覆盖声明保持 unique_ptr<Base>;函数体返回 make_unique<Derived> 的结果时,再利用智能指针支持的转换。

构造期间虚分派止于当前层

基类构造函数对当前对象调用虚函数时,选择基类构造阶段的最终覆盖者,不会跳到尚未完成的派生层。基类初始化完成后,在派生类自身成员初始化期间以及构造函数体中,对当前对象的合法虚调用都选择该派生层的最终覆盖者,而不会进入更派生层。调用仍不得读取尚未初始化的成员;析构时同样只分派到当前正在析构的层次。

示例把基类构造中调用 kind 的结果保存为一,完整对象的 kind 则返回二,说明这不是虚机制失灵,而是对象建立过程的规则。如果需要必须依赖完整派生状态的启动操作,由工厂在成功构造后显式调用,且先明确失败时的资源回收语义。

容易答错的地方

  • 不要在基类构造函数中依赖派生覆盖来初始化派生字段;派生部分此时还没完成。
  • clone 返回裸指针会让所有权约定变隐晦;返回拥有指针时还必须确保基类销毁策略正确。

运行一个例子

最低标准 C++14 · 完整程序 · 下载 .cpp

#include <cassert>
#include <memory>

class Base {
    int observed_;
public:
    Base() : observed_(kind()) {}
    virtual int kind() const { return 1; }
    int observed_during_construction() const { return observed_; }
    virtual std::unique_ptr<Base> clone() const = 0;
    virtual ~Base() = default;
};

class Derived final : public Base {
public:
    int kind() const override { return 2; }
    std::unique_ptr<Base> clone() const override {
        return std::make_unique<Derived>(*this);
    }
};

std::unique_ptr<Base> make_object() {
    return std::make_unique<Derived>();
}

int main() {
    auto object = make_object();
    assert(object->observed_during_construction() == 1);
    assert(object->kind() == 2);
    auto copy = object->clone();
    assert(copy.get() != object.get());
    assert(copy->kind() == 2);
}

在本地编译

g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread objects-virtual-constructor.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

如果 Base::kind 也改成纯虚,能否给它补一个类外定义,让 Base 构造函数中的 kind() 调用安全?

查看参考答案

不能。构造当前对象时进行纯虚的虚调用仍是未定义行为,有函数体也不改变规则。可以改为调用非虚辅助函数,或在确实需要复用纯虚定义时使用明确的 Base::kind() 限定调用;但这种限定调用只执行基类定义,仍不会初始化或调用派生层行为。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录