Abseil Tip 229 템플릿 메타프로그래밍을 위한 순위 기반 오버로드

제목: “이번 주의 팁 #229: 템플릿 메타프로그래밍을 위한 순위 기반 오버로드”

원문 게시일: 2024년 2월 5일
업데이트: 2024년 2월 20일

작성자: Miguel Young de la Sota, Matt Kulukundis

빠른 링크: abseil.io/tips/229


주의사항

이 팁은 템플릿 메타프로그래밍에 대한 고급 내용입니다. 템플릿 메타프로그래밍은 매우 중요한 이유가 있을 때만 사용해야 합니다. 이 글을 읽는 이유는 템플릿 메타프로그래밍이 필요한 경우이거나, 단순히 새로운 기법을 배우고 싶어서일 것입니다.


한 가지 멋진 기법

일반적으로 C++에서는 모든 함수 호출이 단일 “최적” 함수로 해결되지 않으면 모호성 오류가 발생합니다. “최적”의 정의는 암시적 변환, 타입 한정자 등 여러 요소에 의해 결정됩니다.

모호성 오류가 발생하는 상황에서, 명시적인 클래스 계층을 사용하여 원하는 방식으로 “최적”을 정의할 수 있습니다. 이를 순위 기반 오버로드(Ranked Overloads)라고 합니다. 이 기법은 클래스 계층을 활용하여 우선 순위를 지정하고, 컴파일러가 가장 높은 우선 순위의 메서드를 선택하도록 만듭니다.

예를 들어, 비어 있는 태그 타입(tag type) Rank0, Rank1, Rank2 등을 정의하고, 이를 계층적으로 상속하여 오버로드 해소 과정에서 활용할 수 있습니다.


순위 기반 오버로드 예제

// 공개 API
template <typename T>
size_t Size(const T& t);

// 내부 구현
namespace internal_size {

// 순위 계층 정의
struct Rank0 {};
struct Rank1 : Rank0 {};
struct Rank2 : Rank1 {};
struct Rank3 : Rank2 {};

// 다양한 구현
template <typename T>
size_t SizeImpl(Rank3, const std::optional<T>& x) {
  return x.has_value() ? Size(*x) : 0;
}

template <typename T>
size_t SizeImpl(Rank3, const std::vector<T>& v) {
  size_t res = 0;
  for (const auto& e : v) res += Size(e);
  return res;
}

template <typename T>
size_t SizeImpl(Rank3, const T& t)
  requires std::convertible_to<T, absl::string_view>
{
  return absl::string_view{t}.size();
}

template <typename T>
size_t SizeImpl(Rank2, const T& x)
  requires requires { x.length(); }
{
  return x.length();
}

template <typename T>
size_t SizeImpl(Rank1, const T& x)
  requires requires { x.size(); }
{
  return x.size();
}

template <typename T>
size_t SizeImpl(Rank0, const T& x) {
  return 1;
}

}  // namespace internal_size

template <typename T>
size_t Size(const T& t) {
  // 최고 순위인 Rank3에서 시작
  return internal_size::SizeImpl(internal_size::Rank3{}, t);
}

// 사용 예제
auto i = Size("foo");                      // string_view 오버로드 호출
auto j = Size(std::vector<int>{1, 2, 3});  // vector 오버로드 호출
auto k = Size(17);                         // 기본 Rank0 호출

왜 유용한가?

absl::string_view, std::vector, std::optional와 같은 오버로드는 모두 Rank3에서 처리됩니다. 만약 오버로드가 상호 배타적이라면 동일한 순위를 사용할 수 있습니다. 동일 순위의 오버로드는 병렬적으로 시도된다고 이해하면 됩니다.


자세한 예제

Size(x)가 다음 조건에 따라 동작하도록 하고 싶다고 가정해 봅시다:

  1. x.length()를 구현한 타입은 length()를 호출.
  2. x.size()를 구현한 타입은 size()를 호출.
  3. 그 외에는 기본적으로 1을 반환.

잘못된 코드

template <typename T>
size_t Size(const T& x)
  requires requires { x.length(); }
{
  return x.length();
}

template <typename T>
size_t Size(const T& x)
  requires requires { x.size(); }
{
  return x.size();
}

template <typename T>
size_t Size(const T& x) {
  return 1;
}

// 모호성 발생
auto i = Size(std::string("foo"));

위 코드에서는 lengthsize 오버로드가 동등한 순위를 가지므로, 컴파일러가 둘 중 하나를 선택하지 못해 모호성 오류가 발생합니다.


순위 기반 오버로드 적용

// 공개 API
template <typename T>
size_t Size(const T& t);

namespace internal_size {

// 순위 계층 정의
struct Rank0 {};
struct Rank1 : Rank0 {};
struct Rank2 : Rank1 {};

// 다양한 구현
template <typename T>
size_t SizeImpl(Rank2, const T& x)
  requires requires { x.length(); }
{
  return x.length();
}

template <typename T>
size_t SizeImpl(Rank1, const T& x)
  requires requires { x.size(); }
{
  return x.size();
}

template <typename T>
size_t SizeImpl(Rank0, const T& x) {
  return 1;
}

}  // namespace internal_size

template <typename T>
size_t Size(const T& t) {
  // 최고 순위인 Rank2에서 시작
  return internal_size::SizeImpl(internal_size::Rank2{}, t);
}

// 모호성 없이 동작
auto i = Size(std::string("foo"));  // 3

이 방식에서는 Rank2에서 먼저 시도하고, 실패하면 Rank1Rank0으로 순차적으로 넘어갑니다.


결론

순위 기반 오버로드는 템플릿 메타프로그래밍에서 강력한 도구입니다. 그러나 사용 시 주의해야 합니다:

  • 제너릭 프로그래밍은 예상치 못한 결과를 초래할 수 있습니다.
  • 명확한 의도와 주석을 통해 코드 가독성을 유지하세요.

필요할 때 적절히 사용하되, 남용은 피해야 합니다.