Abseil Tip 198 태그 타입(Tag Types)

아래는 “이번 주의 팁 #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::optionalconst로 선언되면 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_placestd::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>의 인스턴스로, 이 태그를 사용하면 컴파일러는 AB를 생성할 타입으로 선택합니다.


태그 타입 사용법

태그 타입은 주로 표준 라이브러리의 제네릭 클래스 템플릿과 상호작용할 때 등장합니다. 하지만 읽기 쉬운 코드 작성을 위해 팩토리 함수를 사용할 수 있습니다. 예를 들어:

// 태그 타입을 사용한 코드 (의도를 파악하기 어려움)
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) 타입이므로 컴파일러가 최적화하여 런타임 오버헤드가 없습니다.

결론

태그 타입은 컴파일러에게 추가 정보를 제공하고, 오버로드를 해소하는 강력한 방법입니다. 표준 라이브러리에서는 생성자 호출 시 태그 타입을 사용해 명확성유연성을 확보합니다. 여러분도 필요에 따라 태그 타입을 사용하여 오버로드나 템플릿 타입 전달을 해결할 수 있습니다.


추가 참고 자료