C++ template - 6. Argument Deduction

JunTak Lee·2023년 6월 26일
0

C++ Template

목록 보기
8/8

cpp가 발전하면서 type deduction 또한 발전하였다
그리고 점차 더 다양한 분야에 적용되기 시작하였다
지금은 사실상 추론할 수 있는 모든 부분에 적용되었다 봐도 무방하다

그런데 문제는 모든 부분에서 동일한 규칙을 적용하기 어렵다는 것이다
그래서 각 분야에서 가장 자연스러운, 즉 일부분 변형된 규칙을 적용하기 시작했다
이 차이점들을 다 다룬다면 아마 배우고 글로 풀어내는데 몇달은 걸릴것이다

그래서 이번글은 작성하면서 최대한 template과 관련된 부분만 적기로 마음먹었다
아마 이 주제는 auto나 CTAD관련해서 몇번 더 다룰 것 같긴하다


Basics

template argument deduction이란건 어떨때 사용되는 것일까
이 부분을 이해하기 위해서는 template instantiation을 다시 볼 필요가 있다
CppReference의 argument deduction page에서도 해당 부분을 언급한다
https://en.cppreference.com/w/cpp/language/template_argument_deduction

function template을 instantiate하기 위해서는 모든 template argument를 알아야 하지만, 모든 template argument가 지정되어야하는 것은 아닙니다. 가능하다면, compiler는 부족한 template argument를 function argument로부터 추론할 것입니다.
CppReference

말그대로 추론이 가능한 경우에 compiler가 대신 해당 부분을 채워준다는 것이다
가장 간단한 예시부터 살펴본다면 아래와 같은 예시가 있을 것이다

template <typename Ret, typename T1, typename T2>
Ret add(const T1& _1, const T2& _2) {
    return _1 + _2;
}

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

위 코드에서 잘생각해본다면 T1T2는 추론이 가능하다
왜냐하면 우리가 function argument로 integer literal을 넘겨주었기 때문이다
즉, compiler는 integer literal이 넘어올 것을 알고 T1T2의 type을 int로 추론가능하다

추론 가능하다는 것은 우리가 굳이 명시해줄 필요가 없다는 뜻이기도하다
따라서 아래와 같이 작성하여도 compiler는 동일한 결과물을 출력한다

template <typename Ret, typename T1, typename T2>
Ret add(const T1& _1, const T2& _2) {
    return _1 + _2;
}

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

이처럼 compiler는 추론가능한 부분을 비워놓더라도 알아서 채워준다

Things we are deducing

그렇다면 compiler는 도대체 어떤 부분을 deduce해서 채워주는 걸까
결과론적으로 본다면 단순히 template parameter가 될 것이다
하지만 이렇게 단순히 치부하기에는 그 아래 너무 많은 것들이 존재한다

Template argument deduction은 template argument들을 결정하려 시도하는데, 이때 각 template argument는 type deduced A(adjustment 후의 argument type)를 추론할 수 있는 parameter P로 치환될 수 있어야 합니다
CppReference + 뭔가 이상한 번역

막상 한글로 번역할려니까 말이 뭔가 이상해져버렸다..
아무튼 template argument를 찾으려고 하는데 갑자기 'P'와 'A'가 등장한다
이 부분에 대해서 Scott Meyers는 이렇게 설명한다
https://youtu.be/wQxj20X-tIU?si=59eQwcCYCKliTK_t&t=597

When you do template type deduction for a function template, you are actually deducing two different types. ... So if you have template parameter T, which you clearly have to deduce the type for ... And you're also deducing a type for what the parameter is.
Scott Meyers

여기서 Scott 형님의 설명에 따르면 compiler는 두가지에 대해서 deducing을 진행한다고 한다
그리고 이 두가지가 template parameter와 parameter type이라는 것이다
이 두가지를 더 쉽게 확인하기 위해 위 예시에 대입한다면 다음과 같을 것이다

template <typename Ret, typename T1, typename T2> 
Ret add(const T1& _1, const T2& _2) {               
    return _1 + _2;
}

