C++ template - 1. Basics

JunTak Lee·2023년 5월 17일
0

C++ Template

목록 보기
1/8

C++에서 template의 역할은 무엇일까
가장 좋은 교본중 하나인 CppReference를 들어가보자
보통 블로그에서 모양자, 볼펜 뭐 이딴걸로 설명하는데 별로 좋아하는 설명은 아니다
https://en.cppreference.com/w/cpp/language/templates

Reference에 따르면 template은 5가지 일을 한다고 명시되어 있다

  • Class template
  • Function template
  • Alias template
  • Variable template
  • Constraints and concepts

벌써부터 아찔하다..
하나씩 차근차근 보도록하자


Class template

A class template defines a family of classes
https://en.cppreference.com/w/cpp/language/class_template

근본중에 근본이라 할 수 있다
애초에 template이 생겨난 이유가 무엇이었던가
Generic Programming을 위해서 생겨난 놈이 아니던가
그냥 class/struct/union 앞에 붙이기만 하면 된다

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

template <typename T>
struct foo_struct {
    //...
};

template <typename T>
union foo_union {
    //...
};

int main() {
    foo_class<int> foo_int;
    foo_class<double> foo_double;
}

흥미로운 사실은 union도 template을 사용할 수 있다는 것이다
https://stackoverflow.com/questions/20743582/can-union-be-templated

스오플 2번째 답변이 흥미로운데, 가끔 data serialization할 일이 생기기 마련이다
물론 다른 방법도 충분히 많이 존재하지만, 이러한 방법은 상당히 신선하게 느껴졌다

Nested Class

다시 reference로 돌아와, 옆에 nested class라는 것이 보인다
class/struct/union안에 존재하는 class/struct/union을 nested class라 부르고, 이 역시 template을 쓸 수 있다

template <typename T>
class foo_class {
public:
    template <typename TT>
    union nested_class {
        //...
    };
};

int main() {
    foo_class<int>::nested_class<double> something;
}

Function template

A function template defines a family of functions
https://en.cppreference.com/w/cpp/language/function_template

그냥 근본2다
별 다를건 없고, 그냥 적당히 선언해놓고 가져다가 쓰면 된다

template <typename ParamT, typename RetT>
RetT foo(ParamT a) {
    return static_cast<RetT>(a);
}

int main() {
    double a = foo<int, double>(5);
}

굳이 Return type이나 Parameter type에서 안쓰고 function 안에서 사용해도 무방하다
아래는 다소 억지스러운 예시를 들어본 경우다

template <typename T, typename RetT>
RetT foo(double a) {
    T tmp = static_cast<T>(a);
    return static_cast<RetT>(tmp);
}

int main() {
    double a = foo<int, double>(5.3);
}

Member function

Reference에서 member function에서도 쓸 수 있다고 적혀있다
사실 별 다른 내용이 존재하는건 아니고, 말 그대로 member function에서도 template을 쓸수있다
당연할수도 있겠지만, constructor나 operator overloading에서도 사용할 수 있다

class foo {
public:
    template <typename T>
    foo(const T& val) {
        //...
    }

    template <typename T>
    void some_mem_fn(T val) {
        //...
    }

    template <typename T>
    int operator[](const T& val) {
        return 0;
    }
};

int main() {
    foo f(1);
    f.some_mem_fn<int>(1);
    return f[1];
}

위 예제는 사실 Constructor에서 template을 사용하면서 type deduction이 사용되었다
이건 지금 당장 다루기보다는 auto와 엮어서 다루고 싶기에 미뤄둔다

지금 다루고 싶은 내용은 class 밖에서 function을 정의하고 싶을때이다
뭐 사실 function template 사용할때랑 동일하게 사용하면 되는데, 이게 class에도 template이 붙어있으면 살짝 머리가 아파온다
근데 정말 별거 없고 그냥 template 두번 쓰면 된다

template <typename T>
class foo {
public:
    template <typename TT>
    foo(const TT& val);

    template <typename TT>
    void some_mem_fn(TT val);

    template <typename TT>
    int operator[](const TT& val);
};

template <typename T>
template <typename TT>
foo<T>::foo(const TT& val) {
    //...
}

template <typename T>
template <typename TT>
void foo<T>::some_mem_fn(TT val) {
    //...
}

template <typename T>
template <typename TT>
int foo<T>::operator[](const TT& val) {
    return 0;
}


