2008-08-31 12 views
12

En C++ cuando se llama a una función virtual desde un constructor, no se comporta como una función virtual. Creo que todos los que se encontraron por primera vez se sorprendieron, pero pensándolo mejor, si el constructor derivado aún no ha ejecutado el objeto es no pero derivado ¿cómo se puede llamar a una función derivada? Las condiciones previas no han tenido la oportunidad de configurarse. Ejemplo:Funciones virtuales en constructores, ¿por qué los idiomas son diferentes?

class base { 
public: 
    base() 
    { 
     std::cout << "foo is " << foo() << std::endl; 
    } 
    virtual int foo() { return 42; } 
}; 

class derived : public base { 
    int* ptr_; 
public: 
    derived(int i) : ptr_(new int(i*i)) { } 
    // The following cannot be called before derived::derived due to how C++ behaves, 
    // if it was possible... Kaboom! 
    virtual int foo() { return *ptr_; } 
}; 

Es exactamente lo mismo para Java y .NET sin embargo, optaron por ir a otro lado, fue la única razón por la el principio de la menor sorpresa?

¿Cuál crees que es la elección correcta?

Respuesta

11

Hay una diferencia fundamental en cómo los lenguajes definen tiempo de vida de un objeto. En Java y .Net, los miembros del objeto son inicializados en cero/nulo antes de ejecutar cualquier constructor y es en este punto en el que comienza el tiempo de vida del objeto. Entonces, cuando ingresas al constructor, ya tienes un objeto inicializado.

En C++, el tiempo de duración del objeto solo comienza cuando el constructor finaliza (aunque las variables miembro y las clases base están completamente construidas antes de que se inicie). Esto explica el comportamiento cuando se llaman funciones virtuales y también por qué el destructor no se ejecuta si hay una excepción en el cuerpo del constructor.

El problema con la definición de Java/.Net de la duración del objeto es que es más difícil asegurarse de que el objeto siempre cumple su invariante sin tener que poner casos especiales para cuando el objeto se inicializa pero el constructor no se ha ejecutado.El problema con la definición de C++ es que tienes este período impar donde el objeto está en el limbo y no está completamente construido.

7

Ambas formas pueden dar resultados inesperados. Su mejor opción es no llamar a una función virtual en su constructor en absoluto.

La forma en C++ creo que tiene más sentido, pero genera problemas de expectativa cuando alguien revisa su código. Si conoce esta situación, no debe colocar su código intencionalmente en esta situación para una depuración posterior.

1

Creo que C++ ofrece la mejor semántica en términos de tener el comportamiento "más correcto" ... sin embargo, es más trabajo para el compilador y el código es definitivamente no intuitivo para alguien que lo lea más tarde.

Con el enfoque .NET, la función debe ser muy limitada para no confiar en ningún estado de objeto derivado.

+1

¿Cómo funciona esto más para el compilador? Simplemente se reduce a configurar el vptr después de llamar al constructor de la clase base. Yo diría que las otras semánticas son más difíciles de implementar ya que debes asegurarte de que después de configurar el vptr en el constructor de la clase derivada, no debe ser anulado por los constructores de las clases base. (Esto supone que el envío dinámico se maneja a través de punteros a tablas de métodos virtuales, que es el enfoque más común). –

+0

@LucTouraille muy tardía +1, eso es muy cierto. el enfoque C++ es mucho más limpio y directo, en realidad –

2

Funciones virtuales en los constructores, ¿por qué los idiomas son diferentes?

Porque no hay un buen comportamiento. Encuentro que el comportamiento de C++ tiene más sentido (ya que los c-tors de clase base se llaman primero, es lógico que deben llamar a las funciones virtuales de la clase base; después de todo, la clase derivada c-tor aún no se ha ejecutado, por lo que puede no haber configurado las condiciones previas correctas para la función virtual de clase derivada).

Pero a veces, cuando quiero usar las funciones virtuales para inicializar el estado (por lo que no importa que se llamen con el estado sin inicializar), el comportamiento C#/Java es más agradable.

0

Delphi hace buen uso de constructores virtuales en el marco VCL GUI:

type 
    TComponent = class 
    public 
    constructor Create(AOwner: TComponent); virtual; // virtual constructor 
    end; 

    TMyEdit = class(TComponent) 
    public 
    constructor Create(AOwner: TComponent); override; // override virtual constructor 
    end; 

    TMyButton = class(TComponent) 
    public 
    constructor Create(AOwner: TComponent); override; // override virtual constructor 
    end; 

    TComponentClass = class of TComponent; 

function CreateAComponent(ComponentClass: TComponentClass; AOwner: TComponent): TComponent; 
begin 
    Result := ComponentClass.Create(AOwner); 
end; 

var 
    MyEdit: TMyEdit; 
    MyButton: TMyButton; 
begin 
    MyEdit := CreateAComponent(TMyEdit, Form) as TMyEdit; 
    MyButton := CreateAComponent(TMyButton, Form) as TMyButton; 
end; 
0

He encontrado el comportamiento C++ muy molesto. No puede escribir funciones virtuales para, por ejemplo, devolver el tamaño deseado del objeto y hacer que el constructor predeterminado inicialice cada elemento. Por ejemplo, sería bueno hacer:

BaseClass() { for (int i=0; i<virtualSize(); i++) initialize_stuff_for_index(i); }

Por otra parte la ventaja de C++ es un comportamiento que desalienta constuctors como el anterior se escriban.

No creo que el problema de llamar a métodos que suponen que el constructor se ha terminado es una buena excusa para C++. Si esto realmente fuera un problema, el constructor no podría llamar al ningún método, ya que el mismo problema puede aplicarse a los métodos para la clase base.

Otro punto en contra de C++ es que el comportamiento es mucho menos eficiente. Aunque el constructor sabe directamente lo que llama, el puntero vtab tiene que cambiarse para cada clase desde la base hasta la final, porque el constructor puede llamar a otros métodos que invocarán funciones virtuales. Según mi experiencia, esto consume mucho más tiempo de lo que se ahorra haciendo que las llamadas a funciones virtuales en el constructor sean más eficientes.

Mucho más molesto es que esto también es cierto para los destructores. Si escribe una función de limpieza virtual() y el destructor de clase base limpia(), ciertamente no hace lo que espera.

Esto y el hecho de que C++ llame a los destructores en objetos estáticos en la salida realmente me cabrearon durante mucho tiempo.

Cuestiones relacionadas