为什么C++中没有虚构造函数?

304

C++为什么没有虚拟构造函数?


17
如果C++有虚构造函数,你会如何使用它们? - R Sahu
11
虚函数有助于动态绑定,这发生在运行时。对象是在运行时创建的,对象的创建需要构造函数。如果这个构造函数是虚拟的,那么会出现“狗追尾巴”的情况(狗代表可怜的编译器 :p)。 - gawkface
2
@RSahu:很可能,请求在C++中使用虚构造函数的人心中有一个想法,即它是一个复制构造函数,并且将根据复制构造函数的参数动态调用。这是有逻辑意义的,但是C++无法处理通过不是指针的实例进行虚拟分派,也不能处理暗示动态堆栈(或更糟的静态)内存的情况。 - Joshua
23个回答

294

听马嘴里说。

来自 Bjarne Stroustrup 的 C++ 风格与技术 FAQ为什么我们没有虚拟构造函数?

虚拟调用是一种在部分信息下完成工作的机制。特别地,“虚拟”允许我们调用一个函数,只需知道任何接口而不需要知道对象的确切类型。要创建一个对象,您需要完全的信息。特别地,您需要知道要创建的对象的确切类型。因此,“对构造函数的调用” 不能是虚拟的。

该 FAQ 条目继续给出了一种实现类似效果的方法,而不需要虚拟构造函数的代码。


能否强制派生类具有无参数构造函数? - axell-brendow
@axell-brendow 有什么意义呢? - user253751

144

虚函数基本上提供了多态行为。也就是说,当您使用一个对象时,它的动态类型与其引用的静态(编译时)类型不同时,它会提供适用于实际对象类型而非对象静态类型的行为。

现在尝试将这种行为应用到构造函数中。当您构造一个对象时,其静态类型始终与实际对象类型相同,因为:

  

要构造一个对象,构造函数需要该对象的确切类型[...]此外[...]您不能拥有指向构造函数的指针

(Bjarne Stroustup《C++语言程序设计》第424页)


4
“你不能拥有指向构造函数的指针”。“指向构造函数的指针”具有与空结构体一样的运行时信息,因为构造函数没有名称。 - curiousguy
@curiousguy:指向构造函数的指针非常有意义。如果你有一个,它会像放置新对象一样运作。 - Joshua
@Joshua 然后...使用放置 new。这个用例是什么? - curiousguy

68
与Smalltalk或Python等面向对象的语言不同,其中构造函数是表示类的对象的虚拟方法(这意味着您不需要GoF 抽象工厂模式,因为您可以传递代表该类的对象而不是自己创建),C++是一种基于类的语言,并且没有表示任何语言结构的对象。 类在运行时不存在,因此您无法在其上调用虚拟方法。
尽管如此,这符合“你不付出你不使用的东西”的哲学,但我见过的每个大型C++项目都最终实现了某种形式的抽象工厂或反射机制。

4
这正是在C++和像Delphi这样的语言中进行构造的区别所在,后者有虚拟构造函数。说得好。 - Frederik Slijkerman
5
在阅读了关于其他语言中对象创建方式的解释之后,我才明白这个问题是如何有意义的。+1。 - j_random_hacker
4
"Advanced C++" 一书作者 James Coplien 讲解了如何在 C++ 中实现虚拟构造函数(例如,new animal("dog"))。请参见 http://users.rcn.com/jcoplien/Patterns/C++Idioms/EuroPLoP98.html#VirtualConstructor 获取有关其实现方式的更多信息。 - Tony Lee

46

我能想到的两个原因:

技术原因

对象只有在构造函数结束后才存在。为了使用虚表分派构造函数,必须有一个现有的对象,并且有一个指向虚表的指针,但是如果对象仍然不存在,那么如何存在指向虚表的指针呢? :)

逻辑原因

当您想声明一种略微多态的行为时,会使用虚关键字。但是构造函数与多态无关,在C ++中,构造函数的工作只是将对象数据放置在内存中。由于虚表(以及通常的多态性)都涉及多态行为而非多态数据,因此声明虚构造函数没有意义。


1
虚析构函数没有展现多态行为?你确定第二个原因吗? - roottraveller
虚函数表是如何更新的?它们真的需要更新虚函数指针吗? - ajaysinghnegi

18

摘要: C++标准可能会为"虚构造函数"规定一种符合直觉且对编译器支持不太困难的表示形式和行为,但为什么要针对此特定功能做出标准更改呢,当使用create() / clone()(见下文)已经可以清晰地实现这个功能?与流程管道中的许多其他语言提案相比,它远远不如那么有用。

讨论

假设我们有一个"虚构造函数"机制:

Base* p = new Derived(...);
Base* p2 = new p->Base();  // possible syntax???

在上面的代码中,第一行构造了一个Derived对象,因此*p的虚函数表可以合理地提供“虚构造函数”以在第二行中使用。(本页上数十个回答声称“对象尚不存在,因此无法进行虚拟构造”的观点过于狭隘,只关注即将构造的对象。)

第二行假定记号new p->Base()请求动态分配并默认构造另一个Derived对象。

注意:

  • 编译器必须在调用构造函数之前协调内存分配-构造函数通常支持自动(非正式的"堆栈")分配、静态(用于全局/命名空间作用域和类/函数-静态对象)和动态(非正式的"堆")当使用new

    • 要构造p->Base()中的对象的大小通常无法在编译时得知,因此动态分配是唯一有意义的途径

  • 对于动态分配,它必须返回指针,以便稍后进行delete

  • 假定的记号明确列出new以强调动态分配和指针结果类型。

