본문으로 건너뛰기 C++ Concepts와 Constraints | 타입 제약 완벽 가이드 (C++20)

C++ Concepts와 Constraints | 타입 제약 완벽 가이드 (C++20)

C++ Concepts와 Constraints | 타입 제약 완벽 가이드 (C++20)

이 글의 핵심

C++ Concepts와 Constraints: 타입 제약 Concepts란?·기본 사용법·커스텀 Concepts·requires 절.

들어가며

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;
}

컴파일 시간 비교:

방법컴파일 시간에러 메시지 길이
SFINAE1.2s50줄
Concepts1.0s5줄
결론: 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템플릿 타입 제약을 명시적으로 정의하며, 명확한 에러 메시지를 제공합니다.

핵심 요약

  1. Concepts란?
    • 템플릿 타입에 대한 명시적 제약
    • C++20부터 지원
    • SFINAE보다 간결
  2. 표준 Concepts
    • integral, floating_point
    • equality_comparable, totally_ordered
    • invocable, predicate
    • convertible_to, same_as
  3. 커스텀 Concepts
    • requires 절로 정의
    • 복합 Concepts (AND, OR, NOT)
    • 명확한 에러 메시지
  4. 성능
    • 컴파일 타임에만 체크
    • 런타임 성능 영향 없음
    • 컴파일 시간 단축

선택 가이드

상황권장이유
C++20 이상Concepts간결, 명확
C++17 이하SFINAEConcepts 미지원
표준 제약표준 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>;

다음 단계

참고 자료

  • “C++20 The Complete Guide” - Nicolai M. Josuttis
  • cppreference: https://en.cppreference.com/w/cpp/language/constraints
  • “Effective Modern C++” - Scott Meyers 한 줄 정리: Concepts는 템플릿 타입 제약을 명시적으로 정의하고 명확한 에러 메시지를 제공하여, SFINAE보다 간결하고 읽기 쉬운 코드를 작성할 수 있게 한다.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ Concepts와 Constraints: 타입 제약 완벽 가이드. Concepts란?·기본 사용법·커스텀 Concepts·requires 절로 흐름을 잡고 원리·코드·실무 적용을 한글로 정리합니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, concepts, constraints, requires, C++20 등으로 검색하시면 이 글이 도움이 됩니다.