C++ template - 3. Parameters and Arguments

JunTak Lee·2023년 5월 19일
0

C++ Template

목록 보기
3/8

C++에서 template은 굉장히 다양한 일을 수행한다
한번 스오플 형님들의 말을 들어보자
https://stackoverflow.com/questions/3937586/generic-programming-vs-metaprogramming

Generic Programming에서도 사용되는데 Metaprogramming에서 사용되기도 한다고 나와있다
둘다 굉장히 추상적이고 포괄적인 개념인데 이 둘다 수행한다고 한다
이렇게 많은 기능들을 단순히 Type만을 인자로 받아 모두 수행할 수는 없다
따라서 C++ template에서는 Argument로 다양한 값을 받는데, 이러한 부분을 짚고 넘어가려한다
원래는 CppReference와 비슷한 흐름으로 작성하려고 했다
근데 이게 순환참조마냥 서로 얽히고 얽혀있어서 그냥 내 방식대로 써본다


Type Template

많은 사람들이 생각하는 template이다
template안에 type이 들어가는 경우이다

#include <string>
#include <vector>

template <typename T>
class foo {
    //...
};

int main() {
    foo<int> foo_int;
    foo<double> foo_double;
    foo<std::string> foo_string;
    foo<std::vector<int>> foo_vec;
}

위 예시에서 int, double 뿐만 아니라 stringvector 모두 잘들어가는 것을 볼 수 있다
이때, STL vector의 경우 Generic Class이다
하지만 element의 type이 int라고 명시해주었기에 Complete Type이 되었고, parameter로 들어갈 수 있는 것이다

만약 int가 빠지면 어떻게 될까
아 참고로 대체적으로 clang쪽이 Error을 더 친절하게 알려줘서 clang 기반으로 설명했다

<source>:10:14: error: use of class template 'std::vector' requires template arguments
    foo<std::vector> foo_vec;
             ^
/opt/compiler-explorer/gcc-12.2.0/lib/gcc/x86_64-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/stl_vector.h:423:11: note: template is declared here
    class vector : protected _Vector_base<_Tp, _Alloc>
          ^
1 error generated.
ASM generation compiler returned: 1
<source>:10:14: error: use of class template 'std::vector' requires template arguments
    foo<std::vector> foo_vec;
             ^
/opt/compiler-explorer/gcc-12.2.0/lib/gcc/x86_64-linux-gnu/12.2.0/../../../../include/c++/12.2.0/bits/stl_vector.h:423:11: note: template is declared here
    class vector : protected _Vector_base<_Tp, _Alloc>
          ^
1 error generated.
Execution build compiler returned: 1

vector는 template arguments를 원하는데 내가 안넣었다고 알려준다
이처럼 Type template parameter는 말그대로 Complete Type을 Parameter로 넣는 경우를 말한다


Non-Type Template

여기서부터 생소하다고 느끼는 사람이 있을 수 있다
적어도 내가 사용해본 다른 언어에서는 이런걸 본적이 없기 때문이다
크게 어려운 개념은 아니고 말그대로 Type이 아닌 Template Parameter들을 지칭한다

Arithmetric type(integral, floating-point)

template에 숫자가 들어갈 수 있다
이 사실을 처음 접했을 당시에는 굉장한 충격이었는데, 아무튼 뭐 그렇다
원래는 정수형(char, int, long...)만 가능했었는데, C++ 20에 와서는 floating-point를 지원한다
따라서 이제 숫자라면 모두 template에 들어간다고 보면된다

template <int T>
class integral {
    //...
};

template <float T>
class floating_point {
    //...
};

int main() {
    integral<0> i;
    floating_point<0.1f> f;
}

따라서 이런 짓도 가능하다

template <int _1, int _2>
struct add {
    enum { value = _1 + _2 };
};

int main() {
    return add<1, 2>::value;
}

여기서 저 덧셈 연산은 compile time에 수행될까
(gcc13.1 --std=c++2b -O0)

main:
        push    rbp
        mov     rbp, rsp
        mov     eax, 3
        pop     rbp
        ret

그렇다
Optimization level이 0인데도 compile time에 수행된다
이걸 이용해서 compile time에 연산을 수행하도록 프로그래밍하는게 바로 TMP, Template Metaprogramming이다

한가지 더 짚고 넘어가야할게 있다
CppReference를 잘 살펴보면, 한가지 조건이 달려있다
그건 바로 temporary object는 template parameter로 전달될 수 없다는 것이다
다음 예제를 살펴보자

template <int T>
class integral {
    //...
};

int main() {
    int int_val = 2;
    const int int_const = 5;
    constexpr int int_constexpr = 3;

    integral<0> i;
    // integral<int_val> i2;        error: no conversion to const expression
    integral<int_const> i3;
    integral<int_constexpr> i4;
}

보면 const나 constexpr 같은 const expression은 잘된다
하지만 변수인 int는 error을 출력한다
즉 변수는 template argument로 들어갈 수 없다는 뜻이다

Other types

다른 type들에는 이런게 있다고 한다

  • lvalue reference type
  • a pointer type
  • a pointer to member type
  • an enumeration type
  • std::nullptr_t
  • a literal class type(with some constraints)

(추가예정)


Template Template

아찔한 놈이 두번 들어간다
고로 2배 아찔하다고 보면 된다

이놈의 역할을 알아보기 위해서는 아까 Type Template의 예시를 다시 생각해볼 필요가 있다
거기에서 std::vector에 template argument를 안넣어 오류가 생겼었다
이놈은 바로 그런 자리에 쓰라고 만든놈이다

#include <vector>

template <
    template <typename, typename>
        typename Container
>
struct foo {
    //...
};

