Programing

생성자 내에서 가상 함수 호출

lottogame 2020. 4. 25. 09:46
반응형

생성자 내에서 가상 함수 호출


두 개의 C ++ 클래스가 있다고 가정하십시오.

class A
{
public:
  A() { fn(); }

  virtual void fn() { _n = 1; }
  int getn() { return _n; }

protected:
  int _n;
};

class B : public A
{
public:
  B() : A() {}

  virtual void fn() { _n = 2; }
};

다음 코드를 작성하면

int main()
{
  B b;
  int n = b.getn();
}

n2로 설정되어 있을 것으로 예상 할 수 있습니다 .

그것은 밝혀 n1. 왜에 설정되어 있습니까?


생성자 또는 소멸자에서 가상 함수를 호출하는 것은 위험하므로 가능할 때마다 피해야합니다. 모든 C ++ 구현은 현재 생성자의 계층 수준에서 정의 된 함수 버전을 호출해야합니다.

C ++ FAQ Lite는 꽤 좋은 세부 섹션 23.7에서이 문제를 다루고 있습니다. 후속 조치를 위해 그 내용과 나머지 FAQ를 읽어보십시오.

발췌 :

[...] 생성자에서 파생 클래스에서 재정의가 아직 발생하지 않았으므로 가상 호출 메커니즘이 비활성화됩니다. 개체는 기본에서 파생 된 "기본 이전"으로 구성됩니다.

[...]

파괴는“기본 클래스 이전에 파생 된 클래스”로 수행되므로 가상 함수는 생성자에서와 같이 작동합니다. 로컬 정의 만 사용되며 객체의 (파괴 된) 파생 클래스 부분을 건드리지 않도록 함수를 재정의하는 호출은 없습니다.

편집 모두에게 가장 많이 수정되었습니다 (감사합니다)


생성자에서 다형성 함수를 호출하는 것은 대부분의 OO 언어에서 재난을 일으키는 레시피입니다. 이 상황이 발생하면 다른 언어가 다르게 수행됩니다.

기본적인 문제는 모든 언어에서 기본 유형이 파생 유형 이전에 구성되어야한다는 것입니다. 이제 문제는 생성자에서 다형성 메소드를 호출한다는 의미입니다. 당신은 그것이 어떻게 행동 할 것으로 기대합니까? 두 가지 접근 방식이 있습니다. 기본 수준에서 메소드를 호출하거나 (C ++ 스타일) 계층의 맨 아래에있는 구성되지 않은 객체에서 다형성 메소드를 호출합니다 (Java 방식).

C ++에서 Base 클래스는 자체 구성을 시작하기 전에 가상 메소드 테이블의 버전을 빌드합니다. 이 시점에서 가상 메소드에 대한 호출은 메소드의 기본 버전을 호출하거나 계층의 해당 레벨에서 구현이없는 경우 호출 되는 순수한 가상 메소드를 생성합니다 . Base가 완전히 구성되면 컴파일러는 Derived 클래스를 작성하기 시작하고 다음 레벨의 계층 구조에서 구현을 가리 키도록 메소드 포인터를 대체합니다.