int main() {
    foo<double> f(1);
    f.some_mem_fn<int>(1);
    return f[1];
}

Template을 얼마나 썼다고 벌써부터 코드 읽기가 힘들어질려고 한다
그래서하는 말이지만, template 쓸때 위처럼 대충 T나 TT쓰는 습관은 좋지 않다
ContainerT, ReturnT와 같이 명시적인 표현으로 작성하는 것이 더 좋다
더 나아가 일반적으로 많이 사용되는 표현을 사용하는 것이 더 좋다
예를들어 나는 libcxx을 흉내내는 것을 좋아하고 선호한다


Alias template

Alias template is a name that refers to a family of types
https://en.cppreference.com/w/cpp/language/type_alias

예전에 어떤 블로그에서 이렇게 소개하는걸 본적이 있다
'typedef는 template을 사용할 수 없습니다. 대신 using을 사용하면 template을 사용할 수 있습니다'

여기서 말하는 using 키워드가 Type alias이고, 거기에 template을 사용하면 alias template이 되는 것이다
using이라는 키워드는 type alias 말고도 여러가지 일을 하는데, 글과는 조금 거리가 있어서 안다룰 생각이다
어쨌든, using을 쓰면 다음과 다음과 같은 문법이 가능하다

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

//template <typename T>
//typedef foo<T> foo2       ERROR!

template <typename T>
using foo2 = foo<T>;        // OK!

int main() {
    foo2<int> a;
}

coding하다보면 느끼는건데 template parameter가 여러개 달릴때 그 중 일부를 고정시키면 상당히 편하다
이게 typedef에서는 안되서 template parameter가 주렁주렁 달린 code를 뚫어져라 쳐다보고 있으면 진짜 아찔하다
따라서 과거의 나처럼 똥고집 부리면서 typedef만 쓰지 말고 using을 적극 활용하기 바란다

template <typename _1, typename _2, typename _3>
class foo {
    //...
};

template <typename T>
using specified_foo = foo<T, int, double>;

int main() {
    specified_foo<float> something;
}

Variable template

A variable template defines a family of variables or static data members
https://en.cppreference.com/w/cpp/language/variable_template

C++14에서 도입된 놈인데 variable에도 template을 쓸 수 있다
사용방법은 아래와 같이 사용하면 된단다
아 물론 constexpr 써도 된다

template <typename T>
static const T pi = static_cast<T>(3.14);

int main() {
    return pi<int>;
}

도대체 이게 왜 필요한가 싶을 수 있는데 TMP를 공부하다보면 type traits란 걸 마주한다
일반적으로 type traits는 struct로 구현하기에 ~::value 혹은 ~::type와 같이 사용한다
~::type는 앞서 말했던 Type alias template으로 뒤에 ::type을 생략할 수 있다
근데 ~::value는 말그대로 value이기에 Type alias template으로 생략이 안된다
이때 variable template을 쓰면 ::value을 생략할 수 있다

#include <type_traits>

template <typename _1, typename _2>
inline constexpr bool is_same = std::is_same<_1, _2>::value;

int main() {
    return is_same<int, int>;
}

Instantiation

이 글을 적으면서 한가지 더 집고 넘어가고 싶은게 있다
CppReference에서 Class template과 Function template 부분에 공통적으로 적혀있는 내용이다

Class/Function Template 자체는 type도, class/function도, 그 어떤 entity도 아니다. Template 정의만 가지고있는 경우 어떤 Code도 생성되지 않는다. Code를 생성시키기 위해서는 template은 instantiation이 되어야한다

내맘대로 해석해본 cppreference

이게 무슨 말인가하면, instance화 되기 전까지 그 어떤 Code도 생성되지 않는다는 것이다
그러니까 쉽게 말해 우리가 type을 구체화하기 전까지는 아무 의미없는 code라는 것이다
뭔가 말하고 나니까 더 복잡해보이기에 예를들어 생각해보자

template <typename T>
void foo(const T& val) {
    //...
}

int main() {
    
}

위 코드에서 생성되는 code는 어떤게 있을까
정답은 아무것도 없다이다

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

우리는 단한번도 foo의 template T를 지정한 적이 없기 때문이다
여기까지 이해했다면 아래 두가지를 머리속에 넣고 다음 Code를 보자

  • static_assert는 안의 값이 true가 아닐경우 error을 만든다
  • std::is_same_v는 두 type이 같으면 true를 뱉어내는 놈이다