int main() {
    foo<std::vector> f;
}

잘 보면 std::vector가 2개의 Template Argument를 받고 있다
두번째 Argument는 allocator인데, 직역하자면 할당자로 메모리 관리하는 객체라고 생각하면 된다

여튼 돌아와서 이제 foo class는 다양한 Template Class를 Template Argument로 받을 수 있다

#include <vector>
#include <deque>
#include <map>

template <
    template <typename...>
        typename Container
>
struct foo {
    //...
};

int main() {
    foo<std::vector> f_vec;
    foo<std::deque> f_deque;
    foo<std::map> f_map;
    //...
}

저기서 ...은 Parameter Pack으로 나중에 설명할 녀석이다
이걸 사용하는 이유는 std::vectorstd::deque는 Template Arguement가 2개 들어가는 반면 std::map은 4개 들어가기 때문이다
따라서 위처럼 3개의 Type을 모두 사용하고 싶다면 Parameter Pack을 사용해야 한다

마지막으로 위 예시들을 잘보면 template template 안에는 typename을 명시하지 않았다
명시를 해도 크게 문제가 되지는 않지만, 문제는 명시를 해도 못써먹는다
애초에 당연한것이 template template을 사용하는 순간 template template parameter에 뭐가 들어가는지는 관심이 없기 때문이다
만약 이때 template template parameter에 뭐가 들어가는지 궁금하다면 이전 글에서 다루었던 Template Specialization을 써야한다

#include <vector>

template <
    typename T
>
struct foo {};

template <
    typename DataT,
    template <typename, typename>
        typename Container,
    typename AllocatorT
>
struct foo<Container<DataT, AllocatorT>> {
    //...
};

int main() {
    foo<std::vector<int>> f_vec;
}

혹은 DataType과 ContainerType을 분리하여 해결할수도 있다

#include <vector>

template <
    typename DataT,
    template <typename, typename>
        typename Container,
    typename Allocator = std::allocator<DataT>
>
struct foo {
    typedef Container<DataT, Allocator> ContainerT;

    //...
};

int main() {
    foo<int, std::vector> f_vec;
}

근데 이렇게되면 Complete Type하고 뭐가 다른건가 싶을 수 있다
그래서 예시를 다르게 들어보겠다

Template Template의 활용

foo라는 class에 container가 존재하고, 이 container에서 일부 데이터만 가져오고 싶다고 하자
근데 이때 container를 여러 종류로 가져오고 싶다면..?
당장에 생각나는 방법은 3가지 정도 있다

  • Parameter에 Container를 Reference로 넘겨서 받아온다
  • template으로 Return type을 지정해준다
    • template에 Complete Type을 넘긴다
    • template에 template template parameter을 사용한다

아래의 코드는 가장 마지막에 제시한 방법으로 구현한 결과이다

#include <iostream>
#include <vector>
#include <deque>

template <typename T>
struct foo {
    typedef std::vector<T>                      container_type;
    typedef typename container_type::iterator   iterator_type;

    container_type some_data;

    template <
        template <typename, typename>
            typename ContainerT,
        typename Allocator = std::allocator<T>
    >
    ContainerT<T, Allocator> 
    get_splited_data(iterator_type begin,
                     iterator_type end) {
        return ContainerT<T, Allocator>{begin, end};
    }
};

int main() {
    foo<int> f;
    f.some_data = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    
    auto splited_container 
        = f.get_splited_data<std::deque>(
            f.some_data.begin() + 2,
            f.some_data.end() - 3);

    for (auto& ele : splited_container)
        std::cout << ele << " ";
    std::cout << std::endl;
}

이런 코드를 처음 접한다면 이게 도대체 뭔가 싶을 것이다..코드가 정말 더럽다..
다 필요없고 우리가 주목해야하는 부분은 get_splited_data 함수를 호출하는 부분이다
보면 std::deque로 3번째 원소에서 6번째 원소까지 담아서 달라고하는 코드다

이게 그래서 도대체 뭐가 이득인가
동일한 동작을하는 다른 방식의 코드와 비교해보면 그 차이가 보인다
(Reference는 굳이 구현해보지 않았다 그거까지 하기엔 너무 귀찮다..)

#include <iostream>
#include <vector>
#include <deque>
#include <type_traits>

template <typename T>
struct foo {
    typedef std::vector<T>                      container_type;
    typedef typename container_type::iterator   iterator_type;

    container_type some_data;

    template <typename ContainerT>
    ContainerT  get_splited_data(iterator_type begin,
                                 iterator_type end) {
        static_assert(std::is_same_v<T, typename ContainerT::value_type>);
        return ContainerT{begin, end};
    }
};

int main() {
    foo<int> f;
    f.some_data = {1, 2, 3, 4, 5, 6, 7, 8, 9};

    auto splited_container 
        = f.get_splited_data<std::deque<int>>(
            f.some_data.begin() + 2,
            f.some_data.end() - 3);

    for (auto& ele : splited_container)
        std::cout << ele << " ";
    std::cout << std::endl;
}

template template 예시보다 조금?은 더 깔끔해진거 같다 아님말고
하지만 여기서 문제가 발생하는데, foo객체에 이미 Data Type이 Fixed 되어있다
그런데 template template을 사용하지 않아 Data Type을 다시 넘겨줘야했다(std::deque<int>)
그리고 무엇보다 이 Type이 Foo의 Type과 동일한지를 검사해줘야 한다(static_assert)
이러한 차이에서 본다면 어느쪽이 의미론적 측면이나 실수를 발생할 여지의 측면에서 더 좋아보이는가
이처럼 template template은 생각보다 유용하게 활용될 수 있다

profile
하고싶은거 하는 사람

0개의 댓글