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

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

이 글의 핵심

C++ Concepts와 Constraints 완벽 가이드. 템플릿 타입에 대한 명시적 제약을 정의하고, 명확한 에러 메시지를 제공합니다. SFINAE보다 간결하고 읽기 쉽습니다.

들어가며

C++20의 Concepts템플릿 타입에 대한 명시적 제약을 정의하는 기능입니다. SFINAE보다 간결하고, 에러 메시지가 명확합니다.

비유로 말씀드리면, Concepts입장권 조건을 명확히 적어 놓은 것에 가깝습니다. “18세 이상”이라고 적으면, 17세가 입장하려 할 때 “나이 조건 불만족”이라고 명확히 알려줍니다. SFINAE는 “조건 불만족”이라고만 하고 무엇이 문제인지 알기 어렵습니다.

이 글을 읽으면

  • Concepts의 개념과 사용법을 이해합니다
  • 표준 Concepts와 커스텀 Concepts를 익힙니다
  • requires 절과 requires 표현식을 파악합니다
  • SFINAE와의 차이를 확인합니다

목차

  1. Concepts란?
  2. 실전 구현
  3. 고급 활용
  4. 성능 비교
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

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

다음 단계

  • Concepts 기초: C++20 Concepts
  • 커스텀 Concepts: C++ 커스텀 Concepts
  • Template Lambda: C++ Template Lambda

참고 자료

한 줄 정리: Concepts는 템플릿 타입 제약을 명시적으로 정의하고 명확한 에러 메시지를 제공하여, SFINAE보다 간결하고 읽기 쉬운 코드를 작성할 수 있게 한다.


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

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

  • C++20 Concepts | 템플릿 에러 메시지를 읽기 쉽게 만드는 방법
  • C++ 커스텀 Concepts 작성 | 도메인에 맞는 제약 조건 정의하기 [#22-2]
  • C++ Template Lambda | “템플릿 람다” 가이드

관련 글

  • C++ 커스텀 Concepts 작성 | 도메인에 맞는 제약 조건 정의하기 [#22-2]