int main() {
    return add<int>(1, 2);  // T: int, const T&: const int&
}

위 예시에서 template parameter와 paramter type은 다음과 같을 것이다

  • template parameter(T): int
  • parameter type(const T&): const int&

다시 CppReference로 돌아와 standard에서 뭐라하는지를 생각해본다면 이제 이해가 간다
parameter type(const int&)이 deduce되고, 이것으로 T(int)가 deduce되는 것이다
실제로 standard를 참고할 경우, 'T'를 구하는데 대부분의 설명이 'P'와 'A'로 진행된다
물론 이 과정 자체에 대해서는 아래에서 다룰 생각이고 여기서는 각 글자의 뜻만 짚고 넘어가고 싶다

  • P: template parameter의 type
  • A: P에 대응하는 argument의 type
  • deduced A: adjustment가 일어난 후의 A

The place where it takes in

사실 사소할 수도 있는 부분이지만, 짚고 넘어가고 싶었다
과연 argument deduction은 언제 수행되는 것일까
CppReference에 따르면 다음과 같은 시점에서 발생한다고 명시되어 있다

Template argument deduction은 template name lookup 후에 발생하고, template argument substitution과 overload resolution 전에 발생합니다
CppReference

이건 아래의 예시를 통해서 확인해볼 수 있다
사실 아래 code가 중요한건 아니고, error message를 봐야한다
clang 16.0.0 --std=c++2b -O0

#include <type_traits>

template <
    typename T, 
    typename = typename std::enable_if_t
        <std::is_integral_v<T>>
>
void foo(T arg) {}

template <typename T>
void bar() {}

int main() {
    foo(1.f);
    bar();
}

// Result
<source>:14:5: error: no matching function for call to 'foo'
    foo(1.f);
    ^~~
<source>:8:6: note: candidate template ignored: requirement 'std::is_integral_v<float>' was not satisfied [with T = float]
void foo(T arg) {}
     ^
<source>:15:5: error: no matching function for call to 'bar'
    bar();
    ^~~
<source>:11:6: note: candidate template ignored: couldn't infer template argument 'T'
void bar() {}
     ^
2 errors generated.

잘 보면 둘다 처음으로 나오는 error는 no matching function for call to ...이다
그런데 그 다음줄이 뭔가 다른건 확인할 수 있다

foo의 경우 clang이 너무 친절한 나머지 requirement not satisfied를 출력해주고 있다
어찌되었든 해당 줄의 맨 마지막을 통해 Tfloat로 바뀐걸 확인할 수 있다
즉, Tfloat로 추론하는데까진 성공했는데 substitution 가능한 function을 못찾은 것이다

반면 bar의 경우는 이야기가 조금 달라진다
이번에는 T의 추론에 실패했다고 뜬다
당연하게도 우리가 추론할 수 있는 여지를 안주었기 때문이지만, 어쨋든 substitution까지 못 넘어갔다
즉, 추론이 불가하다면 substitution은 고려대상이 안된다는 것을 알 수 있다

이처럼 이러한 과정에도 단계라는 것이 존재한다
그리고 컴파일러는 어떠한 과정에서 실패한 것인지를 우리에게 알려준다
이러한 차이를 유심히 살펴본다면 해당 error에 맞는 더 적합한 조취를 취할 수 있을 것이다


Adjustments

기본적인 내용을 다루면서 argument deduction이 어느 부분을 언제 deduce하는지 알아보았다
그렇다면 이제 deduce하면 되는게 아닐까 싶겠지만, 그렇지 않다
Cpp에서는 implicit conversion이 빈번하게 일어난다
이러한 conversion이 template에는 적용되지 않는다면 뭔가 좀 어색할 것이다

예를 들어 function parameter에 const가 붙어있다고 해보자
이런 경우 일반적으로 argument로 const와 non-const value 모두 넘어갈 수 있다
그런데 template의 경우 compiler가 대신 function을 생성해준다
이때, 둘의 type이 같지 않다는 이유로 두가지를 다른 function으로 instantiate한다면..?
어색하다는 느낌 뿐만 아니라, 무분별한 instantiation이 일어난다는 생각이 든다

