콘텐츠로 이동

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 적용 방법 — 네 가지 문법

섹션 제목: “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>)

섹션 제목: “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 — 오류 메시지 비교

섹션 제목: “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 조합 — 실전 예시

섹션 제목: “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을 이용한 오버로드 선택

섹션 제목: “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);
}

Concepts는 서로 포함(subsume) 관계를 형성합니다. 오버로드 해결 시 더 구체적인(포함 범위가 좁은) Concept을 가진 후보가 우선 선택됩니다.

// B가 A를 포함(subsume)하면 B가 더 구체적
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
template<typename T>
concept Integral = std::integral<T>; // Arithmetic의 부분집합
template<typename T>
concept SignedIntegral = Integral<T> && std::signed_integral<T>; // 더 구체적
// 오버로드 셋
template<Arithmetic T> void foo(T) { std::cout << "Arithmetic\n"; }
template<Integral T> void foo(T) { std::cout << "Integral\n"; }
template<SignedIntegral T> void foo(T) { std::cout << "SignedIntegral\n"; }
foo(42); // SignedIntegral 선택 — 가장 구체적
foo(42u); // Integral 선택 — unsigned는 SignedIntegral 미충족
foo(3.14); // Arithmetic 선택 — 유일하게 충족

포함 관계는 requires 표현식의 논리적 포함으로 판단됩니다.
A && BA를 포함(subsume)하고, A || BA에 의해 포함됩니다.


C++20에서는 auto 자리에 Concept을 붙여 함수 인자·반환 타입·변수를 제약할 수 있습니다.

// 함수 파라미터에 제약된 auto
void PrintNumber(std::integral auto n)
{
std::cout << "정수: " << n << "\n";
}
// 반환 타입에 제약된 auto
std::floating_point auto ComputeArea(double r)
{
return 3.14159 * r * r; // double 반환 — floating_point 만족하므로 OK
}
// 변수 타입 제약
std::integral auto x = 42; // OK
// std::integral auto y = 3.14; // 오류: double은 integral 아님
// 제네릭 람다에서도 사용 가능
auto square = [](std::integral auto n) { return n * n; };
auto result = square(5); // OK
// auto bad = square(3.14); // 오류

#include <concepts>
#include <cmath>
// 수치 타입 제약
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// 벡터 공간 개념 — 스칼라 곱과 덧셈 지원
template<typename V>
concept VectorSpace = requires(V v, V u, double s) {
{ v + u } -> std::same_as<V>;
{ v - u } -> std::same_as<V>;
{ s * v } -> std::same_as<V>;
{ v.norm() } -> std::floating_point;
};
template<VectorSpace V>
V normalize(const V& v)
{
return (1.0 / v.norm()) * v;
}
// 반복 가능한 컨테이너에서 합계
template<typename Range>
requires std::ranges::input_range<Range>
&& Numeric<std::ranges::range_value_t<Range>>
auto sum(const Range& r)
{
using T = std::ranges::range_value_t<Range>;
T total{};
for (const auto& v : r) total += v;
return total;
}
// 직렬화 지원 타입 판별
template<typename T>
concept JsonSerializable = requires(const T& obj) {
{ obj.to_json() } -> std::convertible_to<std::string>;
} || std::is_arithmetic_v<T> || std::same_as<T, std::string>;
template<JsonSerializable T>
std::string serialize(const T& value)
{
if constexpr (std::is_arithmetic_v<T>)
return std::to_string(value);
else if constexpr (std::same_as<T, std::string>)
return "\"" + value + "\"";
else
return value.to_json();
}

// 함정 1: requires 절 vs requires 표현식 혼동
template<typename T>
requires requires(T x) { x + x; } // 바깥 requires = 제약, 안쪽 requires = 표현식
T double_val(T x) { return x + x; }
// 함정 2: 포함 관계 없이 모호한 오버로드
template<typename T>
concept A = std::integral<T>;
template<typename T>
concept B = std::signed_integral<T>;
// A와 B 사이에 직접적인 subsumption이 없으면 모호 오류 발생 가능
// B를 A && std::signed_integral<T>로 정의해야 포함 관계 성립
// 함정 3: bool 반환이지만 constexpr이 아닌 함수를 concept에 사용
constexpr bool is_valid(int n) { return n > 0; } // OK
// bool runtime_check(int n) { return n > 0; } // 런타임 함수는 concept 내 사용 불가
// 올바른 패턴
template<auto N>
concept Positive = (N > 0); // 비타입 파라미터 제약

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

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