본문 바로가기

진리는어디에/C++

[C++] 가변 인자 템플릿(Variadic template)

들어가며

C++ 11 이전 버전의 템플릿은 고정 된 갯수만을 사용할 수 있었지만, C++11부터 가변 개수의 템플릿 인자를 받을 수 있는 '가변 인자 템플릿(Variadic template)'이 추가 되었다.

이번 포스트에서는 쩜쩜쩜(...)으로 불리는 가변 인자 템플릿 문법과 파라미터 팩(Parameter pack)과 같은 주요 개념을 살펴 보도록 하겠다.

가변 인자 템플릿 문법 '...'

가변 인자 템플릿 = 쩜쩜쩜(...)

가변 인자 템플릿 문법의 가장 큰 특징은 템플릿 선언에 쩜쩜쩜(...)이 추가 된다는 것이다. 기존 템플릿 문법과 가변 인자 템플릿 문법을 비교해보도록 하겠다.

  • 기존 고정 갯수 템플릿 선언 :
typename | class 템플릿이름
  • 가변 인자 템플릿 선언 :
typename | class ... 템플릿이름

이 처럼 템플릿 파라미터에 '...' 를 사용하므로써 "템플릿 타입 인자의 갯수에 제한이 없다"라는 것을 나타낸다.

REMARK : ...의 정식 명칭은 영어로는 ellipsis operator, 한국어로는 말줄임 연산자라고 읽을 수 있지만 본 포스트에서는 편의를 위해 쩜쩜쩜(...)이라고 읽도록 하겠다.

가변 인자 클래스 템플릿

기존 C++11 이전, 템플릿 클래스가 가능하듯, 가변 인자 템플릿 클래스 역시 가능하다. 아래 예제는 고정 템플릿 갯수를 가진 Pair 클래스와 가변 인자 템플릿이 적용된 Tuple 클래스를 보여주고 있다.

template <class T1, class T2> // 이전 버전 고정된 갯수의 템플릿
class Pair
{
public :
    T1 first;  // T1타입의 변수
    T2 second; // T2타입의 변수
};

template <class... Ts> // C++11이후 적용된 가변인자 템플릿. '...' 주목!!
class Tuple
{
    // 변수 선언이 없다??!!
};

int main()
{
    Pair<int, double> pair;
    
    Tuple<>    t1;  // 템플릿 인자가 없는 경우
    Tuple      t2;  // C++17부터는 템플릿 인자가 없다면 <>도 생략 가능하다
    Tuple<int> t3;
    Tuple<int, double> t4;  // Ts = int, double
}
C++ 11 까지는 타입인자를 하나도 보내지 않더라도 꺽쇠(<>)를 사용해야 했으나, C++17 부터는 타입 인자가 없는 가변 인자 템플릿인 경우 꺽쇠 기호를 생략 가능하도록 변경 되었다.

Pair 클래스는 두 개의 타입 인자를 사용하지 않는다면 컴파일 에러가 발생하지만, Tuple 클래스의 경우 인자 갯수에 상관 없이(심지어 타입 인자가 하나도 없더라도) 클래스 객체를 만들 수 있는 것을 확인할 수 있다.

REMARK : 위 예에서 Pair 클래스는 템플릿 타입을 이용해 멤버 변수를 생성하고 있지만 Tuple 클래스는 가변 템플릿 인자 Ts를 이용한 멤버 변수 선언이 없는 것을 주목하자.

C++에서 가변 인자 템플릿 클래스의 파라미터 팩(parameter pack) 자체를 직접 멤버 변수로 선언할 수는 없다. 왜냐하면 Ts... 같은 팩은 "타입 목록(type list)"이지 "변수 묶음"이 아니기 때문이다. 이에 대한 내용은 아래 'Parameter Pack' 섹션에서 자세히 다룰 예정이니 일단 그러려니 하고 진행하도록 한다.

가변 인자 함수 템플릿

클래스 뿐만 아니라 함수에도 가변 인자 템플릿 적용이 가능하다. 가변 인자 클래스 템플릿과 마찬가로 가변 인자 템플릿 함수 역시 'typename | class ... 템플릿이름'과 같은 문법이다.

노란색으로 마킹 된 부분 주목