따라서 template argument로 type을 그대로 넘기기 보다는 약간은 조정할 필요가 있다
그리고 이러한 부분들이 Standard에 Adjustments로 명시되어있다

If P is not a reference type

P가 reference type이 아닐 경우 일이 약간 까다로워진다
우선 CppReference에서 설명하는건 크게 3가지 이다

  1. A가 array type일 경우, A는 array-to-pointer conversion에 의해 pointer type으로 교체 됩니다
  2. A가 function type일 경우, function-to-pointer conversion에 의해 pointer type으로 교체 됩니다
  3. A가 cv-qualified type일 경우, top-level cv-qualifier들은 모두 무시됩니다

우선 1번과 2번은 암묵적으로 변환되는 형태로서 기존에도 conversion이 자주 되던 놈들이다
예시를 들어본다면 아래와 같을 것이다 사실 CppReference 예시를 베껴왔다..

template <typename T>
void foo(T) {}

void bar() {}

int main() {
    int value[3];
    foo(value);     // P = T, A = int[3]
                    //  -> A is adjusted to int*
                    //  -> T = int*

    foo(bar);       // P = T, A = void()
                    //  -> A is adjusted to void(*)()
                    //  -> T = void(*)()
}

둘다 pointer conversion이 수행된다는 뜻이다

3번은 top-level의 cv-qaulifier가 ignore된다고 써있다
가장 쉬운 예시로 pointer type인 경우가 있을 것이다
이때 const와 pointer를 섞는다면 아래와 같이 4가지 경우를 생각할 수 있다

template <typename T>
void foo(T) {}

int main() {
    int value = 5;

    foo(static_cast<int*>(&value));             // P = T, A = int*, T = int*
    foo(static_cast<int const*>(&value));       // P = T, A = int const*, T = int const*
    foo(static_cast<int *const>(&value));       // P = T, A = int * const
                                                //  -> A is adjusted to int*
                                                //  -> T = int*
    foo(static_cast<int const* const>(&value)); // P = T, A = int const* const
                                                //  -> A is adjusted to int const*
                                                //  -> T = int const*
}

잘보면 위 두가지와 아래 두가지에 차이가 있다는걸 확인할 수 있다
우선 위 두가지의 경우 top-level에 cv-qualifier가 존재하지 않는다
따라서 adjustment 없이 바로 deduction에 들어갈 수 있다
반면 아래 두가지를 본다면 top-level에 const가 존재하는걸 알 수 있다
이러한 top-level cv-qualifier는 adjustment 단계에서 제거된다

If P is a cv-qualified type

이 부분은 바로 위 3번과 상당히 유사하다
왜냐하면 규칙 자체는 동일하기 때문이다
다른 점이라면 이번에는 A가 아니라 P에 적용된다는 것이다

template <typename T> void foo(T*) {}
template <typename T> void foo_pc(T *const) {}

int main() {
    int value = 5;

    foo(&value);        // P = T*, A = int*, T = int
    foo_pc(&value);     // P = T *const, A = int*
                        //  -> P is adjusted to T*
                        //  -> T = int
}

위와 동일하게 top-level에 cv-qualifier가 존재하는 경우에만 adjustment가 발생한다
굳이 pointer type이 아니더라도 동일하게 top-level의 cv-qualifier는 ignore된다
다만, 그 차이를 확연하게 확인하기 위해 pointer type을 사용했다

If P is a reference type

Reference가 붙은 경우에 standard는 다음과 같은 표현을 사용한다

If P is a reference type, the type referred to by P is used for type deduction

이 말을 직역하자면 'P가 참조하고 있는 type' 정도가 될 수 있다
해당 표현이 무슨 뜻일까하고 오랜시간 고민해보았다
주위 사람들에게도 동일한 질문을 해보았지만 뚜렷한 답변을 얻기는 힘들었다
그리고 내가 내린 결론은 reference를 ignore한다이다

