본문 바로가기

진리는어디에/C++

[C++] 팩 확장(pack expansion) 적용 가능 컨텍스트

들어가며

오늘은 지난 [C++] 가변 인자 템플릿(Variadic template)에 이어 팩 확장(pack expansion)에 대해 더 깊게 알아 보는 시간을 가지도록 한다.

들어가기 앞서 팩 확장(pack expansion)이 무엇인지 다시 한번 요약해 보자 :

  • 파라미터 팩을 컨텍스트에서 사용하기 위해서는 확장(pack expansion)이 되어야 한다.
  • '팩 확장' 이란, 컴파일러가 파라미터 팩 안에 있는 내용들을 콤마로 구분하여 생성하는 것을 말한다.
  • 팩 확장을 위해서는 파라미터 팩 이름 뒤에 쩜쩜쩜(...)을 붙여 주면 된다.

그리고 ++ 또는 sqrt 함수를 이용해 패턴까지 포함하여 파라미터 팩을 확장하는 것 까지 살펴 보았다.

이번 포스트에서는 팩 확장이 적용되는 컨텍스트 목록에 대해 살펴 보도록 하겠다.

팩 확장 적용 가능 컨텐스트 7가지

확장이 발생하는 위치에 따라 콤마(,)로 구분된 목록은 함수 매개 변수 목록, 멤버 초기화 목록, 속성 목록등 다른 종류의 목록이 된다. 아래는 팩 확장이 허용 되는 컨텍스의 목록이다.

  1. Brace-enclosed initializers
  2. Function argument lists
  3. Parenthesized initializers
  4. Template argument lists
  5. Lambda captures
  6. Template parameter lists
  7. Base specifiers and member initializer list
REMARK : 일반적으로 parameter와 argument 모두 '인자'로 번역 되어 비슷한 의미로 사용되긴 하지만 여기에서는 두 용어를 구분할 필요가 있다.
Parameter는 정의할 때 사용되는 변수인 반면, Argument는 호출할 때 실제로 전달되는 값을 의미합니다. 

예를 들어 :
함수 f를 선언하기 위해 사용된 A, B, ...Args는 파라미터이며, std::tuple을 생성하기 위해 사용된 A, B, Args...는 아규먼트 라고 부른다.

template <class A, class B, class ... Args> // <- template parameter list
void f(A a, B, Args... args)
{
    std::tuple<A, B, Args...> t; // <- template argument list
}

1. 중괄호 초기화(Brace-enclosed initializers)

배열 초기화 또는 유니폼 초기화 처럼 중괄호({})를 사용하는 초기화에서 함수 파라메터 팩 확장을 사용할 수 있다.

template<class ... Args>
void f(const Args &... args)
{
    int arr[] = { args... };             // 배열을 초기화 하는 {} 괄호 안에서 팩 확장 시도
    std::vector<int> vec = { args... };  // 벡터를 초기화 하는 {} 괄호 안에서 팩 확장 시도
}

// 실제 컴파일러에 의해 생성 되는 코드 - https://cppinsights.io/
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void f<int, int>(const int & __args0, const int & __args1)
{
    int arr[2] = {__args0, __args1};
    std::vector<int, std::allocator<int> > vec = std::vector<int, std::allocator<int> >{std::initializer_list<int>{__args0, __args1}, std::allocator<int>()};
}
#endif

int main()
{
    f(1, 2);
}

2. 함수 호출 인자 리스트(Function argument lists)

함수 호출 시, 함수 인자 리스트 안에서 함수 파라메터 팩 확장 사용 가능

template<class ... Args>
void f(const Args &... args)
{
    int ret = std::min(args... ); // 함수의 인자로 팩 확장
}

// 실제 컴파일러에 의해 생성 되는 코드 - https://cppinsights.io/
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void f<int, int>(const int & __args0, const int & __args1)
{
    int ret = std::min(__args0, __args1);
}
#endif

int main()
{
    f(1, 2);
}

3. 소괄호 '()' 를 이용한 초기화

객체나 변수를 초기화하는 초기화 목록 내에서 함수 파라메터 팩을 확장할 수 있다.

template<class ... Args>
void f(const Args &... args)
{
    std::std::pair p(args... ); // () 를 이용한 객체 초기화
}

/* 컴파일러에 의해 생성 되는 코드 - https://cppinsights.io/ */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void f<int, int>(const int & __args0, const int & __args1)
{
    std::pair<int, int> p = std::pair<int, int>(__args0, __args1);
}
#endif

int main()
{
    f(1, 2);
}

4. 템플릿 아규먼트 리스트(Template argument lists)

