C++ Named Constructor Idiom: 이름을 가진 생성자?

Named Constructor Idiom?

다들 아시다시피 C++의 생성자는 특별한 이름을 가질 수 없습니다. 그런데 가끔씩 가독성을 위해서 Self-descriptive한 이름의 생성자를 만들고 싶을 때가 있습니다. 아니면, 문법적 한계 때문에 꼭 필요한 경우도 생각해볼 수 있죠. 이럴 때 사용할 수 있는 것이 Named constructor idiom 입니다.

첫 번째 예로는 Named constructor idiom을 설명할 때 자주 등장하는 단골 예제를 가져와보겠습니다.

Self-descriptive한 이름 & 문법 극복

class Point
{
public:
  Point(float x, float y);         // 직교 좌표계(데카르트 좌표계)
  Point(float radius, float angle); // 극 좌표계
};

위와 같은 코드가 있습니다. 특정 위치의 점을 나타내는 Point 클래스인데요. 두 개의 생성자를 이용해 직교 좌표계, 극 좌표계 두 가지를 사용할 수 있도록 하고 싶습니다. 얼핏 보면 문제 없어보이지만, 위와 같은 코드는 사용할 수 없습니다. 두 생성자가 같은 타입의 인자를 사용하기 때문입니다.

int main()
{
  Point p{5.7, 1.2}; // 어떤 좌표계를 사용해 생성하는 걸까?
  ...
}

이럴 때 사용할 수 있는 것이 Named constructor idiom 입니다. 이름은 거창하지만 내용은 간단합니다. 생성하고자 하는 객체를 반환하는 static 함수를 만드는 것이죠. 코드로 보겠습니다.

class Point
{
public:
  Point(float x, float y) : x_(x), y_(y) {}
  static Point Rectangular(float x, float y)
  {
    return {x, y};
  }
  static Point Polar(float radius, float angle)
  {
    return {radius * std::cos(angle), radius * std::sin(angle)};
  }

private:
  float x_;
  float y_;
};

간단하죠? Rectangular 함수를 사용하면 직교 좌표계의 x, y 좌표로 Point 객체를 생성할 수 있고, Polar 함수를 사용하면 극 좌표계의 반지름, 각도 값으로 Point 객체를 생성할 수 있습니다. 아래 처럼요.

int main()
{
  Point p1 = Point::Rectangular(1.3, 2.5);
  Point p2 = Point::Polar(4.0, 0.5);
  ...
}

보이다시피 객체를 생성하는 데 두 가지 다른 방법을 사용할 수 있을 뿐더러, Self-descriptive한 이름을 가지기 때문에 가독성도 높아졌죠. 물론, 스타일에 따라 CreateWithRectangularCoordinate() 같은 긴 이름을 사용하는게 더 나았을 수는 있겠지만요.

개선점

하지만 위의 코드도 완벽한 상태는 아닙니다. 왜냐하면 기존의 생성자를 통해서도 객체를 생성할 수 있기 때문이죠. 생성자 대신 static 함수들만을 이용해 객체를 생성하도록 강제하는게 바람직하겠죠. 그러기 위해서는 생성자의 접근 지정자를 protected 혹은 private로 지정해주면 됩니다. 아래가 일부를 생략한 최종 코드입니다.

class Point
{
public:
  static Point Rectangular(float x, float y);
  static Point Polar(float radius, float angle);

private:
  Point(float x, float y) : x_(x), y_(y) {}
  float x_;
  float y_;
};

다른 예

물론 다른 목적으로도 사용될 수 있습니다. 이번에는 하나의 구체적인 시나리오를 들어보겠습니다.

  1. 구조체 Condition의 값에 따라, 다른 상태를 가지는 객체 Object를 생성하고싶다.
  2. Condition 구조체의 특정 값에 대해서는, 객체 생성이 실패하도록 하고 싶다.
  3. 그러나 생성자에서 예외를 던지는 형태로 오류 처리를 하고싶지는 않다.

위의 조건들을 충족하기 위해 Named constructor idiom을 사용할 수 있습니다. 2, 3번 조건을 충족하기 위해, 생성자 대신 optional 혹은 unique_ptr을 반환하는 static 함수를 사용하는 방식으로 말이죠. 코드를 보겠습니다.

class Object
{
public:
  static std::unique_ptr<Object> CreateWithCondition(const Condition& c)
  {
    if (c.value == 1)
    {
      return std::make_unique<Object>(COLOR_WHITE, RECTANGLE);
    }
    else
    {
      return {};
    }
  }

private:
  Object(Color color, Shape shape);
};

이러면 1, 2, 3번 조건을 충족하면서 아래처럼 사용할 수 있겠죠.

...
auto obj = Object::CreateWithCondition(c);
if (!obj)
{
  // ERROR!
}
DoSomethingWithObject(obj);
...

문제점

하지만 또 문제가 있습니다. 위처럼 static 함수에서 std::make_unique를 이용해 유니크 포인터를 생성해 반환하려는 경우 오류가 발생하는 것입니다. private 접근 지정자를 가진 생성자에 접근할 수 없기 때문이죠. 어떻게 해결할 수 있을까요?

이 문제를 해결하는 아주 흥미로운! 두 가지 트릭을 소개하겠습니다.

트릭 1: private 구조체 트릭

class Object
{
  struct private_struct {};
public:
  Object(private_struct p, Color c, Shape s);
  static std::unique_ptr<Object> CreateWithCondition(const Condition& c)
  {
    ...
    return std::make_unique<Object>(private_struct{}, COLOR_WHITE, RECTANGLE);
    ...
  }
};

문제가 해결됐습니다! 트릭이 이해 되시나요? 위처럼 코드를 작성하면, static 함수는 Object 클래스의 멤버이기 때문에 private_struct 구조체에 접근 가능하고, 생성자의 접근 지정자는 public이니 문제없이 std::make_unique로 유니크 포인터를 생성할 수 있습니다!

반면, 외부에서는 Object 클래스의 private_struct 구조체에 접근할 수 없으므로 public 접근 지정자로 공개된 생성자가 있어도 사용을 할 수 없게 되는 것이죠. 흥미롭지 않나요?

트릭 2: 상속

두 번째 트릭은 비교적 간단합니다. 바로 상속을 이용하는 것이죠. 개인적으로는 상속이 가상 함수나 소멸자 등으로 인해 모호한 오류들을 발생시킬 수 있다는 점에서 트릭에 이용하는 것이 꺼림찍합니다. 하지만 첫 번째 트릭에 비해 외적으로 깔끔하고, static 함수 내부에서 해결할 수 있다는 장점이 있어 어쨌거나 사용해볼만 하죠.

class Object
{
public:
  static std::unique_ptr<Object> CreateWithCondition(const Condition& c)
  {
    ...
    class Trick : public Object { using Object::Object; };
    return std::make_unique<Trick>(COLOR_WHITE, RECTANGLE);
    ...
  }

private:
  Object(Color c, Shape s);
};

이렇게 몇 가지 예로 Named constructor idiom에 대해 알아보았습니다. 최근에 실제 업무에서도 위와 같은 코드들을 사용할 일이 생겨서 한 번 정리해봤는데, 흥미로웠나요?

C++을 사용하다가 무언가 막혀서 검색해보면 위와 같은 트릭이나 템플릿을 이용해 흑마술을 부리는 것을 자주 볼 수 있는데, 이것도 C++만의 매력이 아닐까(?) 싶습니다. ^^;

인용 코드 출처: C++FAQ

0 0 vote
Article Rating
구독
Notify of
guest
0 Comments
Inline Feedbacks
View all comments