Skip to content

C++20 Concepts & Requires — 타입 제약 조건 선언과 컴파일 오류 개선

Concepts는 C++20에서 도입된 템플릿 파라미터에 대한 타입 제약 조건 선언 메커니즘입니다. 기존에는 SFINAE(std::enable_if)나 static_assert를 이용해 타입 조건을 걸었지만, 오류 메시지가 수십 줄의 내부 인스턴스화 오류로 출력되어 디버깅이 매우 어려웠습니다.

Concepts를 사용하면:

  • 템플릿 파라미터가 어떤 조건을 만족해야 하는지 명시적으로 선언할 수 있습니다.
  • 조건을 만족하지 않으면 짧고 명확한 오류 메시지가 출력됩니다.
  • 오버로드 해결(overload resolution)에도 Concepts를 활용해 더 세밀한 제어가 가능합니다.

#include <concepts>
#include <type_traits>
// concept 키워드로 선언 — bool 값을 반환하는 제약 조건
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
// 복합 조건 — && 와 || 사용 가능
template<typename T>
concept SignedIntegral = std::integral<T> && std::signed_integral<T>;
// requires 표현식으로 인터페이스 제약
template<typename T>
concept Printable = requires(T x) {
// x에 대해 이 표현식이 유효해야 함
{ std::cout << x } -> std::same_as<std::ostream&>;
};
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};

template<typename T>
concept FullConcept = requires(T x, T y) {
// 1. 단순 표현식 — 해당 표현식이 유효해야 함
x + y;
// 2. 타입 요구사항 — 해당 타입이 존재해야 함
typename T::value_type;
// 3. 복합 요구사항 — 표현식 + 반환 타입 제약
{ x.size() } -> std::convertible_to<std::size_t>;
// 4. 중첩 requires — 추가적인 bool 조건
requires std::copyable<T>;
};
// 중첩 requires 활용 예시
template<typename Container>
concept SizedContainer = requires(Container c) {
{ c.size() } -> std::convertible_to<std::size_t>;
{ c.empty() } -> std::same_as<bool>;
typename Container::value_type;
typename Container::iterator;
};
template<SizedContainer C>
void PrintInfo(const C& container)
{
std::cout << "size: " << container.size()
<< ", empty: " << container.empty() << "\n";
}

3. Concept 적용 방법 — 네 가지 문법

Section titled “3. Concept 적용 방법 — 네 가지 문법”
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// 방법 1: template 파라미터 자리에 concept 이름 사용
template<Number T>
T Add(T a, T b) { return a + b; }
// 방법 2: requires 절 (requires clause)
template<typename T>
requires Number<T>
T Multiply(T a, T b) { return a * b; }
// 방법 3: 축약 함수 템플릿 (abbreviated function template, C++20)
Number auto Square(Number auto x) { return x * x; }
// 방법 4: requires 절 (후위)
template<typename T>
T Subtract(T a, T b) requires Number<T> { return a - b; }

4. 표준 라이브러리 제공 Concepts (<concepts>)

Section titled “4. 표준 라이브러리 제공 Concepts (<concepts>)”
Concept의미
std::same_as<T, U>T와 U가 동일한 타입
std::derived_from<T, Base>T가 Base의 파생 클래스
std::convertible_to<From, To>From이 To로 암시적 변환 가능
std::integral<T>정수 타입
std::floating_point<T>부동소수점 타입
std::copyable<T>복사 가능한 타입
std::movable<T>이동 가능한 타입
std::equality_comparable<T>== 비교 가능한 타입
std::totally_ordered<T>완전 순서 비교(<, >, <=, >=) 가능
std::invocable<F, Args...>F가 Args로 호출 가능
std::regular<T>기본 생성, 복사, 이동, 비교 모두 가능

5. SFINAE vs Concepts — 오류 메시지 비교