아래 예제의 4라인 처럼 템플릿 클래스 인스턴스를 성하는 템플릿 아규먼트 리스트에도 팩 확장을 사용할 수 있다. 다만 앞선 섹션에서 다루었던 함수 파라미터 팩 대신 템플릿 파라미터 팩을 사용한다.

template<class ... Args>
void f(const Args &... args)
{
    // Args... = int, int. template parameter pack
    // args... = 1, 2. function parameter pack
    std::pair<Args...> p(args... );
}

/* 컴파일러에 의해 생성 되는 코드 - https://cppinsights.io/ */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
void f<int, int>(const int & __args0, const int & __args1)
{
    std::pair<int, int> p = std::pair<int, int>(__args0, __args1);
}
#endif

int main()
{
    f(1, 2);
}

5. 람다 캡쳐 리스트(Lambda captures)

아래 예제의 5라인 처럼, 람다 캡쳐 리스트 내에서도 파라미터 팩 확장을 사용할 수 있다. 람다 함수에 의해 생성된 코드 때문에 아래 예제가 복잡해 보일 수 있지만 캡처 리스트의 확장 된 팩의 각 값들이 __args0, __args1로 저장되었다가 operator에서 호출 되는 되는 것을 확인할 수 있다.

template <class ...Args>
void f(const Args&... args)
{
    // 람다 함수 캡처 리스트에 팩 확장 사용
    auto f1 = [args...] { return std::min(args...); };
}

/* 컴파일러에 의해 생성 되는 코드 - https://cppinsights.io/ */
ifdef INSIGHTS_USE_TEMPLATE
template<>
void f<int, int>(const int & __args0, const int & __args1)
{
    class __lambda_8_15
    {
    public: 
        inline /*constexpr */ int operator()() const
        {
            return std::min(__args0, __args1);
        }
    private: 
        const int __args0;
        const int __args1;
    public:
        __lambda_8_15(const int & ___args0, const int & ___args1)
            : __args0{___args0}
            , __args1{___args1}
        {}
    };
    
    __lambda_8_15 f1 = __lambda_8_15{__args0, __args1};
}

int main()
{
    f(1, 2);
}

6. 템플릿 파라미터 리스트(Template parameter lists)

템플릿 파라미터 팩은 다른 템플릿 파라미터 리스트에 사용 될 수 있다. 아래 처럼 Outer 클래스의 템플릿 파라미터 팩을 Inner 클래스를 선언하는 템플릿 파라미터 리스트에 사용 할 수 있다.

template<class ... Args>
class Outer
{
public:
    template<Args... args>
    class Inner
    {
    };
};

/* 컴파일러에 의해 생성 되는 코드 - https://cppinsights.io/ */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class Outer<int, int>
{
public:
    template<Args... ...args>
    class Inner;
    template<>
    class Inner<1, 2>
    {
    public: 
    // inline constexpr Inner() noexcept = default;
    };
};
#endif

int main()
{
    Outer<int, int>::Inner<1, 2> in;
}
REMARK : C++20에서 부터는 템플릿 인자 목록에 int 타입 뿐만 아니라 float, double과 같은 타입을 사용하는 것도 가능하다.

Outer<int, double>::Inner<1, 3.14> in; // ok

7. Base specifiers and member initializer list

팩 확장을 부모 클래스 목록에 적용하여 다중상속을 받을 수 있다.

class A {};
class B {};

template<class ... Ts>
class X : public Ts... // 템플릿 팩 확장을 이용해 다중 상속
{
public: 
    X(const Ts &... args) : Ts(args)... // 팩 확장을 이용해 클래스 멤버 초기화
    {
    }
};

/* 컴파일러에 의해 생성된 가변 인자 클래스 템플릿 - https://cppinsights.io/ */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class X<A, B> : public A, public B
{
    public: 
    inline X(const A & __args0, const B & __args1)
        : A(__args0)
        , B(__args1)
    {
    }
};

#endif

int main()
{
    A a;
    B b;
    X<A, B> x(a, b);
}

마치며

위 7가지 말고도 팩 확장을 허용하는 컨텍스트가 몇 종 더 있으나 거의 사용되지 않는 개변미아 굳이 여기서 까지 다루지는 않는다. 혹시 궁금하신 분들은 [여기]에서 확인 가능하다.

지금까지 내용을 요약 하면 :

  • 팩 확장을 허용하는 특정 컨텍스트가 있다
  • 주로 초기화나 함수 호출, 템플릿 선언 위치에서 가능하다.

부록 1. 같이 읽으면 좋은 글

 

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