class Base {
public:
   Base() { f(); }
   virtual void f() { std::cout << "Base" << std::endl; } 
};
class Derived : public Base
{
public:
   Derived() : Base() {}
   virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
   Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run

Java에서 컴파일러는 기본 생성자 또는 파생 생성자에 들어가기 전에 첫 번째 구성 단계에서 동등한 가상 테이블을 빌드합니다. 그 의미는 다릅니다 (그리고 내 취향에 더 위험합니다). 기본 클래스 생성자가 파생 클래스에서 재정의 된 메서드를 호출하면 실제로 생성되지 않은 개체에서 메서드를 호출하는 파생 된 수준에서 호출이 처리되어 예기치 않은 결과가 발생합니다. 생성자 블록 내에서 초기화 된 파생 클래스의 모든 속성은 '최종'속성을 포함하여 아직 초기화되지 않았습니다. 클래스 수준에서 기본값이 정의 된 요소는 해당 값을 갖습니다.

public class Base {
   public Base() { polymorphic(); }
   public void polymorphic() { 
      System.out.println( "Base" );
   }
}
public class Derived extends Base
{
   final int x;
   public Derived( int value ) {
      x = value;
      polymorphic();
   }
   public void polymorphic() {
      System.out.println( "Derived: " + x ); 
   }
   public static void main( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P

보시다시피, 다형성 ( C ++ 용어로 가상 ) 메소드 호출 은 일반적인 오류 원인입니다. C ++에서는 적어도 아직 구성되지 않은 객체에서 메소드를 호출하지 않을 것이라는 보장이 있습니다 ...


그 이유는 C ++ 객체가 양파처럼 내부에서 외부로 구성되기 때문입니다. 수퍼 클래스는 파생 클래스보다 먼저 구성됩니다. 따라서 B를 만들기 전에 A를 만들어야합니다. A의 생성자가 호출 될 때 아직 B가 아니므로 가상 함수 테이블에는 여전히 A의 fn () 사본에 대한 항목이 있습니다.


C ++ FAQ Lite는 꽤 잘 커버 :

기본적으로 기본 클래스 생성자를 호출하는 동안 오브젝트는 아직 파생 된 유형이 아니므로 기본 유형의 가상 함수 구현이 파생 된 유형이 아닌 호출됩니다.


문제에 대한 한 가지 해결책은 팩토리 메소드를 사용하여 오브젝트를 작성하는 것입니다.

  • afterConstruction () 가상 메소드를 포함하는 클래스 계층 구조의 공통 기본 클래스를 정의하십시오.
클래스 객체
{
공공의:
  가상 무효 afterConstruction () {}
  // ...
};
  • 팩토리 메소드를 정의하십시오.
템플릿 <클래스 C>
C * factoryNew ()
{
  C * pObject = 새로운 C ();
  pObject-> 후 공사 ();

  pObject를 반환;
}
  • 다음과 같이 사용하십시오.
MyClass 클래스 : 공개 객체 
{
공공의:
  가상 무효 afterConstruction ()
  {
    // 무언가를한다.
  }
  // ...
};

MyClass * pMyObject = factoryNew ();


Windows 탐색기의 충돌 오류를 알고 있습니까?! "순수한 가상 함수 호출 ..."
동일한 문제 ...

class AbstractClass 
{
public:
    AbstractClass( ){
        //if you call pureVitualFunction I will crash...
    }
    virtual void pureVitualFunction() = 0;
};

pureVitualFunction () 함수에 대한 구현이없고 함수가 생성자에서 호출되므로 프로그램이 중단됩니다.


vtable은 컴파일러에 의해 생성됩니다. 클래스 객체에는 vtable에 대한 포인터가 있습니다. 수명이 시작되면 해당 vtable 포인터는 기본 클래스의 vtable을 가리 킵니다. 생성자 코드의 끝에서 컴파일러는 코드를 생성하여 vtable 포인터를 클래스의 실제 vtable로 다시 지정합니다. 이렇게하면 가상 함수를 호출하는 생성자 코드가 클래스의 재정의가 아니라 해당 함수의 기본 클래스 구현을 호출 할 수 있습니다.


C ++ 표준 (ISO / IEC 14882-2014) 말의 :

가상 함수 (10.3)를 포함한 멤버 함수는 구성 또는 소멸 (12.6.2) 중에 호출 될 수 있습니다. 클래스의 비 정적 데이터 멤버의 생성 또는 소멸을 포함하여 생성자 또는 소멸자에서 가상 함수가 직접 또는 간접적으로 호출되고 호출이 적용되는 오브젝트가 생성중인 오브젝트 (x라고 함) 인 경우 또는 파괴되는 경우, 호출 된 함수는 생성자 또는 소멸자 클래스의 최종 재정 의자이며 파생 클래스에서 재정의하는 것이 아닙니다. 가상 함수 호출이 명시 적 클래스 멤버 액세스 (5.2.5)를 사용하고 오브젝트 표현식이 x의 전체 오브젝트 또는 해당 오브젝트의 기본 클래스 서브 오브젝트 중 하나를 참조하지만 x 또는 해당 기본 클래스 서브 오브젝트 중 하나가 아닌 경우, 동작은 정의되지 않습니다 .

따라서 virtual생성 순서는 기본 에서 파생으로 시작하고 소멸자의 순서는 파생에서 기본 클래스로 시작하기 때문에 생성 또는 소멸중인 객체를 호출하려고 시도하는 생성자 또는 소멸자에서 함수를 호출하지 마십시오 .

따라서 생성중인 기본 클래스에서 파생 클래스 함수를 호출하는 것은 위험합니다. 마찬가지로, 개체가 생성에서 역순으로 파괴되므로 소멸자에서 더 파생 된 클래스에서 함수를 호출하려고하면 이미 소멸 된 리소스에 액세스 할 수 있습니다 릴리스되었습니다.


다른 답변은 virtual생성자에서 호출 할 때 함수 호출이 예상대로 작동하지 않는 이유를 이미 설명했습니다 . 대신 기본 유형의 생성자에서 다형성 같은 동작을 얻는 데 사용할 수있는 다른 해결 방법을 제안하고 싶습니다.

템플리트 인수가 항상 파생 된 유형으로 추론되도록 템플리트 생성자를 기본 유형에 추가하면 파생 된 유형의 구체적 유형을 알 수 있습니다. 여기에서 static해당 파생 유형에 대한 멤버 함수를 호출 할 수 있습니다 .

이 솔루션에서는 비 static멤버 함수를 호출 할 수 없습니다 . 실행이 기본 형식의 생성자에 있지만 파생 형식의 생성자는 해당 멤버 초기화 목록을 살펴볼 시간조차 없었습니다. 생성중인 인스턴스의 파생 된 유형 부분이 초기화되지 않았습니다. 그리고 비 때문에 static데이터 멤버와 상호 작용하는 거의 확실 멤버 함수는 드문 일이 될 것입니다 원하는 파생 된 유형의 비 전화 static기본 유형의 생성자에서 멤버 함수를.

다음은 샘플 구현입니다.

#include <iostream>
#include <string>

struct Base {
protected:
    template<class T>
    explicit Base(const T*) : class_name(T::Name())
    {
        std::cout << class_name << " created\n";
    }

public:
    Base() : class_name(Name())
    {
        std::cout << class_name << " created\n";
    }


    virtual ~Base() {
        std::cout << class_name << " destroyed\n";
    }

    static std::string Name() {
        return "Base";
    }

private:
    std::string class_name;
};


struct Derived : public Base
{   
    Derived() : Base(this) {} // `this` is used to allow Base::Base<T> to deduce T

    static std::string Name() {
        return "Derived";
    }
};

int main(int argc, const char *argv[]) {

    Derived{};  // Create and destroy a Derived
    Base{};     // Create and destroy a Base

    return 0;
}

이 예제는 인쇄해야합니다

Derived created
Derived destroyed
Base created
Base destroyed

a Derived가 생성 될 때 Base생성자의 동작은 생성되는 객체의 실제 동적 유형에 따라 다릅니다.


먼저, Object가 생성 된 후 포인터에 주소를 할당합니다 .Constructors는 생성시 호출되어 데이터 멤버의 값을 초기화하는 데 사용됩니다. 객체에 대한 포인터는 객체 생성 후 시나리오로 들어옵니다. 그렇기 때문에 C ++에서는 생성자를 virtual로 만들 수 없습니다. 또 다른 이유는 가상 함수의 속성 중 하나가 포인터 만 사용할 수 있기 때문에 가상 생성자를 가리킬 수있는 생성자에 대한 포인터와 같은 것은 없다는 것입니다.

  1. 가상 함수는 생성자가 정적이므로 값을 동적으로 할당하는 데 사용되므로 가상으로 만들 수 없습니다.

지적한 바와 같이, 객체는 시공시 아래로 생성된다. 기본 개체가 생성 될 때 파생 개체가 아직 존재하지 않으므로 가상 함수 재정의가 작동하지 않습니다.

그러나 게터가 상수를 반환하거나 정적 멤버 함수로 표현 될 수있는 경우 가상 함수 대신 정적 다형성 을 사용하는 다형성 게터를 사용하면이 문제를 해결할 수 있습니다.이 예에서는 CRTP ( https://en.wikipedia.org/wiki / Curiously_recurring_template_pattern ).

template<typename DerivedClass>
class Base
{
public:
    inline Base() :
    foo(DerivedClass::getFoo())
    {}

    inline int fooSq() {
        return foo * foo;
    }

    const int foo;
};

class A : public Base<A>
{
public:
    inline static int getFoo() { return 1; }
};

class B : public Base<B>
{
public:
    inline static int getFoo() { return 2; }
};

class C : public Base<C>
{
public:
    inline static int getFoo() { return 3; }
};

int main()
{
    A a;
    B b;
    C c;

    std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;

    return 0;
}

정적 다형성을 사용하여 기본 클래스는 컴파일 타임에 정보가 제공 될 때 호출 할 클래스의 getter를 알고 있습니다.


여기서 가상 키워드의 중요성을 보지 못했습니다. b는 정적 유형 변수이며 해당 유형은 컴파일시 컴파일러에 의해 결정됩니다. 함수 호출은 vtable을 참조하지 않습니다. b가 생성되면 부모 클래스의 생성자가 호출되므로 _n 값이 1로 설정됩니다.


객체의 생성자 호출 중에 가상 함수 포인터 테이블이 완전히 작성되지 않았습니다. 이렇게하면 일반적으로 예상 한 동작이 제공되지 않습니다. 이 상황에서 가상 함수를 호출하면 작동 할 수 있지만 보장되지는 않으며 이식성이 뛰어나고 C ++ 표준을 따르지 않아야합니다.

참고 URL : https://stackoverflow.com/questions/962132/calling-virtual-functions-inside-constructors

반응형