하여튼 top-level cv-qualifier을 ignore하는 것마냥 이것도 무시해버리면 된다
다만 여기서 주의해야할 것이, forwarding reference는 제외다
forwarding reference의 경우는 그 다음에 나온다
따라서 lvalue reference와 cv-qualifier가 존재하는 rvalue reference가 해당된다

template <typename T> void foo(T&) {}
template <typename T> void foo_crr(T const&&) {}

int main() {
    int value = 5;

    foo(value);     					// P = T&, A = int
                                        //  -> P is adjusted to T
                                        //  -> T = int
    foo_crr(static_cast<int&&>(value)); // P = T const&&, A = int
                                        //  -> P is adjusted to T const
}

사실 const rvalue reference 같은 경우에는 아래서 다룰 alternative 중 하나가 적용되어야한다
따라서 P만 adjust 하고 나머지는 더 이상 진행하지 않았다
뭐 이건 그때 다시 다루는걸로 하고, adjust 부분만 보도록하자

한가지 재미있는 점은 reference가 cv-qualified type을 가리키는 경우다
이런 경우에는 reference가 존재하기에 top-level cv-qualifier에 해당하지 않게된다
따라서 아래와 같은 경우에는 T에 cosnt qualifier가 붙은 형태로 deduce 된다

template <typename T>
void foo(T&) {}

int main() {
    foo(static_cast<int const&>(5));	// P = T&, A = int const
    									//	-> P is adjusted to T
                                        //	-> T = int const
}

여기서 잠깐 top-level cv-qualifier는 동일하게 제거해야하는게 아닌가 싶었다
다행이도(?) reference에는 cv-qualifier를 붙일 수 없기 때문에 고민할 필요가 없다

If P is a rvalue reference

c++11에서 등장한 forwarding reference인 경우다 표현 자체는 나중에 추가되긴 했지만..
항상 기억해야할게 forwarding reference는 만능 그자체다
모든 type으로 substitute 될 수 있다
다만 해당 내용은 분량이 상당해서 따로 빼서 다룰 생각이다
여기서는 그냥 어떤 식으로 되는지만 정리해보았다

template <typename T>
void foo(T&&) {}

int main() {
    int value = 5;

    foo(static_cast<int&>(value));  // argument is lvalue, T = int&, P = int& (special case)
    foo(static_cast<int&&>(value)); // argument is rlvaue, T = int, P = int&&
}

저 special case만 조심하면 된다
저게 정확히 lvalue reference일 때만 저런건 아니고, 그냥 lvalue이면 저렇게 된다
물론 rvalue면 아래 두번째 case처럼 된다
문제는 저 special case 때문에 엄청나게 헷갈린다는 것이다


Deduction

초반에 설명했듯이, 결국 목적은 deduced A를 만들 수 있는 template argument를 찾는 것이다
그리고 이걸 위해 열심히 adjustment를 취했다
이제 드디어 deduction을 할 수 있게 되었다

사실 deduction이라고해서 별게 있는건 아니다
오히려 굉장히 단순하다
그냥 adjusted P와 transformed A(위 단계에서 변형된 A)을 비교하기만 하면된다
가장 단순한 예시를 든다면 다음과 같을 것이다

template <typename T>
void foo(T) {}

int main() {
    int value = 5;

    foo(value); // P = T, A = int, T = int
}

P = T이고 A = int 이므로, deduced T = int 라는 결론에 도달한다
사실 이미 adjustments 예시에서 deduction까지 다 적어놓기는 했다
따라서 이미 적은 부분에 대해서는 굳이 다시 언급하지 않으려한다

다만, 위와 같이 일반적인 deduction에 실패한 경우 다른 대안 3가지가 존재한다
그리고 standard에 의하면 이 대안 중 유일하게 한가지만 존재할 때, 해당 방안이 채택된다고 한다
아래 설명을 보면 알겠지만, 이게 겹칠일이 있을까 싶긴하다
아무튼 대안 3가지는 다음과 같다

If P is a reference type