위 예제에서 주목 해야 할 부분은 함수 인자를  부분에도 쩜쩜쩜(...) 이 사용 되었다는 것이다. 타입인자 뿐만 아니라, 실제 함수 인자들도 가변 갯수로 넘겨 받을 수 있다는 의미다.

template<class T1, class T2> // 이전 버전 고정 된 갯수의 템플릿
void f1(T1 arg1, T2 arg2)
{
}

template<class... Ts> // C++ 11 이후 적용된 가변 인자 템플릿
void f2(Ts... args) // 함수 인자도 가변이다. 가변 타입과 변수명 사이에 쩜쩜쩜(...) 추가
{
}

int main()
{
    // f1<int>(3); // error
    f1<int, double>(3, 3.14);
    
    f2<int>(3); // ok
    f2<int, double, char>(3, 3.14, 'A'); // Ts = int, double, char
                                         // args = 3, 3.14, 'A'
    
    return 0;
}

파라미터 팩(Parameter Pack)

앞선 클래스 템플릿 섹션에서 파라미터 팩(parameter  pack)이라는 용어가 언급 되었다. 이번 섹션에서는 가변 인자 템플릿으로 넘어오는 무엇인가인 '파라미터 팩'에 대해 알아 보도록 하겠다.

본격적인 설명전 위의 f2 가변 인자 템플릿 함수를 다시 살펴 보자.

template<class... Ts> // C++ 11 이후 적용된 가변 인자 템플릿
void f2(Ts... args) // 함수 인자도 가변이다
{
}

int main()
{
    f2<int, double, char>(3, 3.14, 'A'); // Ts = int, double, char
                                         // args = 3, 3.14, 'A'
    
    return 0;
}

컴파일러는 위 f2 템플릿 함수를 이용해 다음과 같은 실제 호출 될 함수를 생성 한다.

컴파일러가 생성한 함수를 살펴 보면 :

  • Ts 타입 파라메터에는 'int, double, char' 들어간다, 이를 template parameter pack 이라고 한다.
  • args 함수 인자는 __args0, __args2, __args2로 치환 되고, 각 '3, 3.14, A'가 들어 간다. 이를 function parameter pack 이라고 한다.

이렇게 타입 인자든, 함수 인자든 인자 리스트를 들고 있는 것을 팩(pack)이라고 하며, 템플릿 파라미터 팩, 함수 파라미터 팩을 합쳐서 파라미터 팩(parameter pack) 이라고 부른다.

팩 확장(Pack Expansion)

기존 템플릿 함수는 아래 예제 처럼 각 템플릿 타입을 명시적으로 지정하여 사용할 수 있었다.

template <class T1, class T2, class T3>
void f1(T1 arg1, T2 arg2, T3 arg3)
{
    std::cout << arg1 << ", " << arg2 << "," << arg3 << std::endl;
}

하지만 쩜쩜쩜(...)으로 표현 된 파라미터 팩은 일반적인 방법으로는 팩 안에 있는 타입 인자나 함수 인자에 접근 할 수 없다. 이번 섹션에서는 파라미터 팩 안의 인자들을 사용하는 방법인 팩 확장(pack expansion)에 대해 살펴 보도록 한다.

template <class ...Args>
void f2(Args... args)
{
    // args  = 1, 2, 3
    f1(args); // error C3520: 'args': parameter pack must be expanded in this context    
}

int main()
{
    f2(1, 2, 3);
    return 0;
}

위 예제에서 가변 인자 템플릿 함수 f2는 메인 함수로 부터 1, 2, 3을 받아 Args라는 파라미터 팩을 통해 넘겨 받고 있다.

이제 이 파라미터 팩 args를 그대로 f1에게 전달하게되면 어떻게 될까?

얼핏 생각하면 f2의 args 에는 1, 2, 3이 들어 있을테니, 세 개의 인자를 사용하는 f1에게 그대로 전달하면 될 것 같아 보인다. 하지만 위 예제는 ''args': parameter pack must be expanded in this context' 와 같이 parameter pack은 반드시 풀어서 사용되어야 한다는 에러를 발생하며 실패한다.

expansion을 한국어로 직역하면 확장이 되지만, 좀 더 어울리는 단어로 바꾸려고 하니 '풀어 놓다'라고 번역하게 되었다. 뭉쳐져 있는 꾸러미들을 풀어 놓는다는 의미로 받아들이면 된다.

