주간 팁 #134: make_unique
와 private
생성자
원래 게시일: 2017년 5월 10일 (TotW #134)
작성자: Yitzhak Mandelbaum, Google 엔지니어
최종 업데이트: 2020년 4월 6일
빠른 링크: abseil.io/tips/134
개요
팁 #126을 읽고 이제 new
를 사용하지 않으려고 결심했습니다.
모든 것이 잘 진행되고 있었지만, std::make_unique
를 사용해 private 생성자를 가진 객체를 생성하려고 할 때 컴파일 오류가 발생했습니다.
이 문제의 구체적인 예를 살펴보고 원인을 이해한 다음 해결 방법을 논의해 보겠습니다.
예제: 위젯 생성
위젯을 나타내는 클래스를 정의한다고 가정합니다. 각 위젯에는 고유 식별자가 있으며, 이러한 식별자는 특정 제약 조건을 따라야 합니다.
이 제약 조건을 항상 보장하기 위해, Widget
클래스의 생성자를 private로 선언하고 사용자에게 적절한 식별자로 위젯을 생성할 수 있는 팩토리 함수 Make
를 제공합니다. (팁 #42에서 팩토리 함수가 초기화 메서드보다 선호되는 이유를 확인하세요.)
class Widget {
public:
static std::unique_ptr<Widget> Make() {
return std::make_unique<Widget>(GenerateId());
}
private:
Widget(int id) : id_(id) {}
static int GenerateId();
int id_;
};
하지만 컴파일 시 다음과 같은 오류가 발생합니다:
error: calling a private constructor of class 'Widget'
{ return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
^
note: in instantiation of function template specialization
'std::make_unique<Widget, int>' requested here
return std::make_unique<Widget>(GenerateId());
^
note: declared private here
Widget(int id) : id_(id) {}
^
이유는 Make
함수는 private 생성자에 접근할 수 있지만, std::make_unique
는 접근할 수 없기 때문입니다.
이 문제는 친구(friend) 관계에서도 발생할 수 있습니다. 예를 들어, Widget
클래스의 친구가 std::make_unique
를 사용하여 Widget
을 생성하려고 해도 동일한 문제가 발생합니다.
권장 사항
다음 대안을 고려하세요:
1. new
와 absl::WrapUnique
사용 (이유 설명 포함)
// private 생성자에 접근하기 위해 `new`를 사용합니다.
return absl::WrapUnique(new Widget(...));
2. 생성자를 공개(public)로 변경
생성자가 공개되어도 안전하다면, 이를 공개로 변경하고 언제 직접 생성자를 사용할 수 있는지 명확히 문서화하세요.
많은 경우에 생성자를 private으로 설정하는 것은 과도한 설계일 수 있습니다.
이 경우 생성자를 공개로 선언하고 적절히 문서화하는 것이 가장 좋은 해결책입니다.
그러나 클래스 불변성을 보장해야 하므로 생성자를 private으로 유지해야 하는 경우, new
와 WrapUnique
를 사용하는 것이 적합합니다.
std::make_unique
를 친구로 선언할 수 없을까?
std::make_unique
(또는 absl::make_unique
)를 친구로 선언하여 private 생성자에 접근할 수 있도록 하는 방법도 생각할 수 있습니다.
하지만 이는 좋은 방법이 아닙니다. 이유는 다음과 같습니다:
-
“장거리 친구 선언”을 피해야 합니다.
친구 선언은 관리 비용이 늘어나며, 스타일 가이드에서도 이를 지양할 것을 권장합니다. (Google C++ 스타일 가이드를 참고하세요.) -
구현 세부 사항에 의존하게 됩니다.
예를 들어,make_unique
가 내부적으로new
를 호출한다는 가정에 의존합니다.
나중에 구현이 변경되거나 다른 방식으로new
를 호출하게 된다면 이 가정은 깨질 수 있습니다. -
접근 권한이 널리 퍼집니다.
make_unique
를 친구로 선언하면 모든 사용자가 이를 통해 객체를 생성할 수 있습니다.
그렇다면 차라리 생성자를 공개로 선언하는 것이 더 나은 선택일 수 있습니다.
std::shared_ptr
의 경우는?
std::shared_ptr
에서는 상황이 다릅니다. absl::WrapShared
와 같은 대체제가 없으며, std::shared_ptr<T>(new T(...))
를 사용하는 경우 두 번의 메모리 할당이 발생합니다.
반면, std::make_shared
는 한 번의 메모리 할당으로 처리됩니다.
이 차이가 중요하다면, 패스키(passkey) 패턴을 고려하세요.
이 패턴은 생성자가 특별한 토큰을 받도록 설계하며, 특정 코드에서만 이 토큰을 생성할 수 있습니다.
예제:
class Widget {
class Token {
private:
explicit Token() = default;
friend Widget;
};
public:
static std::shared_ptr<Widget> Make() {
return std::make_shared<Widget>(Token{}, GenerateId());
}
Widget(Token, int id) : id_(id) {}
private:
static int GenerateId();
int id_;
};
위 코드에서 explicit
키워드를 사용하여 기본 생성자를 명시적으로 선언했습니다.
이는 C++17 이전에서는 Token
클래스가 집계형(aggregate)이 되어 {}
를 사용한 초기화가 가능해지는 문제를 방지하기 위함입니다.
패스키 패턴에 대한 자세한 내용은 아래 자료를 참고하세요: