C++

C++ 가상 생성자(Virtual Constructor) 총정리

김구티2 2024. 4. 8. 22:27

1. 가상 생성자의 개념

클래스 생성자를 C++로 가상화하여 다형성 객체를 생성할 수 있을까? Nope. C++는 정적으로 입력된 언어이기 때문에 C++ 컴파일러는 객체를 다형성으로 생성하는 것이 무의미하다. 컴파일러는 객체를 생성하기 위해 클래스 유형을 알고 있어야 한다. 즉, 어떤 유형의 객체를 생성할 것인지는 C++ 컴파일러 관점에서 컴파일 타임 결정이다. 만약 생성자를 가상화한다면 컴파일러는 오류를 플래그로 지정한다. 실제로 생성자 선언문에는 인라인을 제외하고는 다른 키워드가 허용되지 않는다.

실제 시나리오에서, 어떤 입력을 기반으로 클래스 계층 구조에서 파생된 클래스 객체를 생성해야 한다. 즉, 객체 생성과 객체 유형은 긴밀하게 연결되어 있기 때문에 수정 사항을 확장해야 하는 것이다. 가상 생성자의 목적은 객체 생성과 객체 유형을 분리하는 것이다.

그렇다면 런타임에 필요한 유형의 객체를 어떻게 만들 수 있을까?

 

예시:

#include <iostream>
using namespace std;

//// LIBRARY START
class Base
{
public:

     Base() { }

     virtual // Ensures to invoke actual object destructor
     ~Base() { }

     // An interface
     virtual void DisplayAction() = 0;
};

class Derived1 : public Base
{
public:
     Derived1()
     {
          cout << "Derived1 created" << endl;
     }

     ~Derived1()
     {
          cout << "Derived1 destroyed" << endl;
     }

     void DisplayAction()
     {
          cout << "Action from Derived1" << endl;
     }
};

class Derived2 : public Base
{
public:
     Derived2()
     {
          cout << "Derived2 created" << endl;
     }

     ~Derived2()
     {
          cout << "Derived2 destroyed" << endl;
     }

     void DisplayAction()
     {
          cout << "Action from Derived2" << endl;
     }
};

//// LIBRARY END

class User
{
public:

     // Creates Drived1
     User() : pBase(nullptr)
     {
          // What if Derived2 is required? - Add an if-else ladder (see next sample)
          pBase = new Derived1();
}

~User()
{
     if( pBase )
     {
          delete pBase;
          pBase = nullptr;
     }
}

// Delegates to actual object
void Action()
{
     pBase->DisplayAction();
}

private:
     Base *pBase;
};

int main()
{
     User *user = new User();

     // Need Derived1 functionality only
     user->Action();

     delete user;
}

 

위 예시에서 계층 구조 Base, Derived1 및 Derived2가 라이브러리 코드의 일부라고 가정한다. 클래스 User는 계층 구조를 사용하려고 하는 유틸리티 클래스다. main 함수는 User 클래스를 통해 Base 계층 기능을 사용하는 것이다.

 

User 클래스 생성자는 항상 Derived1 객체를 생성하고 있다. User의 소비자(위 예시에서는 main)에게 Derived2 기능이 필요한 경우, User는 new Derived2()를 생성해야 하며, 이는 재컴파일을 강제한다.

자세한 내용을 살펴보기 전에, Derived1 또는 Derived2 객체 중 누가 작성하도록 지시할 것인지에 대한 질문 답하도록 한다. 분명하게, User 클래스의 소비자다. User 클래스는 아래의 예시와 같이 if else를 사용하여 Derived1 또는 Derived2 객체를 작성할 수 있다.

 

예시:

#include <iostream>
using namespace std;

//// LIBRARY START
class Base
{
public:
     Base() { }

     virtual // Ensures to invoke actual object destructor
     ~Base() { }

     // An interface
     virtual void DisplayAction() = 0;
};

class Derived1 : public Base
{
public:
     Derived1()
     {
          cout << "Derived1 created" << endl;
     }

     ~Derived1()
     {
          cout << "Derived1 destroyed" << endl;
     }

     void DisplayAction()
     {
          cout << "Action from Derived1" << endl;
     }
};

class Derived2 : public Base
{
public:
     Derived2()
     {
          cout << "Derived2 created" << endl;
     }

     ~Derived2()
     {
          cout << "Derived2 destroyed" << endl;
     }

     void DisplayAction()
     {
          cout << "Action from Derived2" << endl;
     }
};

//// LIBRARY END

class User
{
public:

     // Creates Derived1 or Derived2 based on input
     User() : pBase(nullptr)
     {
          int input; // ID to distinguish between
                         // Derived1 and Derived2

          cout << "Enter ID (1 or 2): ";
          cin >> input;

          while( (input != 1) && (input != 2) )
          {
               cout << "Enter ID (1 or 2 only): ";
               cin >> input;
          }

          if( input == 1 )
          {
               pBase = new Derived1;
          }
          else
          {
               pBase = new Derived2;
          }

          // What if Derived3 being added to the class hierarchy?
     }

     ~User()
     {
          if( pBase )
          {
               delete pBase;
               pBase = nullptr;
          }
     }

     // Delegates to actual object
     void Action()
     {
          pBase->DisplayAction();
     }

private:
     Base *pBase;
};