팩 확장(pack expansion)은 파라미터 팩 안의 모든 요소들을 콤마(,)로 구분해 풀어 놓겠다는 그리고 파라미터 팩의 확장(팩을 풀어서 사용)은 단순히 팩 변수 뒤에 쩜쩜쩜(...)을 추가하기만 하면된다.

앞선 f2 예제의 args 뒤 에 쩜쩜쩜(...)만 붙여 주면 컴파일러는 아래 처럼 파라미터 팩 args를 __args0, __args1, __args2과 같이 각 값들을 콤마로 구분하여 생성한다.

/* 사용자에의해 작성된 코드 */
template<class ... Args>
void f2(Args... args)
{
    f1(args... ); // parameter pack 뒤에 ...를 붙임
}

/* 컴파일러에 의해 생성되는 실제 코드 from https://cppinsights.io/ */
#ifdef INSIGHTS_USE_TEMPLATEtemplate<>
void f2<int, int, int>(int __args0, int __args1, int __args2)
{
    f1(__args0, __args1, __args2); // 컴파일러는 __args0, 1, 2로 풀어서 코드를 생성한다.
}
#endif

팩 확장은 단순 pack 자체만 아니라 pack을 사용하는 패턴에도 적용 가능하다.

예를 들어 위, args 파라미터 팩 앞에 ++을 추가한다면, 컴파일러는 args 팩 확장 시 각 값들에 ++ 패턴을 적용해 ++__args0, ++__args1, ++__args2과 같은 코드를 생성한다.

// https://cppinsights.io/

/* 사용자에의해 작성된 코드 */
template<class ... Args>
void f2(Args... args)
{
    f1(args... ); // 비교를 위한 단순 팩 확장 호출
    f1(++args... ); // ++ 패턴을 적용해 호출
}

/* 컴파일러에 의해 생성되는 실제 코드 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void f2<int, int, int>(int __args0, int __args1, int __args2)
{
    f1(__args0, __args1, __args2);
    f1(++__args0, ++__args1, ++__args2); // 패턴이 적용 되어 확장 됨
}
#endif

심지어 단순 패턴 뿐만 아니라 함수를 사용하는 패턴도 가능하다. 파라미터 팩의 각 요소들을 확장 할 때,  각 요소들에 sqrt 함수를 적용하여 제곱 결과를 넘겨 주고 싶다고 가정하자. 그럼 아래와 같이 표현하는 것도 가능하다.

// https://cppinsights.io/

/* 사용자에의해 작성된 코드 */
template<class ... Args>
void f2(Args... args)
{
    f1(++args... );     // 비교를 위한 단순 팩 확장 호출
    f1(sqrt(args)... ); // 함수를 적용한 팩 확장 호출
}

/* 컴파일러에 의해 생성되는 실제 코드 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void f2<int, int, int>(int __args0, int __args1, int __args2)
{
    f1(++__args0, ++__args1, ++__args2);
    f1(sqrt(static_cast<double>(__args0)), sqrt(static_cast<double>(__args1)), sqrt(static_cast<double>(__args2)));
}
#endif

마치며

앞에서 살펴 본 내용들을 요약해보자.

  • C++ 11 부터 '가변 인자 템플릿'이 추가됨. 클래스, 함수 모두에 적용 가능. 쩜쩜쩜(...) 연산자를 사용한다. 
  • 파라미터 팩(parameter pack) : 가변 인자 템플릿에 의해 전달 되는 파라미터 정보.
  • 팩 확장(pack expansion) : 컴파일러는 파라미터 팩 안에 있는 모든 요소를 콤마(,)를 이용해 순서대로 나열하는 코드로 변경해 준다.

이상으로 가변 인자 템플릿(Variadic template)과 그것을 저장하는 Parameter Pack, 그리고 pack을 사용하는 방법인 Pack extension에 대해 알아 보았다.

이번 포스트에서는 다른 것 보다도 가변 인자 템플릿(variadic template), 파라미터 팩(parameter pack), 팩 확장(pack expansion)의 개념에 집중하도록 한다. 다음 포스트 [C++] Pack Expansion 에서는 파라미터 팩 확장에 대해 좀 더 자세히 다뤄 보도록 하겠다.

부록 1. 참조

 

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!