C++

[C++11] 가변인자 템플릿을 이용한 Min, Max 함수 구현

aicosmos 2025. 5. 20. 21:14

출처 : 김경진님의 데브피아 (https://blog.naver.com/devmachine/220148995993)

 

std::min, std::max 함수를 사용하다보면...

 

두 오브젝트 간의 최소값, 최대값을 구하는 함수인 min, max 함수는 이미 C++ 표준에 포함되어 있고, 표준 min, max 함수가 없던 시절에도 #define MIN(x, y) ((x) < (y) ? (x) : (y)) 와 같은 매크로 함수로 만들어 사용해 왔습니다. 그런데 가끔은 두 개의 오브젝트가 아닌 세 개 이상의 오브젝트 간의 min, max 값을 구하고 싶은 경우가 있습니다. 이런 경우에는 보통 아래와 같은 형태로 구현하는데, 코드의 가독성이 그리 좋아보이지 않습니다(오브젝트 개수가 늘어날 수록 점점 더 지저분해지겠죠).

 

int x = 10, y = 20, z = 30;
int min_number = std::min(std::min(x, y), z);

 

 물론 위와 같은 방법 외에 다른 방법이 없는 것은 아닙니다. C++ 표준에서는 std::min_element, std::max_element 함수를 제공하기 때문에 컨테이너에 담겨있는 여러 개의 오브젝트들의 최소값, 최대값을 한 번에 뽑아낼 수 있습니다. 하지만 고작 몇 개의 오브젝트를 비교하기 위해 배열이나 벡터와 같은 컨테이너에 담아 파라미터로 전달하는 것 역시 꽤나 귀찮은 일이죠. 우리가 원하는 최적의 형태는 다음과 같은 형태일 것입니다.

 

int x = 10, y = 20, z = 30;
int min_number = Min(x, y, z);

 

위와 같이 파라미터의 개수와 상관없이 동작하는 Min, Max 함수를 만들려면 어떻게 해야할까요? 가장 먼저 생각해볼 수 있는 방법은 아래와 같이 Min, Max 함수의 파라미터 개수별 오버로드 함수를 모두 만들어 버리는 겁니다. 

 

template<typename T>
const T& Min(const T& x, const T& y);

template<typename T>
const T& Min(const T& x, const T& y, const T& z);

...

template<typename T>
const T& Min(const T& a, const T& b, const T& c, const T& d, const T& e, const T& f, const T& g);

 

아주 무식해보이는 방법이긴 하지만 C++11 이전에는 별다른 대안이 없었기 때문에 이런 식으로 구현할 수 밖에 없었습니다. std::tuple 클래스 역시 최대 10개의 원소를 지원하기 위해 이러한 노가다 코드를 구현했었죠. 하지만 C++11 에서는 가변인자 템플릿의 등장으로 더 이상 이러한 고민을 할 필요가 없게 되었습니다(하지만 아쉽게도 가변인자 템플릿은 Visual Studio 2013 버전부터 지원하기 때문에 이전 버전의 컴파일러에서는 컴파일 할 수 없습니다).

 

가변인자 템플릿을 이용한 Min, Max 함수 구현

 

어쩌다보니 서론이 너무 길어졌네요. 이번 포스팅은 가변인자 템플릿의 용법을 설명하려는 목적은 아니니, 이에 대한 자세한 내용이 궁금하신 분들은 구글 검색을 통해 확인하시기 바랍니다. 그럼 바로 가변인자 템플릿을 이용하여 Min, Max 함수를 작성해보도록 하죠. 

 

#include <utility>

template <typename ...Args>
struct are_same;

template <typename T>
struct are_same<T>
{
    enum { value = true };
};

template <typename T, typename... Args>
struct are_same<T, T, Args...>
{
    enum { value = are_same<T, Args...>::value };
};

template <typename T1, typename T2, typename... Args>
struct are_same<T1, T2, Args...>
{
    enum { value = false };
};

template<typename T>
const T& Max(const T& left, const T& right)
{
    return left < right ? right : left;
}

template<typename T, typename... Args>
const T& Max(const T& left, const Args&... args)
{
    static_assert(are_same<T, Args...>::value, "Types are different");

    const T& right = Max(args...);
    return left < right ? right : left;
}

template<typename T>
const T& Min(const T& left, const T& right)
{
    return left < right ? left : right;
}

template<typename T, typename... Args>
const T& Min(const T& left, const Args&... args)
{
    static_assert(are_same<T, Args...>::value, "Types are different");

    const T& right = Min(args...);
    return left < right ? left : right;
}

 

 are_same 구조체는 일단 제껴두고 Min, Max 함수부터 살펴보면 가변인자 템플릿의 전형적인 구현이라고 볼 수 있습니다. 가변인자 함수가 재귀적으로 호출되면서 가변인자가 아닌 함수를 만나 재귀가 종료되는 형태죠. Min, Max 함수는 가변인자 템플릿 구현을 연습해보기 딱 좋은 예제였던 것 같습니다. 

 

구현하면서 한 가지 막혔던 부분은 '가변인자에 서로 다른 타입의 오브젝트가 전달되었을 때 이를 어떻게 걸러낼 것인가' 에 대한 것이었습니다. 이 문제는 메타 프로그래밍으로 해결할 수 있을 것 같아 검색해보니 역시나 스택 오버플로우에 원하는 해결책이 있었습니다. 바로 are_same 과 같은 구조체를 만들어 타입이 다를 경우 컴파일 시점에 오류를 발생시키는 것이죠. are_same 구조체 역시 가변인자 템플릿을 사용하여 구현하는데, 재귀적으로 차근 차근 따라가다 보면 어떤 원리로 동작하는지 이해가 되실겁니다.

 

위에서 작성한 Min, Max 함수는 다음과 같이 사용할 수 있으며, 가변인자 템플릿으로 구현했기 때문에 파라미터 개수의 제한은 없습니다. 단, 마지막 라인처럼 서로 다른 타입의 파라미터를 전달하였을 경우에는 컴파일 오류가 발생하게 됩니다.

 

int x = 10, y = 20, z = 30;
int min_number = Min(x, y, 15, z - y);

double dx = 0.1, dy = -0.5;
double max_number = Max(dx, dy, fabs(dy), dx + dy);

// compile error(Types are different)
Max(x, y, dx, dy);

 

마지막으로 Min, Max 함수만 만들기에는 조금 아쉬워 최소값과 최대값을 동시에 리턴해주는 MinMax 함수도 만들어보았습니다. 가변인자를 사용한다는 것을 제외하면 MinMax 함수는 std::minmax 함수와 유사하게 동작합니다.

 

template<typename T>
std::pair<const T&, const T&> MinMax(const T& left, const T& right)
{
    return (left < right
        ? std::pair<const T&, const T&>(left, right)
        : std::pair<const T&, const T&>(right, left));
}

template<typename T, typename... Args>
std::pair<const T&, const T&> MinMax(const T& left, const Args&... args)
{
    static_assert(are_same<T, Args...>::value, "Types are different");

    auto result = MinMax(args...);

    return std::pair<const T&, const T&>(
        left < result.first ? left : result.first,
        result.second < left ? left : result.second);
}

template<typename T>
std::pair<T, T> MinMaxC(const T& left, const T& right)
{
    return (left < right
        ? std::pair<T, T>(left, right)
        : std::pair<T, T>(right, left));
}

template<typename T, typename... Args>
std::pair<T, T> MinMaxC(const T& left, const Args&... args)
{
    static_assert(are_same<T, Args...>::value, "Types are different");

    auto result = MinMaxC(args...);

    return std::pair<T, T>(
        left < result.first ? left : result.first,
        result.second < left ? left : result.second);
}

 

 참고로 MinMaxC 함수는 결과값을 pair 에 담을 때 값을 복사해서 담는다는 것 이외에는 MinMax 함수와 동일합니다. R-value 파라미터를 전달할 경우에는 결과값을 복사하는 MinMaxC 함수를 호출하는 것이 좋겠죠.

 

저도 가변인자 템플릿을 처음 활용해보는 거라 부족하거나 잘못된 부분이 있을지 모르겠네요. 혹시 그런 부분이 있다면 댓글로 언제든지 말씀해주시기 바랍니다. 

 

 Reference

 

 

Stack Overflow - Variadic templates - incomplete type 구현|작성자 데브머신