int main()
{
     User *user = new User();

     // Need either Derived1 or Derived2 functionality
     user->Action();

     delete user;
}

 

위의 코드는 확장을 위해 명백하게 열려 있지 않으며, 유연하지 않은 설계이다. 라이브러리가 Base 클래스 계층 구조를 새 클래스 Derived3로 업데이트하면, User 클래스는 Derived3 객체를 어떻게 만들 수 있을까? 한 가지 방법은 아래 예시와 같이 새 입력 ID 3을 기반으로 Derived3 객체를 만드는 if else를 업데이트하는 것이다.

 

예시:

#include <iostream>
using namespace std;

class User
{
public:
     User() : pBase(nullptr)
     {
          // Creates Drived1 or Derived2 based on need

          int input; // ID to distinguish between
                         // Derived1 and Derived2

          cout << "Enter ID (1 or 2): ";
          cin >> input;

          while( (input != 1) && (input != 2) )
          {
               cout << "Enter ID (1 or 2 only): ";
               cin >> input;
          }

          if( input == 1 )
          {
               pBase = new Derived1;
          }
          else if( input == 2 )
          {
               pBase = new Derived2;
          }
          else
          {
               pBase = new Derived3;
          }
     }

     ~User()
     {
          if( pBase )
          {
               delete pBase;
               pBase = nullptr;
          }
     }

     // Delegates to actual object
     void Action()
     {
          pBase->DisplayAction();
     }

private:
     Base *pBase;
};

 

위의 수정은 User 클래스의 사용자들에게 다시 컴파일을 강요한다. 그리고 역시나 이것은 잘못된, 무제한적인 설계다. 또한, Base 확장으로 인해 User 클래스는 더 이상의 수정으로부터 닫히지 않게 된다.


문제는 객체를 만드는 것이다. 계층에 새 클래스를 추가하면 User 클래스의 종속 변수가 다시 컴파일된다. 객체를 만드는 작업을 클래스 계층 자체 또는 가상으로 동작하는 함수에 위임할 수는 없을까? 우리는 객체를 만드는 작업을 클래스 계층(또는 정적 함수)에 위임함으로써 User와 Base 계층 간의 긴밀한 결합을 피할 수 있다.

 

예시:

#include <iostream>
using namespace std;

//// LIBRARY START
class Base
{
public:

     // The "Virtual Constructor"
     static Base *Create(int id);

     Base() { }

     virtual // Ensures to invoke actual object destructor
     ~Base() { }

     // An interface
     virtual void DisplayAction() = 0;
};

class Derived1 : public Base
{
public:
     Derived1()
     {
          cout << "Derived1 created" << endl;
     }

     ~Derived1()
     {
          cout << "Derived1 destroyed" << endl;
     }

     void DisplayAction()
     {
          cout << "Action from Derived1" << endl;
     }
};

class Derived2 : public Base
{
public:
     Derived2()
     {
          cout << "Derived2 created" << endl;
     }

     ~Derived2()
     {
          cout << "Derived2 destroyed" << endl;
     }

     void DisplayAction()
     {
          cout << "Action from Derived2" << endl;
     }
};

class Derived3 : public Base
{
public:
     Derived3()
     {
          cout << "Derived3 created" << endl;
     }

     ~Derived3()
     {
          cout << "Derived3 destroyed" << endl;
     }

     void DisplayAction()
     {
          cout << "Action from Derived3" << endl;
     }
};

// We can also declare "Create" outside Base
// But it is more relevant to limit it's scope to Base
Base *Base::Create(int id)
{
     // Just expand the if-else ladder, if new Derived class is created
     // User code need not be recompiled to create newly added class objects

     if( id == 1 )
     {
          return new Derived1;
     }
     else if( id == 2 )
     {
          return new Derived2;
     }
     else
     {
          return new Derived3;
     }
}
//// LIBRARY END

//// UTILITY START
class User
{
public:
     User() : pBase(nullptr)
     {
          // Receives an object of Base hierarchy at runtime

          int input;

          cout << "Enter ID (1, 2 or 3): ";
          cin >> input;

          while( (input != 1) && (input != 2) && (input != 3) )
          {
               cout << "Enter ID (1, 2 or 3 only): ";
               cin >> input;
          }

          // Get object from the "Virtual Constructor"
          pBase = Base::Create(input);
     }

     ~User()
     {
          if( pBase )
          {
               delete pBase;
               pBase = nullptr;
          }
     }

     // Delegates to actual object
     void Action()
     {
          pBase->DisplayAction();
     }

private:
     Base *pBase;
};

//// UTILITY END

//// Consumer of User (UTILITY) class
int main()
{
     User *user = new User();

     // Action required on any of Derived objects
     user->Action();

     delete user;
}

 

User 클래스는 객체 생성과 독립적이다. Base에 그 책임을 위임하고, ID 형태의 입력을 제공한다. 라이브러리가 새로운 클래스 Derived4를 추가하면, 라이브러리 수정자가 Create 내부의 if else문을 확장하여 적절한 객체를 반환한다. User의 소비자는 Base 확장으로 인해 코드를 다시 컴파일할 필요가 없다.

Create 함수는 런타임에 다른 유형의 Base 클래스 개체를 반환하는 데 사용된다. Factory Method라고도 하는 가상 생성자와 같은 역할을 하는 것이다.

728x90