여기서 말하는 Poriginal P, 즉 adjustment가 있기전 P를 말한다
이런 경우 deduced A는 more cv-qualified type으로 conversion이 일어날 수 있다

the deduced A can be more cv-qualified than the transformed A

여기서 말하는 transformed A란, 위 adjustment를 거친 A를 말한다
adjustment 부분을 다시 참고해보면, P뿐만 아니라 A도 adjust가 된다

아무튼 A가 more cv-qualified type으로 implicit conversion이 일어날 수 있다는 것이다
다시 위를 참고하면 딱 하나 deduction까지 설명하지 못한 예제가 있다
해당 예제를 사용해서 설명하자면 다음과 같다

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

int main() {
    
    foo(5); // P = T const&&, A = int
            //  -> P is adjusted to T const
            //  -> deduced A = int const
            //  -> deduced T = int
}

잘보면 deduced AA보다 더 cv-qaulified하다는 것을 알 수 있다
이것은 아마 reference 혹은 pointer type들은 more cv-qalified type으로 implicit conversion이 일어날 수도 있다는 rule 때문에 존재하는게 아닌가 싶다
https://en.cppreference.com/w/cpp/language/cv#Conversions

Qualification conversion and function pointer conversion

만약 A가 pointer라면 두가지 conversion이 일어날 수 있다고 한다
하나는 more cv-qualified type으로의 conversion인 qualification conversion이다
다른 하나는 조금 생소한 개념인데, noexcept가 붙은 function pointer가 function pointer로 implicit conversion 될 수 있다는 것이다
이때 member function pointer 또한 포함된다
https://eel.is/c++draft/conv.fctptr

qualification conversion에 대한 예시는 다음과 같다

template <typename T>
void foo(T const*) {}

int main() {
    int value;
    foo(&value);    // P = T const*, A = int*
                    //  -> deduced A = int const*
                    //  -> deduced T = int
}

사실 바로 위에서 다루었던 내용이랑 동일한 내용이다
위에서 설명했는데 이게 reference 혹은 pointer type인 경우다
따라서 위와 동일하게 more cv-qualified type으로 conversion 될 수 있다

function pointer conversion에 대한 예시는 다음과 같다

void func(int) noexcept {}

template <typename T>
void foo(T) {}

template <typename T>
void foo2(void(*)(T)) {}

