C++ 템플릿 템플릿 인자 | template template parameter 가이드
이 글의 핵심
어떤 컨테이너로 데이터를 저장할지 사용자가 고르게 하려면 템플릿이 다른 템플릿을 인자로 받아야 합니다. 이 글은 std::vector를 넘겼는데 컴파일이 안 되는 흔한 이유인 기본 템플릿 인자 개수 차이를 짚고, 스마트 포인터 정책과 정책 기반 설계처럼 실무에서 이 문법이 쓰이는 경우를 예제로 보여줍니다.
템플릿 템플릿 인자란?
템플릿 템플릿 인자(template template parameter) 는 템플릿이 다른 템플릿을 인자로 받게 하는 문법입니다. vector<int>처럼 이미 구체화된 타입이 아니라, “어떤 컨테이너 템플릿”을 받아서 그 템플릿에 요소 타입만 넘기고 싶을 때 사용합니다. 템플릿 기초와 템플릿 특수화를 먼저 익히면 이해하기 쉽습니다.
기본 예시
#include <vector>
#include <list>
#include <iostream>
template<template<typename> class Container, typename T>
struct Wrapper {
Container<T> data;
void add(T value) {
data.push_back(value);
}
void print() const {
for (const auto& item : data) {
std::cout << item << " ";
}
std::cout << '\n';
}
};
template<typename T>
using Vec = std::vector<T>;
int main() {
Wrapper<Vec, int> w; // Wrapper 내부는 std::vector<int>
w.add(1);
w.add(2);
w.add(3);
w.print(); // 1 2 3
return 0;
}
동작 원리: Container는 “한 개의 타입 인자를 받는 템플릿”이고, Wrapper는 그 템플릿과 요소 타입 T를 받아 Container<T>를 멤버로 가집니다. 이렇게 하면 Wrapper를 vector, list, deque 등 다양한 컨테이너와 함께 사용할 수 있습니다.
여기서 Vec 별칭을 따로 만든 이유는 뒤에서 설명할 인자 개수 문제 때문입니다. 이 문법이 필요한 상황을 먼저 생각해 보면, “Wrapper<std::vector<int>>로 받으면 되지 않나?”라는 의문이 듭니다. 구체 타입 하나만 저장한다면 그게 맞고, 더 단순합니다. 템플릿 템플릿 인자가 필요한 경우는 클래스 안에서 같은 컨테이너 종류를 다른 요소 타입으로 여러 번 만들어야 할 때입니다. 예를 들어 Graph<Container>가 내부에서 Container<Node>와 Container<Edge>를 함께 만들어야 한다면, 사용자에게 두 타입을 따로 받는 것보다 “어떤 컨테이너 템플릿을 쓸지” 하나만 받는 편이 실수가 없습니다.
CTAD와의 차이: 클래스 템플릿 인자 추론(CTAD)은 “생성자 인자로부터 템플릿 인자 추론”이고, 템플릿 템플릿 인자는 “템플릿 자체를 인자로 받는 것”입니다. 역할이 다릅니다.
C++17: typename으로도 선언
C++17부터는 class 대신 typename을 쓸 수 있어, 템플릿 인자라는 것이 더 분명해집니다.
template<template<typename> typename Container, typename T>
void use(Container<T>& c) { }
표준 컨테이너에 적용할 때 주의
std::vector, std::list 등은 두 번째 기본 템플릿 인자(할당자)를 갖습니다. 템플릿 템플릿 인자로 그대로 넘기려면 인자 개수를 맞춰야 합니다.
// std::vector의 실제 정의
// template<typename T, typename Alloc = std::allocator<T>> class vector;
// ❌ 인자 개수 불일치
// template<template<typename> class Container, typename T>
// struct Adapter {
// Container<T> data; // 에러: vector는 2개 인자 필요
// };
// ✅ 해결 1: 인자 개수 맞추기
template<template<typename, typename> class Container, typename T>
struct Adapter {
Container<T, std::allocator<T>> data;
};
Adapter<std::vector, int> a; // OK
// ✅ 해결 2: using으로 래핑 (더 간결)
template<typename T>
using Vec = std::vector<T>;
template<template<typename> class Container, typename T>
struct SimpleAdapter {
Container<T> data;
};
SimpleAdapter<Vec, int> b; // OK
왜 이런 문제가?: C++14까지 템플릿 템플릿 인자는 매개변수 목록이 정확히 일치해야 했습니다. std::vector는 2개 인자(타입, 할당자)를 받지만 template<typename>은 1개만 받으므로, 두 번째 인자에 기본값이 있어도 불일치로 판정됩니다. GCC 10에서 -std=c++14로 컴파일하면 다음과 같은 에러가 납니다.
error: type/value mismatch at argument 1 in template parameter list for 'template<template<class> class C, class T> struct Bad'
note: expected a template of type 'template<class> class C', got 'template<class _Tp, class _Alloc> class std::vector'
C++17에서는 제안서 P0522R0이 채택되어 “기본 인자를 채우면 호환되는 템플릿”도 받아들이도록 규칙이 완화됐습니다. 그래서 같은 코드가 GCC -std=c++17에서는 에러 없이 컴파일됩니다. 문제는 컴파일러마다 적용 시점이 달랐다는 것입니다. Clang은 이 완화 규칙을 오랫동안 -frelaxed-template-template-args 옵션 뒤에 두었다가 19 버전에서야 기본값으로 켰습니다. 그래서 “내 PC의 GCC에서는 되는데 CI의 Clang에서만 깨진다”는 상황이 실제로 자주 생겼습니다. 여러 컴파일러를 지원해야 하는 라이브러리라면 아래 두 방법 중 하나로 명시적으로 맞춰 두는 것이 안전합니다.
실무 패턴:
- using 별칭: 표준 컨테이너를 한 인자 템플릿으로 래핑
- 가변 인자:
template<typename...>로 개수에 상관없이 받기 (C++11부터 가능)
// 가변 인자로 유연하게 (C++11부터)
template<template<typename...> class Container, typename T>
struct FlexibleAdapter {
Container<T> data; // vector, list, set 모두 OK
};
FlexibleAdapter<std::vector, int> v;
FlexibleAdapter<std::list, int> l;
FlexibleAdapter<std::set, int> s;
가변 인자 방식은 받는 쪽은 유연하지만, 사용하는 쪽에서 Container<T>처럼 첫 인자만 채우고 나머지는 기본값에 맡긴다는 가정을 깔고 있습니다. std::map처럼 두 번째 인자에 기본값이 없는 템플릿을 넘기면 Container<T>를 만드는 순간 에러가 납니다. 또 std::array<T, N>처럼 값(비타입) 매개변수를 가진 템플릿은 typename...에 맞지 않아 아예 넘길 수 없습니다. 별칭 방식은 사용자가 별칭을 하나 더 만들어야 하는 수고가 있지만 무엇을 기대하는지가 코드에 드러나고, 커스텀 할당자처럼 나머지 인자를 원하는 값으로 고정할 수도 있다는 장점이 있습니다.
실전 예시 1: 컨테이너 정책 주입
여러 컨테이너를 같은 인터페이스로 쓰는 유틸을 만들 때, 템플릿 템플릿 인자로 “컨테이너 템플릿”을 받을 수 있습니다.
#include <vector>
#include <list>
#include <iostream>
template<typename T>
using Vec = std::vector<T>;
template<typename T>
using List = std::list<T>;
template<template<typename> class Container, typename T>
class Repository {
Container<T> items;
public:
void add(T x) { items.push_back(x); }
size_t size() const { return items.size(); }
void print() const {
for (const auto& item : items) {
std::cout << item << " ";
}
std::cout << '\n';
}
};
int main() {
// vector 기반
Repository<Vec, int> repo_vec;
repo_vec.add(1);
repo_vec.add(2);
repo_vec.print(); // 1 2
// list 기반 (list는 push_back 있음)
Repository<List, int> repo_list;
repo_list.add(10);
repo_list.add(20);
repo_list.print(); // 10 20
return 0;
}
실무 활용: 데이터 저장소의 내부 구현을 유연하게 바꿀 수 있습니다. 성능 테스트 시 vector vs list를 쉽게 비교할 수 있습니다.
주의할 점은 Repository가 컨테이너에 요구하는 인터페이스(push_back, size, 범위 기반 for)가 어디에도 선언돼 있지 않다는 것입니다. std::set을 넘기면 add()를 호출하는 순간에야 'class std::set<int>' has no member named 'push_back' 에러가 나고, 템플릿 인스턴스화 경로가 줄줄이 붙은 긴 메시지가 출력됩니다. 게다가 템플릿 멤버 함수는 호출될 때만 인스턴스화되므로, add()를 쓰지 않는 코드에서는 에러가 드러나지도 않습니다. C++20이라면 requires 절로 “push_back이 있는 컨테이너”를 명시해 두면 잘못된 컨테이너를 넘긴 줄에서 짧은 에러를 받을 수 있습니다.
실전 예시 2: 스마트 포인터 정책
#include <memory>
#include <iostream>
template<template<typename> class Ptr, typename T>
class ResourceManager {
Ptr<T> resource;
public:
ResourceManager(T* raw) : resource(raw) {}
T* get() { return resource.get(); }
void reset(T* raw = nullptr) { resource.reset(raw); }
};
// unique_ptr 기반 (독점 소유)
ResourceManager<std::unique_ptr, int> mgr1(new int(42));
// shared_ptr 기반 (공유 소유)
ResourceManager<std::shared_ptr, int> mgr2(new int(100));
실무 의미: 소유권 정책을 템플릿 인자로 받아, 같은 코드로 독점/공유 소유를 선택할 수 있습니다.
이 예제도 앞의 인자 개수 문제를 그대로 안고 있습니다. std::unique_ptr는 template<class T, class Deleter = std::default_delete<T>>로 매개변수가 2개이므로, template<typename> class Ptr에 넘기면 C++14에서는 std::vector와 같은 “type/value mismatch” 에러가 납니다(C++17 GCC에서는 컴파일됩니다). std::shared_ptr는 매개변수가 하나라 어느 쪽에서도 문제가 없습니다. 또 두 포인터는 복사 의미가 다르므로 ResourceManager<std::unique_ptr, int>는 복사할 수 없고 shared_ptr 버전은 복사할 수 있습니다. “같은 코드”라도 정책에 따라 클래스의 성질이 바뀐다는 점을 사용자에게 알려야 합니다. 실무에서는 이런 추상화보다 필요한 소유권을 직접 쓰는 편이 읽기 쉬운 경우가 많으므로, 정책을 바꿔 끼울 실제 요구가 있을 때만 도입하는 것이 좋습니다.
실전 예시 3: 표준 라이브러리 패턴 - Allocator
#include <memory>
#include <vector>
template<typename T, template<typename> class Allocator = std::allocator>
class CustomVector {
std::vector<T, Allocator<T>> data_;
public:
void push_back(const T& value) {
data_.push_back(value);
}
size_t size() const { return data_.size(); }
};
// 기본 allocator 사용
CustomVector<int> v1;
// 커스텀 allocator 사용
template<typename T>
using CustomAlloc = std::allocator<T>; // 실제로는 커스텀 구현
CustomVector<int, CustomAlloc> v2;
실무 활용: 메모리 풀, 커스텀 할당자를 주입하여 성능을 최적화할 수 있습니다.
관련 개념과 함께 보기
- C++ 가변 인자 템플릿: 파라미터 팩 재귀 전개, Fold Expression, 완벽한 전달: 인자 개수가 가변일 때 패킹·확장
- 템플릿 인자 추론: 함수 템플릿에서 타입 생략
자주 발생하는 문제
문제 1: 표준 컨테이너 인자 개수 불일치
증상: GCC는 error: type/value mismatch at argument 1 in template parameter list, Clang은 template template argument has different template parameters than its corresponding template template parameter (C++14 이하, 또는 완화 규칙이 꺼진 컴파일러)
// ❌ 에러
template<template<typename> class Container, typename T>
struct Bad {
Container<T> data; // vector는 2개 인자 필요
};
// Bad<std::vector, int> b; // 컴파일 에러
해결:
// ✅ 해결 1: using 래퍼
template<typename T> using Vec = std::vector<T>;
Bad<Vec, int> b1;
// ✅ 해결 2: 인자 개수 맞추기
template<template<typename, typename> class Container, typename T>
struct Good {
Container<T, std::allocator<T>> data;
};
Good<std::vector, int> b2;
// ✅ 해결 3: 가변 인자 (C++11부터)
template<template<typename...> class Container, typename T>
struct Flexible {
Container<T> data;
};
Flexible<std::vector, int> b3;
문제 2: 구체화된 타입 전달
증상: GCC는 type/value mismatch ... expected a class template, got 'std::vector<int>' 형태의 에러를 냅니다.
// ❌ 구체화된 타입 전달
// Wrapper<std::vector<int>, int> w; // 에러
// ✅ 템플릿 이름만 전달
Wrapper<Vec, int> w;
핵심: 템플릿 템플릿 인자는 템플릿 자체(예: std::vector)를 받지, 구체화된 타입(예: std::vector<int>)을 받지 않습니다.
문제 3: 기본 인자와 호환성
// std::map은 4개 인자
// template<typename Key, typename T, typename Compare = std::less<Key>,
// typename Alloc = std::allocator<std::pair<const Key, T>>> class map;
// using으로 래핑 필요
template<typename K, typename V>
using Map = std::map<K, V>;
template<template<typename, typename> class Container, typename K, typename V>
struct MapWrapper {
Container<K, V> data;
};
MapWrapper<Map, int, std::string> mw;
실무 패턴
패턴 1: 컨테이너 추상화 레이어
// 컨테이너 인터페이스 통일
template<typename T> using Vec = std::vector<T>;
template<typename T> using Deque = std::deque<T>;
template<template<typename> class Container, typename T>
class DataStore {
Container<T> storage_;
public:
void store(T value) { storage_.push_back(value); }
T retrieve(size_t index) { return storage_[index]; }
size_t count() const { return storage_.size(); }
};
// 성능 테스트: vector vs deque
using VecStore = DataStore<Vec, int>;
using DequeStore = DataStore<Deque, int>;
retrieve가 operator[]를 쓰므로 이 저장소는 임의 접근이 되는 컨테이너(vector, deque)에만 맞습니다. std::list를 넘기면 retrieve를 호출하는 순간 컴파일 에러가 납니다. 이렇게 “템플릿 인자로 받는 것”과 “그 템플릿에 요구하는 연산”이 분리되어 있다는 점이 템플릿 템플릿 인자 설계에서 가장 흔한 함정이며, 요구 사항을 주석이나 concept으로 명시해 두는 습관이 필요합니다.
패턴 2: 정책 기반 설계 (Policy-Based Design)
template<typename T, template<typename> class StoragePolicy>
class Cache {
StoragePolicy<T> storage_;
public:
void put(const T& item) { storage_.add(item); }
T get() { return storage_.retrieve(); }
};
// 다양한 저장 정책
template<typename T> class VectorStorage { /* ... */ };
template<typename T> class ListStorage { /* ... */ };
template<typename T> class SetStorage { /* ... */ };
Cache<int, VectorStorage> cache1; // vector 기반
Cache<int, SetStorage> cache2; // set 기반 (중복 제거)
정책 기반 설계는 Andrei Alexandrescu의 Modern C++ Design으로 널리 알려진 기법으로, 가상 함수 없이 컴파일 시간에 동작을 조합한다는 장점이 있습니다. 호출이 인라인될 수 있어 런타임 비용이 없지만, 대가도 분명합니다. Cache<int, VectorStorage>와 Cache<int, SetStorage>는 서로 전혀 다른 타입이라 같은 std::vector에 담거나 같은 함수 매개변수로 받을 수 없고, 정책 조합마다 코드가 따로 생성되어 바이너리가 커집니다. 저장 방식을 런타임 설정으로 바꿔야 하거나 여러 구현을 한 컬렉션에서 다뤄야 한다면, 가상 함수 인터페이스가 더 맞는 도구입니다.
FAQ
Q1: 템플릿 템플릿 인자는 언제 사용하나요?
A: 컨테이너, 스마트 포인터, 할당자 등 “템플릿 자체”를 정책으로 주입하고 싶을 때 사용합니다.
Q2: std::vector를 직접 넘길 수 없나요?
A: C++14까지는 std::vector가 2개 인자를 받으므로 template<typename> class 자리에 넘길 수 없습니다. C++17의 완화된 매칭 규칙을 지원하는 컴파일러(GCC 7 이후 -std=c++17, Clang 19 이후)에서는 직접 넘길 수 있지만, 이식성이 필요하면 using 별칭이나 template<typename...>로 받는 편이 안전합니다.
Q3: class vs typename 키워드?
A: C++17 이전에는 class만 가능했으며, C++17부터 typename도 사용할 수 있습니다. 의미는 동일합니다.
Q4: 실무에서 자주 사용하나요?
A: 라이브러리 설계, 정책 기반 설계, 제네릭 컨테이너 래퍼에서 사용합니다. 일반 애플리케이션 코드에서는 드물지만, 재사용 가능한 컴포넌트를 만들 때 유용합니다.
Q5: 디버깅 팁은?
A:
- 에러 메시지에서 “template template parameter” 키워드 확인
- 인자 개수 불일치는 using 래퍼로 해결
static_assert로 컨테이너 요구사항 검증 매칭 규칙의 정확한 정의는 cppreference의 Template parameters 문서에서 확인할 수 있습니다.
관련 글: 템플릿 기초, 템플릿 특수화, 가변 인자 템플릿, CTAD. 템플릿 템플릿 인자로 컨테이너·정책 클래스를 템플릿 단위로 받아 유연한 설계를 만들 수 있으며, 표준 컨테이너는 using 래퍼로 감싸 사용하면 됩니다.
같이 보면 좋은 글
- C++ 템플릿 기초
- C++ 템플릿 특수화: 모든 타입에 같은 로직을 쓸 수 없을 때 완전·부분 특수화로 푸는 법
- C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression
- C++17 CTAD: 클래스 템플릿 인자 추론 원리와 추론 가이드가 필요한 경우
- C++ 템플릿 인자 추론 | template argument deduction 가이드
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
- C++20 Concepts
- C++ constexpr if
- C++ 컨테이너 어댑터