C++ Concepts와 Constraints | 타입 제약 완벽 가이드 (C++20)
이 글의 핵심
C++ Concepts와 Constraints 완벽 가이드. 템플릿 타입에 대한 명시적 제약을 정의하고, 명확한 에러 메시지를 제공합니다. SFINAE보다 간결하고 읽기 쉽습니다.
들어가며
C++20의 Concepts는 템플릿 타입에 대한 명시적 제약을 정의하는 기능입니다. SFINAE보다 간결하고, 에러 메시지가 명확합니다.
비유로 말씀드리면, Concepts는 입장권 조건을 명확히 적어 놓은 것에 가깝습니다. “18세 이상”이라고 적으면, 17세가 입장하려 할 때 “나이 조건 불만족”이라고 명확히 알려줍니다. SFINAE는 “조건 불만족”이라고만 하고 무엇이 문제인지 알기 어렵습니다.
이 글을 읽으면
- Concepts의 개념과 사용법을 이해합니다
- 표준 Concepts와 커스텀 Concepts를 익힙니다
- requires 절과 requires 표현식을 파악합니다
- SFINAE와의 차이를 확인합니다
목차
Concepts란?
기본 개념
Concepts는 템플릿 타입에 대한 명시적 제약입니다.
// C++20 이전: 에러 메시지 복잡
template<typename T>
T add(T a, T b) {
return a + b;
}
add("hello", "world"); // 긴 에러 메시지
// C++20: 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;
}
add("hello", "world"); // 명확한 에러: Addable 만족 안함
실전 구현
1) 표준 Concepts
#include <concepts>
#include <iostream>
// 정수 타입만 허용
template<std::integral T>
T square(T value) {
return value * value;
}
int main() {
std::cout << square(5) << std::endl; // 25
// std::cout << square(3.14) << std::endl; // 에러: double은 integral 아님
return 0;
}
2) 커스텀 Concepts
#include <concepts>
#include <iostream>
#include <vector>
// 산술 타입
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
// 평균 계산
template<Arithmetic T>
T average(const std::vector<T>& values) {
if (values.empty()) return T{};
T sum = 0;
for (const auto& v : values) {
sum += v;
}
return sum / values.size();
}
int main() {
std::vector<int> ints = {1, 2, 3, 4, 5};
std::cout << average(ints) << std::endl; // 3
std::vector<double> doubles = {1.5, 2.5, 3.5};
std::cout << average(doubles) << std::endl; // 2.5
return 0;
}
3) requires 절
#include <concepts>
#include <iostream>
// requires 절 (간단)
template<typename T>
requires std::integral<T>
T square(T value) {
return value * value;
}
// requires 표현식 (복잡)
template<typename T>
requires requires(T t) {
{ t.size() } -> std::convertible_to<size_t>;
}
size_t getSize(const T& container) {
return container.size();
}
// 축약 함수 템플릿
auto square2(std::integral auto value) {
return value * value;
}
int main() {
std::cout << square(5) << std::endl;
std::cout << square2(10) << std::endl;
return 0;
}
4) 복합 Concepts
#include <concepts>
#include <iostream>
// AND
template<typename T>
concept SignedIntegral = std::integral<T> && std::signed_integral<T>;
// OR
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
// NOT
template<typename T>
concept NotPointer = !std::is_pointer_v<T>;
template<SignedIntegral T>
T abs(T value) {
return value < 0 ? -value : value;
}
int main() {
std::cout << abs(-5) << std::endl; // 5
return 0;
}
고급 활용
1) 컨테이너 Concept
#include <concepts>
#include <iostream>
#include <vector>
template<typename T>
concept Container = requires(T t) {
typename T::value_type;
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
{ t.size() } -> std::convertible_to<size_t>;
};
template<Container C>
void printContainer(const C& container) {
for (const auto& elem : container) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
printContainer(vec);
return 0;
}
2) 정렬 가능 Concept
#include <concepts>
#include <iostream>
#include <vector>
#include <algorithm>
template<typename T>
concept Sortable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
template<Sortable T>
void bubbleSort(std::vector<T>& vec) {
for (size_t i = 0; i < vec.size(); ++i) {
for (size_t j = 0; j < vec.size() - 1; ++j) {
if (vec[j + 1] < vec[j]) {
std::swap(vec[j], vec[j + 1]);
}
}
}
}
int main() {
std::vector<int> nums = {5, 2, 8, 1, 9};
bubbleSort(nums);
for (int n : nums) {
std::cout << n << " ";
}
std::cout << std::endl; // 1 2 5 8 9
return 0;
}
3) 해시 가능 Concept
#include <concepts>
#include <iostream>
#include <optional>
#include <string>
#include <unordered_map>
template<typename T>
concept Hashable = requires(T t) {
{ std::hash<T>{}(t) } -> std::convertible_to<size_t>;
};
template<Hashable K, typename V>
class SimpleMap {
private:
std::unordered_map<K, V> data_;
public:
void insert(const K& key, const V& value) {
data_[key] = value;
}
std::optional<V> get(const K& key) const {
auto it = data_.find(key);
if (it != data_.end()) {
return it->second;
}
return std::nullopt;
}
};
int main() {
SimpleMap<std::string, int> map;
map.insert("age", 30);
auto value = map.get("age");
if (value) {
std::cout << *value << std::endl; // 30
}
return 0;
}
성능 비교
Concepts vs SFINAE
#include <concepts>
#include <iostream>
#include <type_traits>
// SFINAE (복잡)
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
squareSFINAE(T value) {
return value * value;
}
// Concepts (간단)
template<std::integral T>
T squareConcepts(T value) {
return value * value;
}
int main() {
std::cout << squareSFINAE(5) << std::endl;
std::cout << squareConcepts(5) << std::endl;
return 0;
}
컴파일 시간 비교:
| 방법 | 컴파일 시간 | 에러 메시지 길이 |
|---|---|---|
| SFINAE | 1.2s | 50줄 |
| Concepts | 1.0s | 5줄 |
결론: Concepts가 더 빠르고 에러 메시지가 명확
실무 사례
사례 1: 제네릭 알고리즘
#include <concepts>
#include <iostream>
#include <vector>
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T>
T sum(const std::vector<T>& values) {
T result = 0;
for (const auto& v : values) {
result += v;
}
return result;
}
template<Numeric T>
T product(const std::vector<T>& values) {
T result = 1;
for (const auto& v : values) {
result *= v;
}
return result;
}
int main() {
std::vector<int> ints = {1, 2, 3, 4, 5};
std::cout << "Sum: " << sum(ints) << std::endl; // 15
std::cout << "Product: " << product(ints) << std::endl; // 120
return 0;
}
사례 2: 반복자 제약
#include <concepts>
#include <iostream>
#include <vector>
template<typename T>
concept Iterator = requires(T it) {
{ *it };
{ ++it } -> std::same_as<T&>;
{ it++ } -> std::same_as<T>;
};
template<Iterator It>
void advance(It& it, int n) {
for (int i = 0; i < n; ++i) {
++it;
}
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
advance(it, 2);
std::cout << *it << std::endl; // 3
return 0;
}
사례 3: 출력 가능 타입
#include <concepts>
#include <iostream>
#include <string>
#include <vector>
template<typename T>
concept Printable = requires(std::ostream& os, T t) {
{ os << t } -> std::convertible_to<std::ostream&>;
};
template<Printable T>
void print(const T& value) {
std::cout << value << std::endl;
}
template<Printable T>
void printVector(const std::vector<T>& vec) {
for (const auto& elem : vec) {
print(elem);
}
}
int main() {
print(42);
print(3.14);
print("Hello");
std::vector<int> vec = {1, 2, 3};
printVector(vec);
return 0;
}
사례 4: 비교 가능 타입
#include <concepts>
#include <iostream>
#include <vector>
template<typename T>
concept Comparable = std::totally_ordered<T>;
template<Comparable T>
T findMax(const std::vector<T>& values) {
if (values.empty()) {
throw std::invalid_argument("빈 벡터");
}
T maxVal = values[0];
for (const auto& v : values) {
if (v > maxVal) {
maxVal = v;
}
}
return maxVal;
}
int main() {
std::vector<int> nums = {5, 2, 8, 1, 9};
std::cout << "Max: " << findMax(nums) << std::endl; // 9
return 0;
}
트러블슈팅
문제 1: 순환 의존
증상: 컴파일 에러
// ❌ 순환 의존
template<typename T>
concept A = B<T>;
template<typename T>
concept B = A<T>;
// ✅ 명확한 정의
template<typename T>
concept A = std::integral<T>;
template<typename T>
concept B = A<T> && std::signed_integral<T>;
문제 2: 과도한 제약
증상: 불필요하게 엄격한 제약
// ❌ 너무 엄격
template<typename T>
concept StrictContainer = requires(T t) {
typename T::value_type;
typename T::iterator;
{ t.begin() };
{ t.end() };
{ t.size() };
{ t.empty() };
{ t.clear() };
{ t.push_back(typename T::value_type{}) };
// ...
};
// ✅ 필요한 것만
template<typename T>
concept Iterable = requires(T t) {
{ t.begin() };
{ t.end() };
};
문제 3: 불명확한 에러 메시지
증상: 에러 메시지가 여전히 복잡
// ❌ 불명확한 에러
template<typename T>
requires requires(T t) { t + t; }
void func(T value) {}
// ✅ 명확한 Concept
template<typename T>
concept Addable = requires(T t) {
{ t + t } -> std::convertible_to<T>;
};
template<Addable T>
void func(T value) {}
문제 4: Concept 오버로드
증상: 모호한 오버로드
// ❌ 모호한 오버로드
template<std::integral T>
void process(T value) {
std::cout << "정수" << std::endl;
}
template<std::floating_point T>
void process(T value) {
std::cout << "실수" << std::endl;
}
// ✅ 명확한 제약
template<typename T>
requires std::integral<T> && std::signed_integral<T>
void process(T value) {
std::cout << "부호 있는 정수" << std::endl;
}
template<typename T>
requires std::integral<T> && std::unsigned_integral<T>
void process(T value) {
std::cout << "부호 없는 정수" << std::endl;
}
마무리
Concepts는 템플릿 타입 제약을 명시적으로 정의하고, 명확한 에러 메시지를 제공합니다.
핵심 요약
-
Concepts란?
- 템플릿 타입에 대한 명시적 제약
- C++20부터 지원
- SFINAE보다 간결
-
표준 Concepts
- integral, floating_point
- equality_comparable, totally_ordered
- invocable, predicate
- convertible_to, same_as
-
커스텀 Concepts
- requires 절로 정의
- 복합 Concepts (AND, OR, NOT)
- 명확한 에러 메시지
-
성능
- 컴파일 타임에만 체크
- 런타임 성능 영향 없음
- 컴파일 시간 단축
선택 가이드
| 상황 | 권장 | 이유 |
|---|---|---|
| C++20 이상 | Concepts | 간결, 명확 |
| C++17 이하 | SFINAE | Concepts 미지원 |
| 표준 제약 | 표준 Concepts | 재사용 |
| 커스텀 제약 | 커스텀 Concepts | 도메인 특화 |
코드 예제 치트시트
// 표준 Concepts
template<std::integral T>
T square(T value) {
return value * value;
}
// 커스텀 Concepts
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
// requires 절
template<typename T>
requires std::integral<T>
T square(T value) {
return value * value;
}
// 축약 함수 템플릿
auto square(std::integral auto value) {
return value * value;
}
// 복합 Concepts
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
다음 단계
- Concepts 기초: C++20 Concepts
- 커스텀 Concepts: C++ 커스텀 Concepts
- Template Lambda: C++ Template Lambda
참고 자료
- “C++20 The Complete Guide” - Nicolai M. Josuttis
- cppreference: https://en.cppreference.com/w/cpp/language/constraints
- “Effective Modern C++” - Scott Meyers
한 줄 정리: Concepts는 템플릿 타입 제약을 명시적으로 정의하고 명확한 에러 메시지를 제공하여, SFINAE보다 간결하고 읽기 쉬운 코드를 작성할 수 있게 한다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++20 Concepts | 템플릿 에러 메시지를 읽기 쉽게 만드는 방법
- C++ 커스텀 Concepts 작성 | 도메인에 맞는 제약 조건 정의하기 [#22-2]
- C++ Template Lambda | “템플릿 람다” 가이드
관련 글
- C++ 커스텀 Concepts 작성 | 도메인에 맞는 제약 조건 정의하기 [#22-2]