int main() {
    foo(&func);     // P = T, A = void(*)(int) noexcept
                    //  -> T = void(*)(int) noexcept
    foo2(&func);    // P = void(*)(T), A = void(*)(int) noexcept
                    //  -> deduced A = void(*)(int)
                    //  -> deduced T = int

뭔가 좀 억지 같을 수 있는데, 이게 된다고 명시가 되어있으니 한번 만들어보았다
foo는 그냥 T를 template argument로 사용한 반면, foo2void(*)(T)를 사용했다
foo는 usual deduction form이긴 한데 비교를 위해 가져와보았다

foo2를 잘보면 deduced A가 noexcept가 없는 function pointer인걸 확인할 수 있다
물론 function pointer conversion 또한 일어났다
살짝 주제를 벗어나는 이야기이긴한데 그렇다면 noexcept의 특성도 없어질까

우선 noexcept가 붙어있는 function에서 throw를 할 경우, std::terminate가 호출된다
GCC 13.2 --std=c++2b -O0

void func(int) noexcept { throw 0; }

int main() {
    try {
        func(5);
    } catch (int err) {}
}

// Execution result
terminate called after throwing an instance of 'int'
Program terminated with signal: SIGSEGV

그렇다면 function pointer conversion이 일어난 경우에는 어떨까
GCC 13.2 --std=c++2b -O0

void func(int) noexcept { throw 0; }

template <typename T>
void foo(void(*fp)(T)) { fp(5); }

int main() {
    try {
        foo(&func);     
    } catch (int err) {}
}

// Execution result
terminate called after throwing an instance of 'int'
Program terminated with signal: SIGSEGV

결론은 '똑같다' 이다
애초에 function type이 converting 되는것도 아니고 pointer conversion이다
따라서 function의 noexcept 특성은 그대로 살아있는 것을 확인할 수 있다

If P is a class and P has the form simple-template-id

이게 뭘 가능하게 해준다는건지 설명하기 이전에 simple-template-id 부터 짚고 넘어가야할 것 같다
template specialization 부분에서 나오는 개념인데, standard에서는 다음과 같이 설명한다
https://eel.is/c++draft/temp.names#nt:template-name

template-name (identifier) < template-argument-list (단일 혹은 여러개의 template argument) >

어짜피 우리는 class가 이런 form을 가지고 있을때가 궁금하므로, class로 예시를 들면 다음과 같다

template <typename T1>
struct bar {};

template <typename T2>
void foo(bar<T2> const&) {}

잘 보면 bar<T>P에 사용되었다
이걸 위 standard의 표현에 대입해 보면 다음과 같을 것이다

  • template-name: bar
  • template-argument-list: T2

이제 simple-template-id를 간단하게나마 알아보았으니 다시 본론으로 돌아가자
standard에서는 다음과 같은 대안이 가능하다고 한다

transformed A can be a derived class of the deduced A

그러니까 상속받은 base class로의 upcasting이 일어날 수 있다는 것이다
물론 P가 pointer인 경우에도 가능하다
이건 CppReference에 굉장히 직관적인 예시가 있어서 그대로 들고왔다

template<class T>
struct B {};
 
template<class T>
struct D : public B<T> {};
 
template<class T>
void f(B<T>&) {}
 
void f()
{
    D<int> d;
    f(d);   // P = B<T>&, A = D<int>
            //  -> P is adjusted to B<T>
            //  -> deduced A = B<int>
            //  -> deduced T = int
}

Deduction from a type

지금까지는 주로 P가 아래 4가지 form인 경우에 대해서 deduction rule을 설명을 했었다

  • cv(optional) T
  • T*
  • T&
  • T&&

그런데 만약에 Type안에서 특정부분만 추론하고 싶다면 어떻게 해야할까
예를 들어 container의 value type만 template으로 받고 싶은 경우다
사실 바로 위에 힌트가 있긴한데 정답은 아래와 같다

#include <vector>

template <typename T>
void foo(std::vector<T> value) {}

이처럼 Cpp는 P가 Complete Type이 아니더라도 추론을 해준다
Standard에 따르면 아래 7가지 경우에 대해서도 Deduction이 가능하다고 한다
https://eel.is/c++draft/temp.deduct.type

  • T(optional) [ i(optional) ]
  • T(optional) ( T(optional) )  noexcept ( i(optional) )
  • T(optional) T(optional)::*
  • TT(optional) < T >
  • TT(optional) < i >
  • TT(optional) < TT >
  • TT(optional) <>

여기서 optional이 붙은 경우에는 말 그대로 넣든 말든 상관이 없다
그러니까 해당 부분을 template argument로 받을 수도, 혹은 명시해줄 수도 있다는 뜻이다
하지만 귀찮은 관계로 그냥 다 template argument로 받는다치고 설명하였다

T(optional) [ i(optional) ]

P가 Array인 경우이다
사실 지금까지 Cpp를 사용하면서 이런 문법을 사용해본적이 없긴하다
그런데 또 된다고 하니 궁금해서 찾아보다가 아래와 같은 예시를 만들게 되었다

template <typename T, int I>
void foo(T (&arr)[I]) {}

auto main() -> int {
    int arr[3];
    foo(arr);
}

일반적으로는 C 형식의 문법을 따라 size를 다른 parameter넘긴다
그런데 Cpp에서는 이런것도 가능하게 해준 모양이다
아무튼 위와 같이 작성한다면 array의 size를 template parameter로 받아올 수 있다

여기서 한발자국 더 나아가 I의 type조차 compiler에게 맡겨버릴 수 있다
(사실상 std::size_t 고정이 아닐까 싶긴한데 내가 모르는 뭔가가 있을수도 있으니까..)

template <typename T, typename SizeType, SizeType I>
void foo(T (&arr)[I]) {}

auto main() -> int {
    int arr[3];
    foo(arr);
}

당연하겠지만 다차원 배열도 가능하다

template <typename T, T _1, T _2, T _3>
void foo(T (&arr)[_1][_2][_3]) {}

auto main() -> int {
    int arr[1][2][3];
    foo(arr);
}

T(optional) ( T(optional) )  noexcept ( i(optional) )

뭔가 모양이 많이 이상해보는데 그래도 noexcept라는 너무나도 명백한 힌트가 존재한다
noexcept는 function declarator로써 function 이외에는 붙을 수 없기 때문이다
즉, 위 모양은 P가 function type인 경우에 사용된다는 것을 알 수 있다

그런데 우리가 흔히 noexcept를 붙일때 뒤에 뭘 달진 않는다
그렇다면 도대체 noexcept 뒤에 달려있는 저 i는 뭘까
이 부분은 Standard의 Exception Handling에 나와있다
https://eel.is/c++draft/except.spec#2

In a noexcept-specifier, the constant-expression shall be a contextually converted constant expression of type bool; that constant expression is the exception specification of the function type in which the noexcept-specifier appears

뭔가 길게 적혀있는데 사실 별거없다
그냥 저 constant-expression이 true면 noexcept 특성이 적용되고 false면 적용되지 않는다는 것이다
그리고 그 뒤에 저 constant-expression 부분을 생략한다면 true로 적용된다는 내용도 있다

다시 돌아와서 그렇다면 위 경우에 P는 어떤 형태를 띄고 있을까

int bar(float) {}

template <typename Ret, typename Param>
void foo(Ret(&func)(Param)) {}

auto main() -> int {
    foo(bar);
}

이제 위와 같이 적용할 경우 function의 Return과 Parameter type을 손쉽게 알아낼 수 있다

T(optional) T(optional)::*

이 부분은 지난 글에서도 아주 잠깐 언급을 했었는데 부족해보여서 살짝만 보충하려한다
대충 이런 형태는 Object의 member에 대한 pointer type을 의미한다
그러니까 Object의 member access를 위한 pointer라 보면된다
https://en.cppreference.com/w/cpp/language/operator_member_access

지난글에서도 그랬듯이 해당 내용이 뭔지를 다루는 것은 글의 논점을 흐리는 것 같아 다룰 생각은 없다
다만 이게 Deduction이 어떻게 적용되는지만 보고 넘어가도록 하자

struct bar {
    int some_val;
};

template <typename T, typename DataType>
void foo(DataType T::*) {}

auto main() -> int {
    foo(&bar::some_val);
}

해당 예시를 본다면 bar type의 member variable(some_val)을 가르키는 pointer가 넘어간거다
이렇게만 본다면 이게 도대체 뭔소리인가 싶을 수 있으니 사용예시도 같이 넣어보았다
사실 실전에서 한번도 써본적이 없어서 틀릴수도 있다

#include <iostream>

struct bar {
    int some_val;
};

template <typename T, typename DataType>
void foo(DataType T::* ref, T& inst) {
    std::cout << inst.*ref;
}

auto main() -> int {
    bar b{3};
    foo(&bar::some_val, b);
}

// Result: 3

bar에서 some_val을 가르키는 pointer와 bar type의 Object를 넣어주었다
여기에 Member access operator을 사용하면 접근이 가능하다
그리고 이제는 template까지 더해졌기에 type deduction이 일어난다는 것이다
따라서 Object의 type과 member variable의 type을 알아낼 수 있다

물론 member function pointer을 넣어도 동일하게 동작한다

#include <iostream>

struct bar {
    void some_func() {}
};

template <typename T, typename Ret, typename... Param>
void foo(Ret (T::*func)(Param...), T& inst, Param... args) {
    (inst.*func)(args...);
}

auto main() -> int {
    bar b{3};
    foo(&bar::some_func, b);
}

TT(optional) < T, I, TT >

여기서 말하는 TT는 template template parameter이다
template template parameter는 지난 글들에서 몇번 다룬적이 있기에 예시만 빠르게 보도록하자

template <typename T>
struct bar {};

template <
    template <typename...> 
        typename TT, 
    typename T
>
void foo(const TT<T>&) {}

auto main() -> int {
    foo(bar<int>{});
}

더 쉬운 이해를 위해 줄바꿈을 좀 넣어보았다
사실 이전 글들에서 이미 다 설명했던 내용이라 딱히 설명할건 없다
그냥 template template parameter도 deduction이 된다는 점만 알면된다
그 아래에 존재하는 TT(optional) < I > 형태도 볼게 별로 없다
그냥 type parameter 대신에 non-type parameter가 들어간거다

template template parameter가 template template parameter안에 들어가는 경우는 좀 골때린다
처음에는 이게 재귀형식으로 해준다는줄 알고 여러가지로 해보았다
근데 그건 또 아닌거 같다
아무튼 2-3시간의 삽질을 통해 도달한 결론은 아래와 같다

template <typename T>
struct bar {};

template <
    template <typename...>
        typename TT
>
struct bar_TT {};

template <
    template <template <typename...> typename...>
        typename TTT,
    template <typename...>
        typename TT
>
void foo(const TTT<TT>&) {}

auto main() -> int {
    foo(bar_TT<bar>{});
}

template을 3중으로 쓴게 template template이 맞는건가 싶긴한데, 별다른 표현이 안보인다
그래서 이것도 template template이라 생각하고 작성해보았다
아무튼 이런 형식으로도 Compile은..? 된다
애초에 template을 이렇게까지 쓸일이 있나싶다..


초반부에 언급했던 CppCon 영상에서 Scott Meyers는 이런말을 한다

In C++98, I never understood how type deduction worked. Because it worked so naturally ... It turns out that in C++11, the scope of type deduction is no longer limited just to template

적용 범위가 확장되었다는 것인데, 사실 나는 해당 문장에서 "naturally"란 표현이 신경쓰인다
실제로 C++11이 등장하기 전까지는 그냥 Compiler가 해주는 대로 받아먹으면 되었다
하지만 이제는 Compiler가 해주는대로 받아먹기에는 뭔가 좀 문제가 있어보인다
이제 최소 한번은 Deduce된 Type이 뭔지를 생각해보거나 확인해볼 필요가 있다
만약 이걸 하지 않는다면 의도와 다른 type이 deduce 될 수도 있기 때문이다

그리고 이러한 생각이나 확인을 위해서는 Deduction Rule을 한번 정도는 이해할 필요가 있다고 본다
물론 Deduction rule을 달달 외우고 다니라는 의미는 아니다
다만 의도한 Type과 다르다면 어디서 발생한 문제인지 정도는 알 수 있어야한다는 것이다
Type Deduction을 안쓰면 되지 않냐고 할 수도 있겠지만..이건 좀 힘들다
Deduction이 필수적인 부분도 있거니와, 당장에 STL만 쓰더라도 deduction이 어딘선가 개입한다
그리고 무엇보다 typing양이 획기적으로 줄고 가독성도 많이 증가한다

가벼운 마음으로 시작했던 글이 2달반이라는 시간이 지나고서야 끝났다
처음 시작할 당시만 하더라도 Type deduction을 너무 만만하게 보았던것 같다
그래서 Standard와 CppReference를 뒤지면서 한달반 넘는시간동안 공부만 했다
그러다보니 글이 사실상 Standard와 CppReference를 잘섞어놓은 복사본이 된거 같아 아쉽다
이 글을 처음 적기 시작한 이유는 사실 학교에서 template을 잘 알려주지 않아서이다
그래서 지금까지 최대한 쉽게 적으려고 노력을 많이 해왔고, 너무 세부적인 내용 일부는 제외한 것도 있다
그런데 이번 글은 막상 적고 보니 나 자신이 이해하는데 급급해 이러한 노력이 부족했던것 같다

profile
하고싶은거 하는 사람

0개의 댓글