编译器需要:

  • 找出Derived需要多少内存,可以通过调用隐式的virtual sizeof函数或通过RTTI获得此类信息
  • 调用operator new(size_t)来分配内存
  • 使用放置new调用Derived()

或者

  • 为将动态分配和构造结合在一起的函数创建额外的虚表项

因此-似乎规定和实现虚构造函数并非难以克服的问题,但是百万美元的问题是:它比使用现有的C++语言特性更好吗...? 就个人而言,我认为下面的解决方案没有任何好处。


`clone()`和`create()`

C++ FAQ文档记录了一个“虚构造函数”习惯用法,其中包含virtualcreate()clone()方法以默认构造或复制构造新的动态分配对象:

class Shape {
  public:
    virtual ~Shape() { } // A virtual destructor
    virtual void draw() = 0; // A pure virtual function
    virtual void move() = 0;
    // ...
    virtual Shape* clone() const = 0; // Uses the copy constructor
    virtual Shape* create() const = 0; // Uses the default constructor
};
class Circle : public Shape {
  public:
    Circle* clone() const; // Covariant Return Types; see below
    Circle* create() const; // Covariant Return Types; see below
    // ...
};
Circle* Circle::clone() const { return new Circle(*this); }
Circle* Circle::create() const { return new Circle(); }

也可以更改或重载create()以接受参数,但为了匹配基类/接口的virtual函数签名,重载的参数必须完全匹配其中一个基类重载。有了这些明确的用户提供的功能,很容易添加日志记录、仪器调节、修改内存分配等。


1
不同之处在于这些“克隆”和“创建”函数不适用于容器,也不适用于值传递等内容。因此它们无法实现我们想要的——多态性而不会破坏常规的值语义。 - David Schwartz
1
@DavidSchwartz:clonecreate不能直接与标准容器一起使用,但编写一个小的管理类型从复制构造函数等处进行clone非常简单(例如,请参见此处)。这样的管理对象也可以通过值传递,如果您发现使用引用更容易。使用clone / create,将private和管理对象设置为“friend”,可以确保一致的使用。尽管如此,这确实是一种额外的复杂性,可能会使新的C ++程序员感到沮丧... - Tony Delroy
1
这并不是小菜一碟。链接指向的代码已经相当复杂,甚至不能使标准容器工作。例如,没有 operator<。此外,由于它不是语言的一部分,因此将使用这种东西的代码与不使用它的代码进行互操作将非常困难。 - David Schwartz

15

我们有这个功能,只不过它不是一个构造函数 :-)

struct A {
  virtual ~A() {}
  virtual A * Clone() { return new A; }
};

struct B : public A {
  virtual A * Clone() { return new B; }
};

int main() {

   A * a1 = new B;
   A * a2 = a1->Clone();    // virtual construction
   delete a2;
   delete a1;
}

1
一个使用案例,请参见: virtual __fastcall TYesNoDialog(TComponent *Owner); - Eric

15

除了语义上的原因,直到对象被构造后才会有vtable,因此虚拟指定是无用的。


3
错误。vtable是静态且不变的。它们存在于可执行文件的代码和静态数据加载之后。 - curiousguy
1
正确,它们被定义为静态和常量,但只是没有被分配和设置。 - Marius
1
它们在程序启动时设置。 - curiousguy
2
@Rich 虚函数在构造函数中的工作方式与任何其他地方完全相同。虚函数调用始终基于对象的动态类型。 - curiousguy
2
@Rich No:在基类构造函数中,构造函数中的虚函数调用将动态地调用基类版本,基于此时的动态类型:基类。正在构建对象上的虚拟调用无论是在构造函数体中还是在构造函数调用的任何其他函数中都是相同的。基类子对象的动态类型随着派生类的构造开始而改变。您只能通过打印typeid(*this).name()来查看它。 - curiousguy
显示剩余4条评论

7
在C++中,虚函数是运行时多态性的一种实现方式,它们将进行函数重写。通常情况下,在需要动态行为时使用virtual关键字。它只有在对象存在时才会起作用。而构造函数被用于创建对象。构造函数将在对象创建时被调用。
因此,如果你将构造函数声明为virtual,根据虚拟关键字的定义,它应该有现有的对象来使用,但是构造函数用于创建对象,所以这种情况永远不会存在。因此,你不应该使用构造函数作为虚函数。
因此,如果我们尝试声明虚构造函数,编译器会抛出错误:

Constructors cannot be declared virtual


7
您可以在 @stefan 的答案中找到示例和技术原因。根据我的理解,这个问题的逻辑答案是:
虚拟关键字的主要用途是在我们不知道基类指针将指向哪种对象类型时,启用多态行为。
但是,更简单的想法是,要使用虚拟功能,您需要一个指针。指针需要什么?指向的对象!(考虑到程序的正确执行情况)
因此,我们基本上需要一个已经存在于内存中的对象(我们不关心内存是在编译时还是运行时分配的),以便我们的指针可以正确地指向该对象。
现在,考虑一下当要指向的类的对象被分配了某些内存时的情况 -> 它的构造函数将在那个实例自动调用!
因此,我们可以看到我们实际上不需要担心构造函数是否为虚拟函数,因为在任何情况下,只要您希望使用多态行为,我们的构造函数就已经被执行,使我们的对象可供使用!

6

虚函数被用来根据指针所指向的对象类型调用函数,而不是根据指针本身的类型调用函数。但是构造函数并没有被“调用”,它只在对象被声明时调用一次。因此,在C++中,构造函数不能被声明为虚函数。


那么,为什么析构函数可以是虚函数? - Bình Nguyên
快速搜索可以得到答案:https://dev59.com/IHRB5IYBdhLWcg3w77on - Rayee Roded

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接