제목: “이번 주의 팁 #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)
가 다음 조건에 따라 동작하도록 하고 싶다고 가정해 봅시다:
x.length()
를 구현한 타입은length()
를 호출.x.size()
를 구현한 타입은size()
를 호출.- 그 외에는 기본적으로
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"));
위 코드에서는 length
와 size
오버로드가 동등한 순위를 가지므로, 컴파일러가 둘 중 하나를 선택하지 못해 모호성 오류가 발생합니다.
순위 기반 오버로드 적용
// 공개 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
에서 먼저 시도하고, 실패하면 Rank1
과 Rank0
으로 순차적으로 넘어갑니다.
결론
순위 기반 오버로드는 템플릿 메타프로그래밍에서 강력한 도구입니다. 그러나 사용 시 주의해야 합니다:
- 제너릭 프로그래밍은 예상치 못한 결과를 초래할 수 있습니다.
- 명확한 의도와 주석을 통해 코드 가독성을 유지하세요.
필요할 때 적절히 사용하되, 남용은 피해야 합니다.