[모던 C++ #7] SFINAE 트릭을 떠나 Concepts로!

과거 C++ 템플릿 프로그래밍에서 타입 제약을 표현하기 위해 사용되던 기법 중 하나가 바로 SFINAE(Substitution Failure Is Not An Error)였습니다. 이는 템플릿 인자의 대입 과정에서 타입이 맞지 않을 경우 에러 대신 다른 오버로드를 시도하는 기법으로, 조건부로 템플릿을 선택하는 메타프로그래밍 트릭이었습니다.

그러나 SFINAE는 코드가 복잡해지고, 컴파일 에러 메시지가 난해해진다는 단점이 있었습니다. 모던 C++20에서는 Concepts를 통해 이 문제를 해결했습니다. Concepts는 템플릿 인자에 대한 명확한 제약 조건을 선언적으로 표현할 수 있어 코드 가독성과 유지보수성, 그리고 에러 진단을 크게 개선합니다.

관련 참고 자료:

과거: SFINAE 트릭을 통한 템플릿 제약

SFINAE는 템플릿 인자 치환 시 발생하는 에러를 "치환 실패"로 처리하고 다른 오버로드를 탐색하게 하는 규칙을 활용하는 기법입니다. 이 기법을 사용하면 특정 타입 조건에 따라 템플릿 함수를 활성화하거나 비활성화할 수 있지만, 코드가 복잡해지고 이해하기 어려워집니다.

#include <type_traits>
#include <iostream>

// SFINAE를 활용한 예: T에 size_type 멤버 타입이 있는지 감지
template <typename T>
struct has_size_type {
private:
    template <typename U>
    static auto check(int) -> typename std::enable_if<!std::is_void<typename U::size_type>::value, std::true_type>::type;
    
    template <typename>
    static std::false_type check(...);
public:
    static constexpr bool value = decltype(check<T>(0))::value;
};

int main() {
    std::cout << has_size_type<std::string>::value << "\n"; // 1 (true)
    std::cout << has_size_type<int>::value << "\n"; // 0 (false)
    return 0;
}

위 예제는 has_size_type이라는 trait을 정의하기 위해 SFINAE를 사용한 것입니다. 이 코드는 복잡하고, 템플릿 에러 메시지도 난해합니다.

현재: Concepts를 통한 명확한 타입 제약

C++20에서 도입된 Concepts는 템플릿 매개변수의 타입 조건을 선언적으로 표현할 수 있는 기능을 제공합니다. 이를 통해 템플릿 코드가 훨씬 간결해지고, 에러 메시지도 명확해집니다.

#include <concepts>
#include <iostream>
#include <string>

// Concept 정의: T는 size_type이라는 멤버 타입이 있어야 한다.
template<typename T>
concept HasSizeType = requires { typename T::size_type; };

template<HasSizeType T>
void print_size_type() {
    std::cout << "T has a size_type\n";
}

int main() {
    print_size_type<std::string>(); // OK
    // print_size_type<int>(); // 컴파일 에러: int에 size_type 없음
    return 0;
}

concept 키워드를 사용해 HasSizeType이라는 조건을 간단히 정의하고, 이를 템플릿 인자에 직접 적용할 수 있습니다. 이 경우 int 타입을 전달하면 명확한 컴파일 에러 메시지를 통해 무엇이 문제인지 쉽게 알 수 있습니다.

Concepts와 의도 전달력

Concepts는 템플릿 인자가 충족해야 할 조건을 코드 상에서 직접 문서화합니다. 덕분에 협업 시 코드 리뷰나 유지보수가 쉬워지며, 템플릿 메타프로그래밍이 훨씬 접근하기 쉬워집니다.

또한 함수 오버로드 시, 어떤 템플릿이 선택되는지 명확히 기술할 수 있으므로, 복잡한 enable_if나 std::void_t 기반 SFINAE 코드를 읽을 필요가 줄어듭니다.

왜 이런 변화가 필요한가?

  1. 가독성 및 유지보수성 향상
    SFINAE 기반 코드는 복잡하고, 의도를 파악하기 어렵습니다. Concepts는 템플릿 조건을 선언적으로 표현하여, 코드의 목적을 명확히 드러냅니다.
  2. 명확한 에러 메시지
    SFINAE는 치환 실패 시 대안을 탐색하지만, 컴파일러 진단 메시지가 난해하게 표시될 때가 많습니다. Concepts를 사용하면 컴파일러가 "이 Concept를 만족하지 않는다"는 식으로 친절한 에러를 보여줍니다.
  3. 모던 C++ 철학 반영
    C++20의 Concepts는 언어 차원에서 템플릿 타입 제약을 지원하는 기능으로, 타이핑 안전성과 의도 표현력을 강화하는 모던 C++ 철학에 부합합니다.
반응형