C++20 Concepts & Requires — 타입 제약 조건 선언과 컴파일 오류 개선
개요 — Concepts란 무엇인가
Section titled “개요 — Concepts란 무엇인가”Concepts는 C++20에서 도입된 템플릿 파라미터에 대한 타입 제약 조건 선언 메커니즘입니다. 기존에는 SFINAE(std::enable_if)나 static_assert를 이용해 타입 조건을 걸었지만, 오류 메시지가 수십 줄의 내부 인스턴스화 오류로 출력되어 디버깅이 매우 어려웠습니다.
Concepts를 사용하면:
- 템플릿 파라미터가 어떤 조건을 만족해야 하는지 명시적으로 선언할 수 있습니다.
- 조건을 만족하지 않으면 짧고 명확한 오류 메시지가 출력됩니다.
- 오버로드 해결(overload resolution)에도 Concepts를 활용해 더 세밀한 제어가 가능합니다.
1. Concept 선언 기본 문법
Section titled “1. Concept 선언 기본 문법”#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>;};2. requires 표현식 네 가지 형태
Section titled “2. requires 표현식 네 가지 형태”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 — 오류 메시지 비교”SFINAE 방식 (C++14/17)
Section titled “SFINAE 방식 (C++14/17)”// 구식 방식 — 오류 메시지가 장황함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:// ... (수십 줄 내부 오류)Concepts 방식 (C++20)
Section titled “Concepts 방식 (C++20)”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)8. 커스텀 Concept 설계 패턴
Section titled “8. 커스텀 Concept 설계 패턴”// 직렬화 가능한 타입을 위한 Concepttemplate<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을 추가하는 방식으로 접근하면 효과적입니다.