C++ Tag Dispatch 완벽 가이드 | 컴파일 타임 함수 선택과 STL 최적화
이 글의 핵심
C++ Tag Dispatch 완벽 가이드에 대한 실전 가이드입니다. 컴파일 타임 함수 선택과 STL 최적화 등을 예제와 함께 상세히 설명합니다.
Tag Dispatch란? 왜 필요한가
문제 시나리오: Iterator별 최적화
문제: std::advance(it, n)은 iterator를 n만큼 이동시킵니다. vector의 iterator는 Random Access라 it += n (O(1))이 가능하지만, list의 iterator는 Bidirectional이라 while (n--) ++it (O(n))만 가능합니다. 어떻게 컴파일 타임에 최적 구현을 선택할까요?
해결: Tag Dispatch는 빈 구조체(태그)를 함수 인자로 전달해, 오버로딩 해석으로 컴파일 타임에 함수를 선택합니다.
#include <iterator>
#include <iostream>
// Random Access: O(1)
template<typename Iter>
void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
std::cout << "Fast: it += n\n";
it += n;
}
// Bidirectional: O(n)
template<typename Iter>
void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
std::cout << "Slow: while loop\n";
if (n >= 0) {
while (n--) ++it;
} else {
while (n++) --it;
}
}
// 인터페이스
template<typename Iter>
void my_advance(Iter& it, int n) {
// iterator_traits로 태그 추출 → 오버로딩 해석
advance_impl(it, n, typename std::iterator_traits<Iter>::iterator_category{});
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it1 = vec.begin();
my_advance(it1, 2); // "Fast: it += n"
std::list<int> lst = {1, 2, 3, 4, 5};
auto it2 = lst.begin();
my_advance(it2, 2); // "Slow: while loop"
}
flowchart TD
start["my_advance(it, n)"]
traits["iterator_traitsIter iterator_category"]
tag1["random_access_iterator_tag"]
tag2["bidirectional_iterator_tag"]
impl1["advance_impl(it, n, random_access_iterator_tag)\nit += n (O(1))"]
impl2["advance_impl(it, n, bidirectional_iterator_tag)\nwhile (n--) ++it (O(n))"]
start --> traits
traits --> tag1
traits --> tag2
tag1 --> impl1
tag2 --> impl2
목차
- 기본 구조
- STL Iterator 예제
- Type Traits와 결합
- SFINAE vs Tag Dispatch
- 자주 발생하는 문제와 해결법
- 프로덕션 패턴
- 완전한 예제: 컨테이너 삽입
1. 기본 구조
최소 Tag Dispatch
#include <iostream>
// 1. 태그 정의
struct fast_tag {};
struct slow_tag {};
// 2. 태그별 구현
void process_impl(int value, fast_tag) {
std::cout << "Fast processing: " << value * 2 << '\n';
}
void process_impl(int value, slow_tag) {
std::cout << "Slow processing: " << value << '\n';
}
// 3. 인터페이스 (태그 선택)
template<typename T>
void process(T value) {
// 조건에 따라 태그 선택
if constexpr (sizeof(T) <= 4) {
process_impl(value, fast_tag{});
} else {
process_impl(value, slow_tag{});
}
}
int main() {
process(10); // "Fast processing: 20"
process(10L); // "Slow processing: 10" (long은 8바이트)
}
핵심: 빈 구조체 fast_tag{}를 함수 인자로 전달해 오버로딩 해석으로 함수를 선택합니다.
2. STL Iterator 예제
std::distance 구현
#include <iterator>
#include <iostream>
#include <vector>
#include <list>
// Random Access: O(1)
template<typename Iter>
typename std::iterator_traits<Iter>::difference_type
distance_impl(Iter first, Iter last, std::random_access_iterator_tag) {
std::cout << "Fast distance: last - first\n";
return last - first;
}
// Input: O(n)
template<typename Iter>
typename std::iterator_traits<Iter>::difference_type
distance_impl(Iter first, Iter last, std::input_iterator_tag) {
std::cout << "Slow distance: counting\n";
typename std::iterator_traits<Iter>::difference_type n = 0;
while (first != last) {
++first;
++n;
}
return n;
}
// 인터페이스
template<typename Iter>
typename std::iterator_traits<Iter>::difference_type
my_distance(Iter first, Iter last) {
return distance_impl(first, last,
typename std::iterator_traits<Iter>::iterator_category{});
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << my_distance(vec.begin(), vec.end()) << '\n'; // Fast, 5
std::list<int> lst = {1, 2, 3, 4, 5};
std::cout << my_distance(lst.begin(), lst.end()) << '\n'; // Slow, 5
}
3. Type Traits와 결합
정수 타입별 처리
#include <iostream>
#include <type_traits>
// 태그
struct signed_tag {};
struct unsigned_tag {};
// 구현
template<typename T>
void print_impl(T value, signed_tag) {
std::cout << "Signed: " << value << " (can be negative)\n";
}
template<typename T>
void print_impl(T value, unsigned_tag) {
std::cout << "Unsigned: " << value << " (always positive)\n";
}
// 인터페이스
template<typename T>
void print_number(T value) {
using tag = std::conditional_t<
std::is_signed_v<T>,
signed_tag,
unsigned_tag
>;
print_impl(value, tag{});
}
int main() {
print_number(-10); // "Signed: -10 (can be negative)"
print_number(10u); // "Unsigned: 10 (always positive)"
}
4. SFINAE vs Tag Dispatch
SFINAE 방식
#include <type_traits>
#include <iostream>
// SFINAE: enable_if로 함수 활성화/비활성화
template<typename T>
std::enable_if_t<std::is_integral_v<T>>
process(T value) {
std::cout << "Integral: " << value << '\n';
}
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>>
process(T value) {
std::cout << "Floating: " << value << '\n';
}
단점: 복잡한 조건에서 enable_if 중첩이 어렵고, 에러 메시지가 불친절합니다.
Tag Dispatch 방식
#include <type_traits>
#include <iostream>
// 태그
struct integral_tag {};
struct floating_tag {};
// 구현
template<typename T>
void process_impl(T value, integral_tag) {
std::cout << "Integral: " << value << '\n';
}
template<typename T>
void process_impl(T value, floating_tag) {
std::cout << "Floating: " << value << '\n';
}
// 인터페이스
template<typename T>
void process(T value) {
using tag = std::conditional_t<
std::is_integral_v<T>,
integral_tag,
floating_tag
>;
process_impl(value, tag{});
}
장점: 조건 로직이 인터페이스에 집중되고, 구현은 단순 오버로딩으로 깔끔합니다.
5. 자주 발생하는 문제와 해결법
문제 1: 태그 계층 구조 무시
증상: 잘못된 오버로딩 선택.
원인: Iterator 태그는 계층 구조를 가집니다 (random_access_iterator_tag는 bidirectional_iterator_tag를 상속).
// ❌ 잘못된 사용: 계층 구조 무시
template<typename Iter>
void func_impl(Iter it, std::input_iterator_tag) {
// Input만 처리
}
// ✅ 올바른 사용: 계층 구조 고려
template<typename Iter>
void func_impl(Iter it, std::random_access_iterator_tag) {
// Random Access 최적화
}
template<typename Iter>
void func_impl(Iter it, std::bidirectional_iterator_tag) {
// Bidirectional 최적화
}
template<typename Iter>
void func_impl(Iter it, std::input_iterator_tag) {
// Input 기본 구현
}
문제 2: 태그 객체 생성 비용
증상: 불필요한 생성자 호출.
원인: 태그는 빈 구조체지만, 함수 인자로 전달 시 생성됩니다.
// ❌ 잘못된 사용: 태그 객체 생성
void func_impl(int value, fast_tag tag) { // 복사
// ...
}
// ✅ 올바른 사용: 태그는 값으로 전달 (최적화됨)
void func_impl(int value, fast_tag) { // 이름 없는 인자
// 컴파일러가 최적화
}
문제 3: 잘못된 태그 선택
증상: 의도와 다른 함수 호출.
원인: std::conditional_t 조건 실수.
// ❌ 잘못된 사용: 조건 반대
template<typename T>
void process(T value) {
using tag = std::conditional_t<
std::is_integral_v<T>,
floating_tag, // 반대!
integral_tag
>;
process_impl(value, tag{});
}
// ✅ 올바른 사용: 조건 확인
template<typename T>
void process(T value) {
using tag = std::conditional_t<
std::is_integral_v<T>,
integral_tag,
floating_tag
>;
process_impl(value, tag{});
}
6. 프로덕션 패턴
패턴 1: 다단계 태그
#include <type_traits>
#include <iostream>
// 태그 계층
struct base_tag {};
struct derived1_tag : base_tag {};
struct derived2_tag : base_tag {};
// 구현
void process_impl(int value, derived1_tag) {
std::cout << "Derived1: " << value << '\n';
}
void process_impl(int value, derived2_tag) {
std::cout << "Derived2: " << value << '\n';
}
void process_impl(int value, base_tag) {
std::cout << "Base: " << value << '\n';
}
// 인터페이스
template<typename T>
void process(T value) {
if constexpr (sizeof(T) == 1) {
process_impl(value, derived1_tag{});
} else if constexpr (sizeof(T) == 4) {
process_impl(value, derived2_tag{});
} else {
process_impl(value, base_tag{});
}
}
패턴 2: 태그 + Concept (C++20)
#include <concepts>
#include <iostream>
// 태그
struct arithmetic_tag {};
struct other_tag {};
// Concept
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
// 구현
void process_impl(Arithmetic auto value, arithmetic_tag) {
std::cout << "Arithmetic: " << value << '\n';
}
void process_impl(auto value, other_tag) {
std::cout << "Other\n";
}
// 인터페이스
template<typename T>
void process(T value) {
if constexpr (Arithmetic<T>) {
process_impl(value, arithmetic_tag{});
} else {
process_impl(value, other_tag{});
}
}
7. 완전한 예제: 컨테이너 삽입
#include <iostream>
#include <vector>
#include <list>
#include <iterator>
// 태그
struct random_access_tag {};
struct other_tag {};
// Random Access: reserve 가능
template<typename Container, typename Iter>
void insert_impl(Container& cont, Iter first, Iter last, random_access_tag) {
std::cout << "Optimized: reserve + insert\n";
auto dist = std::distance(first, last);
cont.reserve(cont.size() + dist);
cont.insert(cont.end(), first, last);
}
// Other: reserve 불가
template<typename Container, typename Iter>
void insert_impl(Container& cont, Iter first, Iter last, other_tag) {
std::cout << "Normal: insert\n";
cont.insert(cont.end(), first, last);
}
// 인터페이스
template<typename Container, typename Iter>
void my_insert(Container& cont, Iter first, Iter last) {
using category = typename std::iterator_traits<Iter>::iterator_category;
using tag = std::conditional_t<
std::is_base_of_v<std::random_access_iterator_tag, category>,
random_access_tag,
other_tag
>;
insert_impl(cont, first, last, tag{});
}
int main() {
std::vector<int> vec;
std::vector<int> src1 = {1, 2, 3, 4, 5};
my_insert(vec, src1.begin(), src1.end()); // "Optimized: reserve + insert"
std::list<int> src2 = {6, 7, 8};
my_insert(vec, src2.begin(), src2.end()); // "Normal: insert"
for (int x : vec) {
std::cout << x << ' ';
}
std::cout << '\n'; // 1 2 3 4 5 6 7 8
}
정리
| 개념 | 설명 |
|---|---|
| Tag Dispatch | 빈 구조체(태그)로 함수 오버로딩 선택 |
| 목적 | 컴파일 타임 함수 선택, 최적화 |
| 장점 | SFINAE보다 간결, 에러 메시지 친절 |
| 단점 | 태그 정의 필요, 런타임 오버헤드 없음 |
| 사용 사례 | STL iterator, type traits, 조건부 최적화 |
Tag Dispatch는 STL에서 iterator별 최적화에 널리 사용되는 강력한 메타프로그래밍 패턴입니다.
FAQ
Q1: Tag Dispatch는 언제 쓰나요?
A: 컴파일 타임에 타입 정보로 함수를 선택하고, SFINAE보다 간결하게 구현하고 싶을 때 사용합니다.
Q2: SFINAE와 차이는?
A: SFINAE는 함수 시그니처에 조건을 넣어 함수를 활성화/비활성화하고, Tag Dispatch는 인터페이스에서 태그를 선택해 오버로딩으로 함수를 선택합니다. Tag Dispatch가 더 깔끔합니다.
Q3: 런타임 오버헤드는?
A: 없습니다. 태그는 빈 구조체라 컴파일러가 최적화로 제거합니다.
Q4: C++20 Concept과 비교는?
A: Concept은 템플릿 제약을 명시적으로 표현하고, Tag Dispatch는 함수 선택에 집중합니다. 함께 사용 가능합니다.
Q5: Iterator 태그 계층은?
A: input_iterator_tag ← forward_iterator_tag ← bidirectional_iterator_tag ← random_access_iterator_tag ← contiguous_iterator_tag (C++20).
Q6: Tag Dispatch 학습 리소스는?
A:
- “C++ Templates: The Complete Guide” by Vandevoorde & Josuttis
- cppreference: Tag Dispatch
- “Effective STL” by Scott Meyers
한 줄 요약: Tag Dispatch로 컴파일 타임에 최적 함수를 선택할 수 있습니다. 다음으로 Expression Template을 읽어보면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ SFINAE | “Substitution Failure Is Not An Error” 가이드
- C++ Type Traits | “타입 특성” 완벽 가이드
- C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
관련 글
- C++ Tag Dispatching |
- C++ enable_if |
- C++ 반복자 무효화 에러 |
- C++ 반복자 |
- C++ SFINAE |