#include <cassert>
#include <type_traits>

template <typename T>
void foo(const T& val) {
    static_assert((std::is_same_v<int, float>));
}

int main() {

}

template이 없었다면, 당연하게도 error을 호출해야하는 code이다
그런데 compile이 멀쩡하게 잘 된다
Compiler에 문제가 있는게 아니라, Instantiation이 얼어나지 않아 생성된 Code가 없기 때문이다
그런데 만약 한가지 경우라도 Instantiation이 일어나면 어떻게 될까

#include <cassert>
#include <type_traits>

template <typename T>
void foo(const T& val) {
    static_assert((std::is_same_v<int, float>));
}

int main() {
    foo<int>(5);
}

// Compiler Output
<source>: In instantiation of 'void foo(const T&) [with T = int]':
<source>:10:13:   required from here
<source>:6:25: error: static assertion failed
    6 |     static_assert((std::is_same_v<int, float>));
      |                    ~~~~~^~~~~~~~~~~~~~~~~~~~~
<source>:6:25: note: 'std::is_same_v<int, float>' evaluates to false
ASM generation compiler returned: 1
<source>: In instantiation of 'void foo(const T&) [with T = int]':
<source>:10:13:   required from here
<source>:6:25: error: static assertion failed
    6 |     static_assert((std::is_same_v<int, float>));
      |                    ~~~~~^~~~~~~~~~~~~~~~~~~~~
<source>:6:25: note: 'std::is_same_v<int, float>' evaluates to false
Execution build compiler returned: 1

바로 빨간줄이 뜬다

What is wrong with this code

그래서 이게 왜 문제가 되는걸까
"왜"라는 궁금증을 해결하기 위해 template의 역할을 다시 상기시켜보자
template에는 다양한 type이라도 들어갈 수 있다

그렇다면 우리가 의도한 방향과 다른 type이 들어간다면 어떻게 될까

class bar {
    void some_mem_fn() {
        //...
    }
};

class bar2 {};

template <typename T>
void foo(const T& val) {
    val.some_mem_fn();
}

int main() {
    
}

foo의 Parameter val에 bar Type의 Instance를 넣는다면 전혀 문제 될게 없다
그런데 아직 Code를 재대로 이해하지 못한 사람이 boo Type Instance를 넣는다면 어떨까
우선 compile이 안될거다
그리고 이걸 고치려들꺼다

이 사람에게는 두자기 선택지가 존재한다

  • bar2 type이 아니라 bar type으로 고쳐서 넣는다
  • bar2 class를 수정한다
  • code짠 사람한테 연락해서 "응애해줘"를 시전한다

이게 큰 문제가 아니라고 생각할 수 있다
저런 단순한 Code는 이해하기 쉽고 고치기도 쉬우니까
문제는 Template로 떡칠된 Code를 맞딱드릴때이다
이해하기도 힘들고 debugging은 더더욱이 어려운 code에서 저런 상황은 악몽과도 같다

지금은 이런 문제점들을 해결하고자 다양한 해결법이 존재한다
그런데 이러한 해결법들조차 template을 알아야 가능하다


무언가를 새로 배울때 어디에 사용되는지를 아는 것도 중요하다고 생각한다
그렇기에 간단하게 나마 요약해보았다
사실 concept를 안적긴 했는데 따로 다루고 싶어서 일단은 빼놓았다

Instantiation을 다루면서 꼭하고 싶은 말이 한가지 있었다
우리가 Instantiation한 경우에 대해서만 Code가 생성된다는 것을 항상 기억해야 한다
약간 말을 바꿔서 두가지로 생각을 해보면 다음과 같을 것이다

  • 지금 당장 Compile이 되어도, Template에 다른 Type이 들어가면 Compile이 안될수 있다
  • 지금 당장에 Compile이 되더라도, 우리가 의도한 방향으로 동작하지 않을 수 있다

옛말에 이런말이 있다

Expect the Unexpected

모두가 나와 같은 생각을 하지않는다
그렇다고 나몰라라하는 것은 굉장히 안일한 생각이다
따라서 우리는 위 말처럼 Unexpected를 예측하고 방어해야한다

profile
하고싶은거 하는 사람

0개의 댓글