C++ 템플릿 에러 메시지 읽는 법: 첫 줄·note·candidate 순서와 자주 나오는 패턴
이 글의 핵심
std::sort에 비교할 수 없는 타입을 넘기는 것 같은 작은 실수도 템플릿 인스턴스화 스택 전체가 출력되어 에러가 수백 줄로 불어납니다. 중간 스택을 무시해도 되는 이유와 Clang을 보조 컴파일러로 쓰면 도움이 되는 점을 설명하고, C++20 Concepts로 에러 메시지를 짧고 읽기 쉽게 만드는 방법까지 다룹니다.
들어가며: “error: 템플릿 에러 300줄… 뭐가 문제죠?”
C++ 템플릿을 쓰다 보면 수백 줄의 에러 메시지를 마주하게 됩니다. 특히 STL 알고리즘이나 컨테이너에 맞지 않는 타입을 넣으면, 오류가 내 코드가 아니라 표준 라이브러리 헤더 깊은 곳에서 발생하고 컴파일러는 그 지점까지의 템플릿 인스턴스화 경로 전체를 출력합니다. 메시지가 긴 것은 컴파일러가 불친절해서가 아니라, 템플릿이 “사용하는 순간에” 코드를 생성하는 방식이라 오류가 난 곳과 원인이 있는 곳이 멀리 떨어져 있기 때문입니다.
가장 흔한 예는 비교 연산자가 없는 타입을 정렬하는 것입니다.
struct Point { int x, y; };
std::vector<Point> pts = {{3, 1}, {1, 2}};
std::sort(pts.begin(), pts.end()); // ❌ Point에는 operator<가 없음
// GCC 출력(발췌):
// In instantiation of 'constexpr bool __gnu_cxx::__ops::_Iter_less_iter::operator()(...)
// [with _Iterator1 = __gnu_cxx::__normal_iterator<Point*, std::vector<Point> > ...]':
// ... required from 'void std::__insertion_sort(...)'
// ... (중략: std::__sort, std::__introsort_loop ...)
// main.cpp:5:14: required from here
// error: no match for 'operator<' (operand types are 'Point' and 'Point')
실제 원인은 마지막의 no match for 'operator<' 한 줄이고, 내가 고칠 곳은 required from here가 가리키는 main.cpp:5입니다. 그 사이의 줄들은 std::sort가 내부적으로 어떤 함수를 거쳐 비교에 도달했는지를 보여 줄 뿐입니다. 이 글은 이런 메시지의 구조, 첫 줄·because·required from에서 핵심을 찾는 순서, 자주 나오는 에러 패턴 10가지, GCC·Clang·MSVC의 메시지 차이, 그리고 Concepts로 메시지를 짧게 만드는 방법을 다룹니다.
템플릿 에러 메시지 구조
전형적인 템플릿 에러 구조
Clang 형식으로 보면 다음과 같이 네 부분으로 나눌 수 있습니다.
[1] /usr/include/c++/13/bits/predefined_ops.h:45:23: error: invalid operands to binary expression ('Point' and 'Point')
{ return *__it1 < *__it2; }
~~~~~~ ^ ~~~~~~
[2] /usr/include/c++/13/bits/stl_algo.h:1812:14: note: in instantiation of function template specialization
'__gnu_cxx::__ops::_Iter_less_iter::operator()<...>' requested here
... (표준 라이브러리 내부 경로 여러 줄) ...
[3] main.cpp:5:10: note: in instantiation of function template specialization
'std::sort<__gnu_cxx::__normal_iterator<Point *, std::vector<Point>>>' requested here
std::sort(pts.begin(), pts.end());
^
[4] /usr/include/c++/13/bits/stl_pair.h:...: note: candidate template ignored: could not match 'pair<_T1, _T2>' against 'Point'
... (operator< 후보 목록 수십 줄) ...
구조 분석:
- [1] 실제 에러: 무엇이 실패했는지(여기서는
Point < Point불가). 위치는 라이브러리 헤더일 수 있습니다. - [2] 인스턴스화 경로: 라이브러리 내부에서 어떤 템플릿을 거쳐 [1]에 도달했는지
- [3] 내 코드의 호출 지점: 경로 중 내 소스 파일이 처음 나오는 줄. 고칠 곳은 대개 여기입니다.
- [4] 후보 목록: 컴파일러가 시도했지만 맞지 않은
operator<오버로드들. 대부분 무관합니다.
주의사항: 중간의 bits/stl_*.h 줄들은 “왜 실패했는지”가 아니라 “어떤 템플릿이 전개되었는지”라서, 첫 error 줄과 내 파일이 나오는 requested here/required from here 줄만 먼저 읽으면 됩니다.
핵심: 중간 경로는 대부분 무시해도 됩니다
중요한 정보:
- 첫
error:줄: 무엇이 실패했는지 - 내 파일이 나오는
required from here/requested here: 어디를 고쳐야 하는지 - “note: because” 부분(concept·제약 실패 시): 어떤 조건이 만족되지 않았는지
- “candidate” 목록(오버로드 실패 시): 각 오버로드가 왜 실패했는지
무시해도 되는 부분:
- 라이브러리 내부의 “required from” 반복
operator<,operator<<처럼 흔한 연산자에 대해 나열되는 수십 개의 무관한 후보
GCC와 Clang은 출력 순서가 반대라는 점을 알아 두면 헷갈리지 않습니다. GCC는 In instantiation of ...로 경로를 먼저 보여 주고 마지막에 error:를 출력하는 반면, Clang은 error:를 먼저 출력하고 경로를 note:로 붙입니다. 그래서 GCC 출력에서는 에러 블록의 아래쪽, Clang 출력에서는 위쪽부터 보는 것이 빠릅니다.
에러 메시지 읽는 순서
1단계: 첫 error 줄 확인
main.cpp:10:5: error: no matching function for call to 'std::sort'
^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
에러 타입 무엇이 문제인지
핵심 정보:
- 파일:줄:열: 에러가 발생한 위치(내 코드일 수도, 라이브러리 헤더일 수도 있음)
- 에러 종류:
no matching function,ambiguous,no match for 'operator<',static assertion failed등 - 관련 이름: 어떤 함수·연산자·타입이 문제인지
2단계: “note: because” 찾기
concept이나 requires로 제약된 템플릿에서는 실패한 조건이 because로 표시됩니다.
note: because 'double' does not satisfy 'integral'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
실제 원인
이 줄이 가장 중요합니다. 어떤 타입이 어떤 조건을 만족하지 못했는지를 직접 알려줍니다. 제약이 없는 옛날 스타일 템플릿에는 이 줄이 없고, 대신 템플릿 본문 안에서 실패한 식이 error:로 나옵니다.
3단계: candidate 목록 확인
note: candidate template ignored: requirement 'std::is_integral_v<double>' was not satisfied
note: candidate function not viable: requires 3 arguments, but 2 were provided
note: candidate function not viable: 'this' argument has type 'const MyClass', but method is not marked const
각 후보가 왜 실패했는지 나와 있습니다. 가장 가까운 후보를 찾아 수정하면 됩니다. 후보가 수십 개일 때는 내가 호출하려던 함수의 이름과 매개변수 개수가 맞는 후보만 골라 읽으면 됩니다.
4단계: 중간 스택 건너뛰기
note: in instantiation of function template specialization 'std::sort<...>' requested here
note: required from 'void std::__sort(...)'
note: required from 'void std::__introsort_loop(...)'
... (반복) ...
이 부분은 건너뛰세요. 템플릿이 어떻게 인스턴스화되었는지 보여 주지만, 원인 자체를 담고 있지는 않습니다. 예외는 내가 만든 템플릿이 여러 겹 중첩되어 있을 때입니다. 그때는 경로 중 내 파일에 속한 줄들을 따라가면 어느 단계에서 타입이 예상과 달라졌는지 보입니다.
자주 나오는 에러 패턴 10가지
no match for ‘operator<’ (정렬·검색)
가장 흔한 에러: 알고리즘이 요구하는 연산이 타입에 없습니다.
// ❌ 에러 코드
struct Point { int x, y; };
std::vector<Point> pts;
std::sort(pts.begin(), pts.end());
// GCC: error: no match for 'operator<' (operand types are 'Point' and 'Point')
// Clang: error: invalid operands to binary expression ('Point' and 'Point')
원인: 비교자 없이 호출한 std::sort는 원소를 <로 비교하는데, Point에는 operator<가 없습니다. std::set<Point>, std::map<Point, V>, std::lower_bound도 같은 에러를 냅니다. 반대로 std::unordered_map<Point, V>에서는 std::hash<Point>가 없다는 에러(call to implicitly-deleted default constructor of 'std::hash<Point>')로 나타납니다.
해결법:
// ✅ 해결 1: 비교자 전달
std::sort(pts.begin(), pts.end(),
[](const Point& a, const Point& b) {
return std::tie(a.x, a.y) < std::tie(b.x, b.y);
});
// ✅ 해결 2: C++20 기본 비교 연산자
struct Point {
int x, y;
auto operator<=>(const Point&) const = default;
};
이 사례에서 흔히 하는 오해가 하나 있습니다. std::vector<std::unique_ptr<int>>는 std::sort로 정렬됩니다. std::sort는 원소를 복사하지 않고 이동하며, unique_ptr에는 operator<가 있기 때문입니다. 다만 그 <는 가리키는 값이 아니라 포인터 주소를 비교하므로, 컴파일은 되지만 의도와 다른 순서가 나옵니다. 값으로 정렬하려면 [](const auto& a, const auto& b) { return *a < *b; } 비교자가 필요합니다. 에러가 나지 않는 버그가 에러가 나는 버그보다 더 위험한 예입니다.
call of overloaded function is ambiguous
에러: 여러 오버로드가 똑같이 좋은 후보입니다.
// ❌ 에러 코드
void print(int x) { std::cout << "int: " << x << '\n'; }
void print(double x) { std::cout << "double: " << x << '\n'; }
print(42L); // long → int? long → double?
// GCC: error: call of overloaded 'print(long int)' is ambiguous
// Clang: error: call to 'print' is ambiguous
원인: long에서 int로 가는 것도, double로 가는 것도 모두 표준 변환(conversion)이라 순위가 같습니다. 반면 print(3.14f)는 모호하지 않습니다. float → double은 변환보다 순위가 높은 승격(promotion)이라 double 버전이 선택되기 때문입니다. 마찬가지로 print('a')는 char → int 승격으로 int 버전이 선택됩니다. 어떤 호출이 모호한지는 이 승격/변환 순위 규칙으로 결정되므로, “비슷해 보이는데 왜 이것만 모호한가”의 답은 대개 여기에 있습니다.
해결법:
// ✅ 해결 1: 명시적 캐스팅
print(static_cast<double>(42L));
// ✅ 해결 2: long 오버로드 추가
void print(long x) { std::cout << "long: " << x << '\n'; }
substitution failure / no type named ‘value_type’
에러: 시그니처의 의존 타입을 만들 수 없습니다.
// ❌ 에러 코드
template <typename T>
typename T::value_type getFirst(const T& container) {
return container[0];
}
int arr[5] = {1, 2, 3, 4, 5};
auto x = getFirst(arr); // int[5]에는 value_type이 없음
// GCC: error: no matching function for call to 'getFirst(int [5])'
// note: template argument deduction/substitution failed:
// error: 'int [5]' is not a class, struct, or union type
// Clang: note: candidate template ignored: substitution failure [with T = int[5]]:
// type 'int[5]' cannot be used prior to '::' because it has no members
반환 타입에서 실패했기 때문에 이 오버로드는 SFINAE로 후보에서 제외되고, 최종 에러는 no matching function이 됩니다. 같은 T::value_type을 함수 본문에서 썼다면 SFINAE가 적용되지 않아 no type named 'value_type' 하드 에러가 났을 것입니다. 에러 문구가 다른 이유가 바로 이 위치 차이입니다.
해결법:
// ✅ 해결 1: std::iterator_traits 사용
template <typename T>
auto getFirst(const T& container)
-> typename std::iterator_traits<decltype(std::begin(container))>::value_type {
return *std::begin(container);
}
// ✅ 해결 2: C++20 Concepts
template <typename T>
requires requires(T t) { t[0]; }
auto getFirst(const T& container) {
return container[0];
}
static assertion failed
에러: static_assert가 실패함.
// ❌ 에러 코드
template <typename T>
void process(T value) {
static_assert(std::is_integral_v<T>, "T must be integral type");
// ...
}
process(3.14); // double은 integral이 아님
// error: static assertion failed: T must be integral type
해결법:
// ✅ 올바른 타입 전달
process(42);
// ✅ 또는 제약 완화
template <typename T>
requires std::is_arithmetic_v<T> // integral 또는 floating_point
void process(T value) {
// ...
}
static_assert는 메시지를 직접 쓸 수 있어 읽기 쉽지만, 함수 본문에서 실패하므로 오버로드 해석에는 참여하지 않습니다. 즉 process를 다른 타입용으로 오버로드해 두어도 static_assert 버전이 선택되면 그대로 에러입니다. 여러 버전 중 하나를 고르게 하려면 requires나 enable_if로 시그니처에 제약을 두어야 합니다.
no known conversion (명시적 템플릿 인자 불일치)
에러: 명시한 템플릿 인자와 실제 인자 타입이 맞지 않습니다.
// ❌ 에러 코드
template <typename T>
void print(const std::vector<T>& vec) {
for (const T& x : vec) {
std::cout << x << '\n';
}
}
std::vector<int> vec = {1, 2, 3};
print<double>(vec); // vector<int>를 vector<double>로 취급
// note: candidate function template not viable:
// no known conversion from 'vector<int>' to 'const vector<double>' for 1st argument
vector<int>와 vector<double>은 원소끼리 변환이 가능해도 서로 무관한 타입입니다. 템플릿은 인스턴스마다 완전히 별개의 타입이 되므로, vector<Derived*>도 vector<Base*>로 변환되지 않습니다.
해결법:
// ✅ 타입 추론 사용
print(vec); // T = int로 자동 추론
// ✅ 또는 올바른 타입 명시
print<int>(vec);
incomplete type
에러: 전방 선언만 있고 정의가 없습니다.
// ❌ 에러 코드
class MyClass; // 전방 선언만
std::vector<MyClass> vec; // 객체 생성·소멸에는 완전한 타입 필요
// GCC: error: invalid use of incomplete type 'class MyClass'
// Clang: error: arithmetic on a pointer to an incomplete type 'MyClass'
원인: std::vector는 원소의 크기와 소멸자를 알아야 객체를 만들고 없앨 수 있습니다. 메시지는 컴파일러마다 달라서, Clang은 라이브러리 내부의 포인터 연산이나 sizeof에서 실패한 형태로 보고하기도 합니다. Visual Studio 편집기(IntelliSense)의 incomplete type is not allowed는 컴파일러가 아니라 EDG 기반 편집기 분석기의 문구입니다.
해결법:
// ✅ 해결 1: 정의 포함
#include "MyClass.h"
std::vector<MyClass> vec;
// ✅ 해결 2: 포인터 사용 (전방 선언만으로 가능)
std::vector<MyClass*> vec;
// ✅ 해결 3: unique_ptr 사용
std::vector<std::unique_ptr<MyClass>> vec;
해결 3에는 함정이 있습니다. std::unique_ptr<MyClass>를 멤버로 가진 클래스(pimpl 패턴)의 소멸자가 헤더에서 암시적으로 정의되면, MyClass가 불완전한 곳에서 delete가 인스턴스화되어 invalid application of 'sizeof' to incomplete type 에러가 납니다. 소멸자를 헤더에 선언만 하고 MyClass가 완전해지는 .cpp에서 = default로 정의하면 해결됩니다. 에러 위치가 unique_ptr.h 내부로 나와서 원인을 찾기 어렵기로 유명한 경우입니다.
deduced conflicting types
에러: 템플릿 인자를 한 가지로 추론할 수 없습니다.
// ❌ 에러 코드
template <typename T>
void process(T value, T other) {
// ...
}
process(42, 3.14); // T = int? double?
// GCC: note: deduced conflicting types for parameter 'T' ('int' and 'double')
템플릿 인자 추론에서는 암시적 변환을 고려하지 않습니다. 일반 함수 void process(double, double)이었다면 42가 double로 변환되어 호출되지만, 템플릿은 각 인자에서 T를 따로 추론한 뒤 결과가 다르면 바로 실패합니다.
해결법:
// ✅ 해결 1: 타입 명시
process<double>(42, 3.14);
// ✅ 해결 2: 두 개의 템플릿 인자
template <typename T1, typename T2>
void process(T1 value, T2 other) {
// ...
}
// ✅ 해결 3: 공통 타입 사용
template <typename T1, typename T2>
void process(T1 value, T2 other) {
using Common = std::common_type_t<T1, T2>;
Common a = value;
Common b = other;
// ...
}
invalid operands to binary expression
에러: 연산자를 사용할 수 없는 타입.
// ❌ 에러 코드
template <typename T>
T add(T a, T b) {
return a + b;
}
struct MyClass {};
MyClass obj1, obj2;
auto result = add(obj1, obj2); // MyClass에 operator+ 없음
// Clang: error: invalid operands to binary expression ('MyClass' and 'MyClass')
// GCC: error: no match for 'operator+' (operand types are 'MyClass' and 'MyClass')
해결법:
// ✅ 해결 1: operator+ 정의
struct MyClass {
int value;
MyClass operator+(const MyClass& other) const {
return {value + other.value};
}
};
// ✅ 해결 2: Concepts로 제약
template <typename T>
requires requires(T a, T b) { a + b; }
T add(T a, T b) {
return a + b;
}
해결 2는 에러를 없애 주지 않습니다. 대신 에러가 나는 위치를 템플릿 본문(return a + b;)에서 호출 지점(add(obj1, obj2))으로 옮기고, “a + b가 유효해야 한다는 조건이 실패했다”는 이유를 붙여 줍니다. 라이브러리를 만드는 입장에서는 이것이 사용자에게 주는 가장 큰 도움입니다.
member access into incomplete type
에러: 전방 선언된 타입의 멤버 접근.
// ❌ 에러 코드
class MyClass;
template <typename T>
void print(const T& obj) {
std::cout << obj.value << '\n'; // MyClass 정의 필요
}
void show(const MyClass& obj) {
print(obj);
}
// Clang: error: member access into incomplete type 'const MyClass'
참조로 받는 것까지는 전방 선언만으로 가능하지만, 멤버에 접근하는 순간 정의가 필요합니다. 템플릿은 인스턴스화 시점에 검사되므로, 에러 위치가 print 본문으로 표시되더라도 고칠 곳은 show가 있는 파일의 #include입니다.
해결법:
// ✅ 정의 포함
#include "MyClass.h"
template <typename T>
void print(const T& obj) {
std::cout << obj.value << '\n';
}
too many template arguments
에러: 템플릿 인자 개수 불일치.
// ❌ 에러 코드
std::vector<int, std::allocator<int>, int> vec; // 인자 3개
// error: too many template arguments for class template 'vector'
해결법:
// ✅ 올바른 인자 개수
std::vector<int> vec; // 기본 할당자 사용
std::vector<int, std::allocator<int>> vec2; // 명시적 할당자
이 에러는 매크로 안에서 쉼표가 들어간 템플릿을 쓸 때 가장 헷갈리게 나타납니다. MY_MACRO(std::map<int, int>)는 전처리기가 쉼표를 인자 구분자로 보고 std::map<int와 int> 두 인자로 나누기 때문입니다. 이때는 괄호로 한 번 더 감싸거나 using IntMap = std::map<int, int>; 별칭을 먼저 만들어 넘깁니다.
컴파일러별 에러 메시지 비교
동일한 에러, 다른 메시지
// 에러 코드
template <typename T>
void print(T value) {
std::cout << value.name << '\n'; // int에는 name 없음
}
print(42);
GCC 에러 메시지
main.cpp: In instantiation of 'void print(T) [with T = int]':
main.cpp:10:10: required from here
main.cpp:5:24: error: request for member 'name' in 'value', which is of non-class type 'int'
5 | std::cout << value.name << '\n';
| ^~~~
특징: 인스턴스화 경로(In instantiation of, required from here)를 먼저 보여 주고 에러를 마지막에 출력합니다. [with T = int]처럼 템플릿 인자를 함께 적어 줍니다.
Clang 에러 메시지
main.cpp:5:24: error: member reference base type 'int' is not a structure or union
std::cout << value.name << '\n';
~~~~~^~~~~
main.cpp:10:5: note: in instantiation of function template specialization 'print<int>' requested here
print(42);
^
특징: 에러를 먼저 보여 주고 경로를 note로 붙입니다. 표현식의 범위를 ~~~로 표시해 어느 부분이 문제인지 보기 쉽습니다.
MSVC 에러 메시지
main.cpp(5): error C2228: left of '.name' must have class/struct/union
main.cpp(5): note: type is 'int'
main.cpp(10): note: see reference to function template instantiation 'void print<int>(T)' being compiled
with
[
T=int
]
특징: 에러 코드(C2228)가 붙어 문서를 검색하기 쉽고, 템플릿 인자를 with [ ... ] 블록으로 따로 표시합니다. Visual Studio의 오류 목록 창은 첫 줄만 보여 주므로, 템플릿 에러는 출력(Output) 창에서 전체를 보는 것이 좋습니다.
권장: 다른 컴파일러를 보조로 사용
# 주 컴파일러가 GCC/MSVC여도, 에러 확인용으로 Clang 사용
clang++ -std=c++20 -fsyntax-only main.cpp
# 또는 Compiler Explorer (godbolt.org)에서 여러 컴파일러를 나란히 비교
어느 컴파일러가 “항상” 더 읽기 쉽다고 단정하기는 어렵습니다. 최근 GCC는 concept 진단이 꽤 자세하고, Clang은 표현식 범위 표시가 좋습니다. 제 경험으로는 한 컴파일러의 메시지가 이해되지 않을 때 다른 컴파일러의 같은 에러를 보면, 문구가 달라서 오히려 원인이 바로 보이는 경우가 많습니다. 빌드 시스템을 바꿀 필요 없이 -fsyntax-only로 문법·타입 검사만 돌리면 되므로 비용도 거의 들지 않습니다.
C++20 Concepts로 에러 단축
Before: 제약 없는 알고리즘
// C++17 스타일
template <typename T>
void process(const std::vector<T>& vec) {
std::sort(vec.begin(), vec.end()); // const vector는 정렬 불가
}
// GCC: error: assignment of read-only location '* __first' (stl_algo.h 내부)
// 또는 no matching function for call to 'swap(const int&, const int&)'
// ... 그 위로 std::__sort, std::__introsort_loop 등 인스턴스화 경로 ...
const vector의 begin()은 const_iterator를 돌려주므로 원소에 쓸 수 없습니다. 그런데 std::sort는 이 사실을 시그니처에서 검사하지 않기 때문에, 내부 구현이 원소를 교환하려는 지점까지 들어가서야 실패합니다.
After: 제약된 알고리즘(std::ranges)
// C++20
template <typename T>
void process(const std::vector<T>& vec) {
std::ranges::sort(vec); // const vector는 정렬 불가
}
// error: no match for call to '(const std::ranges::__sort_fn) (const std::vector<int>&)'
// note: constraints not satisfied
// note: the required expression ... is invalid / 'sortable<...>' evaluated to false
std::ranges::sort는 std::sortable concept으로 제약되어 있어, 호출 지점에서 바로 “제약을 만족하지 않는다”고 보고합니다. 메시지에 sortable이라는 이름이 나오므로 “정렬 가능한 범위가 아니다”라는 원인을 짐작하기도 쉽습니다. 수정은 매개변수를 std::vector<T>&로 바꾸거나 복사본을 정렬하는 것입니다.
Concepts 적용 예제
#include <concepts>
#include <vector>
#include <algorithm>
// 제약 조건 명시
template <typename T>
requires std::integral<T>
T add(T a, T b) {
return a + b;
}
// 사용
add(1, 2); // ✅ OK
add(1.5, 2.5); // ❌ error: no matching function for call to 'add(double, double)'
// note: constraints not satisfied
// note: the expression 'integral<T>' [with T = double] evaluated to 'false'
제약이 한 단계라면 메시지가 몇 줄로 끝납니다. concept이 다른 concept으로 여러 겹 정의되어 있으면 note도 그만큼 깊어지는데, GCC는 기본적으로 일부 깊이까지만 보여 주고 -fconcepts-diagnostics-depth=3 같은 옵션으로 더 자세히 펼칠 수 있습니다.
디버깅 전략
전략 1: 이진 탐색으로 원인 좁히기
// 복잡한 템플릿 코드
template <typename T>
void complex(const T& value) {
auto result = transform(value);
process(result);
output(result);
}
// 에러가 나면: 각 단계를 분리해서 테스트
template <typename T>
void complex(const T& value) {
auto result = transform(value);
// 여기까지 컴파일되나? → 주석 처리하며 확인
// process(result);
// output(result);
}
단계를 나눌 때는 중간 결과에 static_assert(std::is_same_v<decltype(result), 예상타입>);을 넣어 보는 것이 주석 처리보다 빠릅니다. 템플릿 에러의 상당수는 auto로 받은 중간 타입이 예상과 다른 것(참조가 빠졌거나, const가 붙었거나, 프록시 타입)에서 시작되기 때문입니다.
전략 2: 타입 출력으로 확인
// 컴파일 타임에 타입을 알고 싶을 때: 정의 없는 템플릿 트릭
template <typename T> struct TypeDisplay; // 선언만
template <typename T>
void debug_type(T&& value) {
TypeDisplay<decltype(value)> td; // error: aggregate 'TypeDisplay<int&> td' has incomplete type
}
// 실행 시 타입 확인 (이름은 구현마다 맹글링되어 나올 수 있음)
#include <typeinfo>
std::cout << typeid(value).name() << '\n';
TypeDisplay 트릭은 일부러 에러를 내서 컴파일러가 에러 메시지에 정확한 타입(참조·const 포함)을 찍게 만드는 방법입니다. static_assert의 메시지는 문자열 리터럴이어야 하므로(C++26 이전) typeid(T).name()을 넣을 수 없고, typeid는 참조와 최상위 const를 지워 버리기 때문에 타입 디버깅에는 이 트릭이 더 정확합니다.
전략 3: 단순화된 테스트 케이스
// 복잡한 코드에서 에러가 나면
// 최소 재현 코드(Minimal Reproducible Example) 작성
// Before: 복잡한 코드
template <typename T, typename U, typename V>
auto complex_function(T a, U b, V c) -> decltype(a + b * c) {
// 100줄 로직
}
// After: 최소 재현
template <typename T>
auto simple(T a, T b) -> decltype(a + b) {
return a + b;
}
// 이것만 테스트해서 에러 원인 파악
최소 재현 코드를 Compiler Explorer에 올려 두면 여러 컴파일러와 표준 버전을 한 번에 바꿔 볼 수 있어서, “GCC 버그인가 내 코드 문제인가”도 빠르게 가를 수 있습니다.
전략 4: 컴파일러 옵션 활용
# GCC: 출력할 인스턴스화 경로 개수 제한 (기본 10, 0이면 제한 없음)
g++ -ftemplate-backtrace-limit=5 main.cpp
# GCC: concept 실패 원인을 더 깊이 표시
g++ -std=c++20 -fconcepts-diagnostics-depth=3 main.cpp
# Clang: 에러 메시지 색상 강조
clang++ -fcolor-diagnostics main.cpp
# MSVC: 열 위치와 캐럿(^) 표시
cl /diagnostics:caret main.cpp
-ftemplate-backtrace-limit은 경로가 길 때 앞뒤 일부만 남기고 중간을 생략하므로, 내 파일이 경로 중간에 있으면 오히려 가려질 수 있습니다. 경로가 필요할 때는 제한을 늘리고 grep으로 내 파일 이름만 걸러 보는 편이 확실합니다.
전략 5: Concepts로 사전 검증
// 템플릿 사용 전에 제약 확인
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
template <Addable T>
T add(T a, T b) {
return a + b;
}
// 잘못된 타입은 호출 지점에서 즉시 에러
struct NoAdd {};
add(NoAdd{}, NoAdd{}); // error: constraints not satisfied
// 개념 자체를 단독으로 검사해 볼 수도 있음
static_assert(Addable<int>);
static_assert(!Addable<NoAdd>);
실전 사례 분석
사례 1: STL 알고리즘 출력 타입 불일치
에러 코드:
std::vector<std::string> names = {"Alice", "Bob", "Charlie"};
std::vector<int> ids = {1, 2, 3};
// ❌ 결과(string)를 int 벡터에 쓰려 함
std::transform(names.begin(), names.end(), ids.begin(), ids.begin(),
[](const std::string& name, int id) {
return name + std::to_string(id);
});
// GCC: error: cannot convert 'std::__cxx11::basic_string<char>' to 'int' in assignment
// (stl_algo.h 내부 *__result = __binary_op(*__first1, *__first2); 에서)
문제: std::transform의 출력 반복자가 ids.begin()인데, 람다의 결과 타입은 std::string입니다. 알고리즘은 출력 반복자 타입을 검사하지 않으므로, 에러는 내부 구현의 대입문에서 납니다.
해결:
// ✅ 올바른 코드
std::vector<std::string> results;
std::transform(names.begin(), names.end(), ids.begin(),
std::back_inserter(results),
[](const std::string& name, int id) {
return name + std::to_string(id);
});
사례 2: 중첩 의존 타입 오류
에러 코드:
// ❌ 에러 코드
template <typename Container>
void printFirst(const Container& c) {
typename Container::value_type::iterator it; // 중첩 타입
// ...
}
std::vector<int> vec = {1, 2, 3};
printFirst(vec);
// error: 'int' is not a class, struct, or union type
문제: Container::value_type은 int이고, int에는 iterator가 없습니다.
해결:
// ✅ 올바른 코드
template <typename Container>
void printFirst(const Container& c) {
typename Container::const_iterator it = c.begin(); // const 참조이므로 const_iterator
// ...
}
c가 const Container&이므로 c.begin()은 const_iterator를 반환합니다. 여기서 typename Container::iterator it = c.begin();이라고 쓰면 conversion from '__normal_iterator<const int*, ...>' to non-scalar type '__normal_iterator<int*, ...>' requested 에러가 납니다. 이런 실수를 피하는 가장 쉬운 방법은 auto it = c.begin();입니다.
사례 3: SFINAE로 후보가 모두 제외됨
에러 코드:
// ❌ 에러 코드
template <typename T>
auto getSize(const T& container) -> decltype(container.size()) {
return container.size();
}
int arr[5];
auto s = getSize(arr); // 배열에는 size() 없음
// error: no matching function for call to 'getSize(int [5])'
// note: candidate template ignored: substitution failure [with T = int[5]]:
// member reference base type 'const int[5]' is not a structure or union
해결:
// ✅ 해결 1: std::size 사용 (C++17)
#include <iterator>
auto s = std::size(arr); // 배열도 지원
// ✅ 해결 2: 오버로드 추가
template <typename T, size_t N>
size_t getSize(const T (&arr)[N]) {
return N;
}
에러 메시지 패턴 인식
”note: candidate” 읽는 법
note: candidate function template not viable: requires 2 arguments, but 1 was provided
^^^^^^^^ ^^^^^^^^ ^^^^^^^^ ^^^ ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
후보 함수 템플릿 불가능 이유
패턴:
not viable: 이 후보는 사용 불가requires X arguments, but Y were provided: 인자 개수 불일치'this' argument has type 'const T': const 불일치no known conversion from 'X' to 'Y': 타입 변환 불가candidate template ignored: substitution failure: SFINAE로 제외됨candidate template ignored: deduced conflicting types: 인자마다 다른 타입이 추론됨
”note: because” 읽는 법
note: because 'double' does not satisfy 'integral'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
실제 원인 (이 부분만 읽으면 됨)
핵심: because 뒤의 내용이 실제 원인입니다. concept이 중첩되어 있으면 because가 여러 줄 이어지는데, 가장 안쪽(마지막) 줄이 구체적으로 어떤 식이 실패했는지를 알려 줍니다.
”required from” 읽는 법
note: in instantiation of function template specialization 'std::sort<...>' requested here
note: required from 'std::__sort<...>'
note: required from 'std::__introsort_loop<...>'
의미: 템플릿 인스턴스화 체인입니다. 라이브러리 내부 줄은 건너뛰어도 되고, 내 파일이 나오는 줄만 찾으면 됩니다.
도구 활용
Compiler Explorer (godbolt.org)
1. godbolt.org 접속
2. 에러 코드 붙여넣기
3. 컴파일러를 여러 개 추가 (GCC, Clang, MSVC)
4. 에러 메시지 나란히 비교
장점:
- 여러 컴파일러·버전·표준 모드로 동시에 테스트
- 로컬 환경 설치 없이 재현 코드 공유 가능 (링크로 동료에게 전달)
C++ Insights (cppinsights.io)
템플릿이 실제로 어떤 코드로 인스턴스화되는지, auto와 범위 기반 for가 어떻게 풀리는지 보여 줍니다.
// 입력
template <typename T>
T add(T a, T b) { return a + b; }
auto x = add(1, 2);
// 출력 (인스턴스화된 코드, 개략)
template<>
int add<int>(int a, int b) { return a + b; }
int x = add(1, 2);
에러 자체를 읽어 주지는 않지만, “이 템플릿에 실제로 어떤 타입이 들어갔는가”를 눈으로 확인할 수 있어 전략 2의 타입 출력과 비슷한 역할을 합니다.
자주 나오는 템플릿 에러 메시지 대조표
| 에러 메시지 | 의미 | 해결법 |
|---|---|---|
no match for 'operator<' | 알고리즘이 요구하는 연산 없음 | 비교자 전달 또는 연산자 정의 |
no matching function | 오버로드를 찾지 못함 | 후보별 실패 이유 확인 |
ambiguous | 여러 오버로드가 같은 순위 | 명시적 캐스팅 또는 오버로드 추가 |
substitution failure | 시그니처 치환 실패(SFINAE) | 제약 조건·타입 확인 |
static assertion failed | static_assert 실패 | 제약 조건 확인 |
no known conversion | 타입 변환 불가 | 올바른 타입 전달 |
incomplete type | 전방 선언만 있음 | 정의 포함, 소멸자 위치 확인 |
deduced conflicting types | 템플릿 인자 추론 실패 | 명시적 타입 지정 |
자주 묻는 질문 (FAQ)
Q. 에러 메시지를 텍스트 파일로 저장하려면?
A. 컴파일러는 진단을 표준 에러로 출력하므로 2>로 리다이렉트합니다.
# GCC/Clang
g++ main.cpp 2> error.txt
# 내 파일이 나오는 줄만 추리기
g++ main.cpp 2>&1 | grep -n "main.cpp"
Visual Studio에서는 출력(Output) 창의 빌드 로그를 복사하거나, msbuild를 명령줄에서 실행해 파일로 리다이렉트할 수 있습니다.
Q. 템플릿 에러를 컴파일 타임에 더 일찍 잡으려면?
A. 가능하면 시그니처에 제약(Concepts, requires)을 두고, 본문에서만 확인할 수 있는 조건은 static_assert로 보강합니다.
template <std::integral T> // 호출 지점에서 검사 (오버로드 해석에 참여)
void process(T value) {
static_assert(sizeof(T) <= 8, "64비트를 넘는 정수는 지원하지 않습니다"); // 본문 검사
// ...
}
Q. 외부 라이브러리의 템플릿 에러는 어떻게 읽나요?
A. 내 코드의 호출 지점을 먼저 찾으세요. 인스턴스화 경로에서 라이브러리 경로가 아닌 첫 파일이 대개 고칠 곳입니다.
/usr/include/boost/... ← 라이브러리 내부 (무시)
note: required from ...
/usr/include/boost/... ← 라이브러리 내부 (무시)
note: required from here
main.cpp:42: ← 여기! 내 코드
라이브러리가 Concepts를 쓰지 않는 오래된 버전이라면, 문서에서 해당 함수가 요구하는 타입 조건(예: “T must be DefaultConstructible”)을 찾아 내 타입과 대조하는 것이 가장 빠릅니다.
고급 주제: 템플릿 메타프로그래밍 에러
SFINAE 에러 패턴
// SFINAE: Substitution Failure Is Not An Error
template <typename T>
auto getSize(const T& container) -> decltype(container.size()) {
return container.size();
}
template <typename T, size_t N>
size_t getSize(const T (&arr)[N]) {
return N;
}
// 사용
std::vector<int> vec;
getSize(vec); // ✅ 첫 번째 오버로드 선택
int arr[5];
getSize(arr); // ✅ 두 번째 오버로드 선택 (첫 번째는 SFINAE로 제외)
enable_if 에러
// ❌ 에러 코드
template <typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
void print(T value) {
std::cout << value << '\n';
}
print(3.14); // double은 integral이 아님
// Clang: note: candidate template ignored: requirement 'is_integral_v<double>' was not satisfied
// GCC: note: template argument deduction/substitution failed:
// error: no type named 'type' in 'struct std::enable_if<false, void>'
enable_if 실패 메시지는 컴파일러마다 차이가 큽니다. Clang은 enable_if를 알아보고 조건식을 보여 주지만, GCC는 enable_if<false, void>에 type이 없다는 구현 수준의 문구를 냅니다. 조건이 복잡한 enable_if일수록 GCC 메시지로는 어떤 부분 조건이 거짓인지 알기 어렵습니다.
해결: Concepts로 대체 (C++20).
// ✅ C++20
template <std::integral T>
void print(T value) {
std::cout << value << '\n';
}