Section titled “5. SFINAE vs Concepts — 오류 메시지 비교”
// 구식 방식 — 오류 메시지가 장황함
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T> OldDouble(T x)
{
return x * 2;
}
// OldDouble("hello"); 호출 시 오류:
// error: no matching function for call to 'OldDouble(const char[6])'
// note: candidate: ... enable_if_t<is_integral_v<T>, T> OldDouble(T)
// note: template argument deduction/substitution failed:
// ... (수십 줄 내부 오류)
template<std::integral T>
T NewDouble(T x) { return x * 2; }
// NewDouble("hello"); 호출 시 오류:
// error: no matching function for call to 'NewDouble(const char[6])'
// note: constraints not satisfied:
// 'std::integral<const char*>' evaluated to false
// (명확하고 짧은 메시지)

6. 여러 Concept 조합 — 실전 예시

Section titled “6. 여러 Concept 조합 — 실전 예시”
#include <concepts>
#include <iterator>
// 컨테이너를 정렬할 수 있는 조건
template<typename Container>
concept Sortable = requires(Container c) {
{ std::begin(c) } -> std::random_access_iterator;
{ std::end(c) } -> std::random_access_iterator;
} && std::totally_ordered<typename Container::value_type>;
template<Sortable C>
void Sort(C& container)
{
std::sort(std::begin(container), std::end(container));
}
// 수학적 그룹(더하기 가능, 0 원소 존재) 개념
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ T{} }; // 기본 생성 (0 원소)
};
template<Addable T>
T Sum(const std::vector<T>& values)
{
T result{};
for (const auto& v : values) result = result + v;
return result;
}

7. Concept을 이용한 오버로드 선택

Section titled “7. Concept을 이용한 오버로드 선택”

Concepts는 오버로드 해결에서 더 구체적인 제약을 가진 후보를 우선 선택합니다.

template<typename T>
concept Integral = std::integral<T>;
template<typename T>
concept SignedIntegral = Integral<T> && std::signed_integral<T>;
// 일반 정수 처리
template<Integral T>
void Process(T value)
{
std::cout << "일반 정수: " << value << "\n";
}
// 부호 있는 정수 처리 — 더 구체적인 제약이므로 우선 선택됨
template<SignedIntegral T>
void Process(T value)
{
std::cout << "부호 정수: " << value << " (음수 가능)\n";
}
Process(42); // SignedIntegral 버전 선택 (int는 signed)
Process(42u); // Integral 버전 선택 (unsigned int)

// 직렬화 가능한 타입을 위한 Concept
template<typename T>
concept Serializable = requires(T obj, std::ostream& os, std::istream& is) {
{ obj.Serialize(os) } -> std::same_as<void>;
{ obj.Deserialize(is) } -> std::same_as<void>;
{ obj.GetTypeId() } -> std::convertible_to<int>;
};
// 범위 기반 처리를 위한 범용 함수
template<Serializable T>
void SaveToFile(const T& obj, const std::string& path)
{
std::ofstream file(path);
obj.Serialize(file);
}
// 팩토리 함수 — 기본 생성 + 복사 가능 타입만 허용
template<typename T>
requires std::default_initializable<T> && std::copyable<T>
std::vector<T> CreateN(int n, const T& prototype)
{
return std::vector<T>(n, prototype);
}

구분SFINAE (C++11/14)Concepts (C++20)
오류 메시지장황하고 파악 어려움명확하고 짧음
가독성낮음 (enable_if 중첩)높음 (선언적 문법)
오버로드 우선순위복잡한 규칙제약 포함 관계로 명확
인터페이스 문서화암묵적명시적
지원 버전C++11 이상C++20 이상

Concepts는 단순히 문법 편의 기능이 아니라, 템플릿 라이브러리의 인터페이스 계약을 코드로 표현하는 수단입니다. <concepts> 헤더의 표준 Concepts를 먼저 활용하고, 프로젝트 도메인에 맞는 커스텀 Concept을 추가하는 방식으로 접근하면 효과적입니다.