C++ concepts, 템플릿 인자를 제약하는 법
C++20 concepts는 템플릿 인자가 만족해야 하는 요구를 이름으로 적는 제약입니다.
이 글은 cppreference의 constraints·concepts 문서를 2026-09-01 기준으로 정리한 일반 설명이며, 표준 버전과 구현에 따라 세부 동작은 달라질 수 있습니다.
concepts는 무엇인가?
한 줄 답:
이 개념은 템플릿 인자가 만족해야 할 요구를 이름으로 묶은 컴파일 시점 조건이며, 템플릿 인터페이스의 일부가 됩니다.
클래스 템플릿, 함수 템플릿, 제네릭 람다를 포함한 템플릿 함수에는 템플릿 인자에 대한 제약을 연결할 수 있습니다. 이 제약은 여러 후보 가운데 적절한 오버로드나 템플릿 특수화를 선택하는 데 사용됩니다. 이름 있는 요구는 컴파일 시점에 평가되는 술어이므로, 선언만 읽어도 해당 템플릿이 어떤 인자를 기대하는지 드러납니다.
개념은 네임스페이스 범위에서 이름과 제약 식을 함께 정의합니다. 기본 형태는 template <typename T> concept 이름 = 제약식;이며, 이름을 식별자로 사용하면 요구가 만족될 때 true, 만족되지 않을 때 false가 되는 조건으로 취급됩니다.
다음 Hashable은 <concepts> 표준 라이브러리에 정의된 이름이 아니라, cppreference의 constraints 문서에 나오는 문서 예시 개념입니다.
template<typename T>
concept Hashable = requires(T a)
{
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
같은 요구는 템플릿 매개변수와 함수 선언의 여러 위치에 연결할 수 있습니다. 아래 형태들은 같은 Hashable<T> 제약을 서로 다른 문법 위치에 적는 대응 예시이며, 한 함수에 모두 함께 쓰는 형태는 아닙니다.
- 타입 제약:
template<Hashable T> void f(T) {} - 템플릿 매개변수 목록 뒤의
requires절:template<typename T> requires Hashable<T> void f(T) {} - 함수 선언자 끝의
requires절:template<typename T> void f(T) requires Hashable<T> {} - 축약 함수 템플릿 매개변수:
void f(Hashable auto) {}
타입 제약 표기에서는 문맥에서 추론된 타입이 개념의 첫 번째 인수로 암묵적으로 전달됩니다. 따라서 일반적인 개념 호출에서 Hashable<T>라고 적는 조건을 Hashable T처럼 줄여 쓸 수 있습니다.
개념 정의 자체에는 몇 가지 제한이 있습니다. 자기 자신을 재귀적으로 참조할 수 없고, 이미 다른 제약을 붙인 템플릿 매개변수로 개념을 제한할 수도 없습니다. 개념의 명시적 인스턴스화, 명시적 특수화, 부분 특수화도 허용되지 않습니다.
requires는 어떻게 쓰나?
한 줄 답:
requires 절은 템플릿이나 함수에 조건을 붙이고, requires 식은 그 조건을 만들기 위한 유효성 검사를 표현합니다.
requires 절은 템플릿 매개변수 목록 바로 뒤에 놓거나 함수 선언자의 마지막에 둘 수 있습니다. 절 뒤에는 템플릿 인자와 관련된 상수 식이 와야 하며, 보통 이름 있는 개념이나 여러 개념의 논리 결합, 또는 requires 식을 적습니다. 함수 호출처럼 보이는 조건식은 필요한 경우 괄호로 묶어 하나의 식으로 표현해야 합니다.
다음에서 Addable은 설명을 위해 정의한 예시 개념 이름입니다. 첫 번째 선언의 requires (T x) { x + x; } 부분이 requires 식이고, 두 번째 선언에서 requires Addable<T> 부분이 그 식으로 만든 조건을 함수에 연결하는 requires 절입니다.
template<typename T>
concept Addable = requires (T x) {
x + x;
};
template<typename T>
requires Addable<T>
T add(T a, T b) {
return a + b;
}
같은 절은 함수 선언자 끝에도 template<typename T> T add(T a, T b) requires Addable<T> { return a + b; }처럼 둘 수 있습니다. requires true처럼 항상 참인 상수 식도 문법상 가능하지만, 실제 제약에는 의미가 드러나는 개념이나 유효성 검사를 사용합니다.
requires 식은 bool 타입의 prvalue를 만들며, requires { 요구 목록 } 또는 requires (지역 매개변수 목록) { 요구 목록 } 형태로 씁니다. 요구는 C++20에서 다음 네 종류로 나뉩니다.
- 단순 요구:
a + b;처럼 식이 유효한지만 확인합니다. 식은 실제로 평가하지 않습니다. - 타입 요구:
typename T::inner;처럼 중첩 타입이나 타입 이름을 사용할 수 있는지 확인합니다. - 복합 요구:
{ x + 1 } -> std::same_as<int>;처럼 식의 유효성과 결과 타입에 대한 제약을 함께 확인합니다.{ *x } -> std::convertible_to<typename T::inner>;처럼 결과가 특정 타입으로 변환 가능한지도 표현할 수 있습니다. - 중첩 요구: 지역 매개변수를 바탕으로
requires뒤에 추가 조건을 적습니다.
식의 지역 매개변수는 표기와 유효성 검사에만 쓰이며 저장 공간이나 수명, 연결을 갖는 실제 객체가 아닙니다. 이 매개변수 목록에는 기본 인수를 둘 수 없고 말줄임표로 끝낼 수도 없습니다.
표준 개념은 어디서 보나?
한 줄 답:
표준 라이브러리의 이름 있는 요구는 <concepts> 헤더와 std 네임스페이스의 concepts library 문서에서 확인합니다.
이 라이브러리는 템플릿 인자를 컴파일 시점에 검증하고, 타입의 속성에 따라 함수 분기를 선택할 때 사용할 기본 개념을 제공합니다. 핵심 언어 개념은 <concepts> 헤더에 정의되며 std 네임스페이스에 있습니다.
자주 확인할 이름은 다음과 같습니다.
| 이름 | 뜻 |
|---|---|
std::integral | 정수형 타입입니다. |
std::same_as<T, U> | 두 타입이 같은지 확인합니다. |
std::convertible_to<From, To> | From이 To로 암시적 변환 가능한지 확인합니다. |
std::derived_from<Derived, Base> | 한 타입이 다른 타입에서 파생되었는지 확인합니다. |
std::floating_point | 부동소수점 타입인지 확인합니다. |
std::signed_integral, std::unsigned_integral처럼 정수의 성질을 더 좁히는 이름도 같은 문서에서 확인할 수 있습니다. 비교·객체·호출과 관련된 개념도 같은 표준 라이브러리 문서에 있으며, iterator·algorithms·ranges 라이브러리에는 추가 개념이 정의되어 있습니다.
헤더와 네임스페이스를 명시한 뒤 타입 제약을 매개변수에 바로 적을 수 있습니다.
#include <concepts>
template<std::integral T>
T twice(T value) {
return value + value;
}
std::integral T는 문맥에서 추론된 T를 이 개념의 인수로 전달하는 타입 제약입니다. 따라서 호출에 사용한 타입이 정수형 요구를 만족할 때만 이 함수 템플릿이 후보가 됩니다.
표준 개념은 구문적 요구와 의미적 요구를 함께 가질 수 있습니다. 구문적 요구를 만족하면 개념이 satisfied 상태가 되고, 의미적 요구까지 실제로 지키면 modeled 상태가 되지만, 일반적으로 컴파일러가 직접 확인할 수 있는 범위는 구문적 요구입니다.
SFINAE 대신 이 제약을 쓰는 이유는 무엇인가?
한 줄 답:
이 제약은 템플릿 인터페이스에 요구를 드러내고, 조건 위반을 인스턴스화 초기에 컴파일 시점에 진단해 오류의 원인을 따라가기 쉽게 합니다.
이전의 템플릿 필터링은 인자를 치환하는 과정에서 실패하는지에 의존하기도 했습니다. 여기서는 그 구현 방법을 다루기보다, 이름 있는 요구를 선언에 직접 연결했을 때 생기는 차이에 집중합니다.
조건을 만족하지 않는 템플릿은 인스턴스화 초기부터 걸러집니다. 그 결과 일반적인 연산 오류가 긴 후보 목록과 함께 나타나는 대신, 어떤 이름 있는 개념이 만족되지 않았는지와 해당 조건의 위치가 진단에 드러나기 쉬워집니다. 오류가 사라진다는 뜻은 아니지만, 실패한 요구를 템플릿 인터페이스와 연결해 읽을 수 있습니다.
이름 있는 요구는 템플릿의 인터페이스 일부가 되므로, 함수 본문을 읽기 전에 허용할 인자의 범위를 파악할 수 있습니다. 또한 여러 제약을 &&로 결합하면 왼쪽부터 확인하고, 왼쪽 조건이 만족되지 않을 때 오른쪽에 대한 치환을 시도하지 않습니다. 이 단락 평가가 즉시 문맥 밖에서 발생할 수 있는 불필요한 치환 실패를 피하는 데 도움을 주며, || 결합도 같은 방식으로 단락 평가됩니다.
제약의 이름은 단순히 특정 연산 하나가 존재하는지를 나열하기보다 숫자나 호출 가능한 대상처럼 코드가 다루려는 의미 범주를 나타내는 편이 적절합니다. 템플릿 제약과 별개로 C++ 언어 기능의 범위를 점검하는 참고 글은 C++ 기술면접 질문 10가지에서 확인할 수 있습니다.
FAQ
한 줄 답:
템플릿 인자의 요구를 이름으로 표현하려면 개념을 정의한 뒤 타입 제약이나 requires 절로 템플릿 선언에 연결하면 됩니다.
C++20에서 템플릿 인자를 이름으로 제약하려면 무엇을 사용합니까? concept 정의와 타입 제약 또는 requires 절을 사용합니다. 직접 요구를 적어야 하면 requires 식으로 유효성 검사를 정의합니다.
requires 절과 requires 식은 무엇이 다릅니까? requires 절은 템플릿이나 함수 선언에 연결하는 제약이고, requires 식은 요구 목록을 검사해 bool prvalue를 만드는 식입니다.
표준 개념 이름은 어디서 확인합니까? <concepts> 헤더와 concepts library 문서에서 확인합니다. std::integral, std::same_as, std::convertible_to처럼 문서에 정의된 이름을 사용합니다.
제약을 어기면 언제 진단됩니까? 템플릿 인스턴스화 초기의 컴파일 시점에 진단됩니다. 진단에는 만족되지 않은 개념이나 요구가 표시될 수 있습니다.
출처
한 줄 답:
본문의 정의와 예시는 2026-09-01 기준으로 확인한 cppreference의 constraints·concepts·requires 문서를 바탕으로 정리했습니다.
- Constraints and concepts — 제약의 정의와 개념 선언, 적용 위치, 논리 결합, 단락 평가, 진단을 확인했습니다.
- Concepts library —
<concepts>헤더와std네임스페이스의 표준 개념 이름, 구문적·의미적 요구를 확인했습니다. requireskeyword —requires키워드가 템플릿 제약과 중첩 요구에 사용되는 방식을 확인했습니다.- Requires expression —
requires식의 구문과 단순·타입·복합·중첩 요구, 지역 매개변수 규칙을 확인했습니다.