아래는 “이번 주의 팁 #198: 태그 타입(Tag Types)”에 대한 한글 번역입니다.
제목: “이번 주의 팁 #198: 태그 타입(Tag Types)”
원문 게시일: 2021년 8월 12일
업데이트: 2022년 1월 24일
작성자: Alex Konradi
빠른 링크: abseil.io/tips/198
개요
다음과 같은 Foo
클래스를 고려해 봅시다:
class Foo {
public:
explicit Foo(int x, int y);
Foo& operator=(const Foo&) = delete;
Foo(const Foo&) = delete;
};
Foo
는 이동이나 복사가 불가능하지만, 생성자는 공개되어 있으므로 다음과 같이 인스턴스를 만들 수 있습니다:
std::optional<Foo> maybe_foo;
maybe_foo.emplace(5, 10);
하지만 std::optional
이 const
로 선언되면 emplace()
를 호출할 수 없습니다. 다행히도 std::optional
은 이를 해결할 수 있는 생성자를 제공합니다:
const std::optional<Foo> maybe_foo(std::in_place, 5, 10);
여기서 std::in_place
는 무엇일까요? 문서를 살펴보면 std::optional
생성자 중 하나는 std::in_place_t
타입을 첫 번째 인자로 받습니다. std::in_place
는 std::in_place_t
의 인스턴스이므로 컴파일러는 이 인자를 보고 적절한 “emplacing 생성자”를 선택합니다.
태그 타입(Tag Types)을 활용한 오버로드 해소
std::in_place_t
는 태그 타입(tag type)의 일종입니다. 태그 타입은 특정 오버로드를 선택하기 위해 컴파일러에게 정보를 제공하는 역할을 합니다. 이러한 태그 타입의 인스턴스를 함수나 생성자의 첫 번째 인자로 전달하면, 컴파일러는 해당 타입과 일치하는 오버로드를 선택합니다.
예를 들어 std::optional
의 경우, 첫 번째 인자가 std::in_place_t
이므로 컴파일러는 “생성자 오버로드”를 통해 Foo
의 생성자를 호출합니다.
이 개념은 C++11의 std::piecewise_construct_t
에서 시작되어 C++17부터는 std::in_place_t
와 같은 여러 태그 타입이 추가되었습니다.
템플릿과 태그 타입
태그 타입은 오버로드 해소 외에도 템플릿 생성자에 타입 정보를 전달하는 데 유용합니다. 다음과 같은 두 구조체를 예로 들어보겠습니다:
struct A { A(); /* 내부 멤버 */ };
struct B { B(); /* 내부 멤버 */ };
std::variant<A, B>
를 사용해 A
또는 B
를 생성하려면 다음과 같이 시도할 수 있습니다:
// A와 B가 복사 또는 이동 생성 가능할 경우 작동하지만, 불필요한 복사 비용이 발생합니다.
std::variant<A, B> with_a{A()};
std::variant<A, B> with_b{B()};
// C++는 생성자에 명시적으로 템플릿 매개변수를 지정하는 문법을 지원하지 않습니다.
std::variant<A, B> try_templating_a<A>{};
std::variant<A, B><B> try_templating_b{};
이 문제를 해결하기 위해 std::in_place_type
을 사용할 수 있습니다:
std::variant<A, B> with_a{std::in_place_type<A>};
std::variant<A, B> with_b{std::in_place_type<B>};
std::in_place_type<T>
는 std::in_place_type_t<T>
의 인스턴스로, 이 태그를 사용하면 컴파일러는 A
나 B
를 생성할 타입으로 선택합니다.
태그 타입 사용법
태그 타입은 주로 표준 라이브러리의 제네릭 클래스 템플릿과 상호작용할 때 등장합니다. 하지만 읽기 쉬운 코드 작성을 위해 팩토리 함수를 사용할 수 있습니다. 예를 들어:
// 태그 타입을 사용한 코드 (의도를 파악하기 어려움)
std::optional<Foo> with_tag(std::in_place, 5, 10);
// 팩토리 함수를 사용한 코드 (의도가 더 명확함)
std::optional<Foo> with_factory = std::make_optional<Foo>(5, 10);
팩토리 함수는 태그 타입보다 가독성이 높지만, 특정 상황에서는 동작하지 않을 수 있습니다. 예를 들어:
// Foo가 move-constructible이 아닌 경우 컴파일 오류가 발생합니다.
std::optional<std::optional<Foo>> foo(std::make_optional<Foo>(5, 10));
이 문제는 std::in_place
를 사용하면 해결됩니다:
// 모든 것을 제자리에서 생성하여 Foo의 생성자를 단 한 번만 호출합니다.
std::optional<std::optional<Foo>> foo(std::in_place, std::in_place, 5, 10);
태그 타입의 장점
- 태그 타입은 리터럴 타입(literal type)이므로
constexpr
인스턴스를 헤더 파일에 선언할 수 있습니다. - 태그 타입은 빈(empty) 타입이므로 컴파일러가 최적화하여 런타임 오버헤드가 없습니다.
결론
태그 타입은 컴파일러에게 추가 정보를 제공하고, 오버로드를 해소하는 강력한 방법입니다. 표준 라이브러리에서는 생성자 호출 시 태그 타입을 사용해 명확성과 유연성을 확보합니다. 여러분도 필요에 따라 태그 타입을 사용하여 오버로드나 템플릿 타입 전달을 해결할 수 있습니다.
추가 참고 자료
- std::integer_sequence: 컴파일 타임 인덱싱
- passkey와 std::make_shared: 접근 제어를 위한 태그 사용