-
[Effective Modern C++] Type deduction : TemplateC++/Effective Modern C++ 2021. 12. 21. 09:27
형식 연역 (Type deduction)은 소스코드에서 특정한 코드가 어떤 것으로 바뀌는 것 입니다.
여기서 '특정한 코드'란 template, auto, decltype 등 이 있으며
바뀌게 되는 '어떤 것' 에는 자료형부터, 함수, 람다식 등이 있습니다.
형식 연역은 특정한 규칙에 의거하여 이루어집니다.
개발자가 추론한 것과 컴파일러가 실제로 연역한 것이 다를 수 있기 때문에, 이 규칙을 확실히 이해해야 합니다.
귀찮고, 복잡하다고 안 쓰기에는 template과 auto는 굉장히 유용한 기능이기 때문입니다.
이번 글에서는 template의 형식 연역에 대하여 살펴보도록 하겠습니다.
함수 템플릿은 보통 아래와 같은 형태로 선언되고, 호출됩니다.
//Declaration template<typename T> void function(ParamType param); //Call function(expr);
위와 같이 선언된 함수 템플릿을 호출하는 코드를 컴파일 하게 될 경우
컴파일러는 expr을 이용해 T와, ParamType 두 가지의 형식을 연역하게 됩니다.
T와 ParamType이 다른 이유는, ParamType에는 const, &, &&등의 수식어가 붙기 때문입니다.
예를 들어 아래와 같은 경우에서
//Declaration template<typename T> void function(const T& param); //Call int x = 0; function(x);
T는 int로 연역되지만, ParamType은 const int&로 연역됩니다.
여기까지 본다면, T는 expr의 형식과 같게 되는 것이라고 기대할 수 있습니다.
하지만, T는 expr뿐만 아니라 ParamType의 형태와도 연관이 있습니다.
이 경우는 ParamType에 따라 3가지의 경우로 나뉩니다.
- ParamType이 포인터 혹은 참조 형식이지만 보편 참조(Universal reference)는 아닐 경우
- ParamType이 보편 참조일 경우
- ParamType이 포인터도 아니고 참조도 아닐 경우
여기서 보편 참조란, r-value 혹은 l-value참조와는 다른 것 입니다.
이에 관해서는 차후 다른 글에서 다루도록 하겠습니다.
위와 같은 세 경우에 대하여, 템플릿의 형식 연역을 살펴보도록 하겠습니다.
1. ParamType이 포인터 혹은 참조 형식이지만 보편 참조는 아닐 경우
더보기형식 연역은 다음과 같이 진행됩니다.
- expr이 참조 형식이면 참조 부분을 무시합니다.
- 이후 expr의 형식을 ParamType에 대하여 Pattern-matching방식으로 대응시켜서 T의 형식을 결정합니다.
예제를 살펴보도록 하겠습니다.
//Declaration template<typename T> void function(T& param); //Variable int x = 27; const int cx = x; const int& rx = x; //Call function(x); function(cx); function(rx);
위와 같은 변수와 호출에서, 각각 T와 ParamType은 아래 표와 같이 연역됩니다.
Call T ParamType function(x) int int& function(cx) const int const int& function(rx) const int const int& 위 진행 방식을 숙지했다면, 크게 어려울 점은 없습니다.
주목해야 할 부분은 3번째 rx를 이용한 호출에서, T가 const int로 연역되었다는 부분입니다.
이 또한 위 진행 방식에 맞게, expr의 참조 부분을 무시하는 과정에서 연역된 결과입니다.
이번에는 param을 포인터로 바꿔서 살펴보겠습니다.
//Declaration template<typename T> void function(T* param); //Variable int x = 27; const int *px = &x; //Call function(&x); function(px);
위와 같은 변수와 함수 호출에서, 각각 T와 ParamType은 아래 표와 같이 연역됩니다.
Call T ParamType function(&x) int int* function(px) const int const int* 특별하게 주목할 부분이 없이, 형식 연역 과정을 잘 따라가면 나올 수 있는 결과입니다.
모든 형식 연역 과정이 자명함을 볼 수 있습니다.
2. ParamType이 보편 참조일 경우
더보기템플릿이 보편 참조일 경우의 형식 연역은 다음과 같이 진행됩니다.
- expr이 l-value일 경우, T와 ParamType 모두 l-value 참조로 연역됩니다.
위 문단에서 T를 연역할 때 참조성을 제거한 것과 대조적입니다.
또한, ParamType의 형태가 r-value 참조이지만, 연역되는 형식은 l-value 참조입니다. - expr이 r-value일 경우, 위 문단의 형식 연역 과정이 적용됩니다.
예제를 살펴보도록 하겠습니다.
//Declaration template<typename T> void function(T&& param); //Variable int x = 27; const int cx = x; const int& rx = x; //Call function(x); //l-value function(cx); //l-value function(rx); //l-value function(27); //r-value
위와 같은 변수 선언과 함수 호출에서, T와 ParamType은 각각 아래 표와 같이 연역됩니다.
Call T ParamType function(x) int& int& function(cx) const int& const int& function(rx) const int& const int& function(27) int int&& 보편 참조가 관여하는 경우, 위의 표와 같이 r-value, l-value에 대하여 각각 다른 연역 규칙이 적용됩니다.
그 외의 경우에는 위와 같은 일이 발생하지 않습니다.
3. ParamType이 포인터도 아니고 참조도 아닐 경우
더보기포인터도, 참조도 아닌 경우면 Pass by value의 상황입니다.
값이 전달되는 경우의 형식 연역은 다음과 같이 진행됩니다.
- expr이 참조 형식이면 참조 부분을 무시합니다.
- 이후 expr이 const일 경우 const 또한 무시합니다.
- 이후 expr이 volatile일 경우 volatile 또한 무시합니다.
예제를 살펴보도록 하겠습니다.
//Declaration template<typename T> void function(T param); //Variable int x = 27; const int cx = x; const int& rx = x; //Call function(x); function(cx); function(rx);
위와 같은 변수 선언과 함수 호출에서, T와 ParamType은 각각 아래 표와 같이 연역됩니다.
Call T ParamType function(x) int int function(cx) int int function(rx) int int 참조, const가 모두 무시되었음을 볼 수 있습니다.
param이 원본 매개변수와 관련이 없는 복사된 변수이기 때문에, 원본 번수의 상수성과는 관계가 없습니다.
여기서 주의할 점은, const가 매개변수에 대해서만 무시된다는 점 입니다.
예제를 살펴보도록 하겠습니다.
//Declaration template<typename T> void function(T param); //Variable const char* const ptr = "Pointer"; //Call function(ptr);
위처럼 const객체를 가리키는 const포인터의 경우, 아래와 같이 연역됩니다.
Call T ParamType function(ptr) const char* const char* ptr을 function에 전달하면, ptr의 비트들이 param에 복사되어 전달됩니다.
이후 Pass by value 방식의 형식 연역이 진행되어 param의 상수성이 제거, param은 const char*을 가리키는 수정 가능한 포인터가 되는 것 입니다.
변수를 구성하는 모든 const가 사라지는 것이 아님에 유의합시다.
4. 예외 : 배열에 대하여
더보기배열의 경우에는 형식 연역이 조금 다르게 됩니다.
이러한 예외는 많은 상황에서 배열이 포인터로 붕괴 (Decay)되는 것에서 기인합니다.
const char arr[] = "Array"; const char* ptrArr = arr;
위와 같은 경우가 대표적인 예 입니다.
arr과 ptrArr의 형식은 다르지만, 배열이 포인터로 붕괴하면서 형식 불일치가 발생하지 않습니다.
이 상황을 생각하면서, 다음 예제를 살펴보겠습니다.
//Declaration template<typename T> void function(T param); //Variable const char arr[] = "Array"; const char* ptrArr = arr; //Call function(arr); function(ptrArr);
배열과 포인터에 대하여, Pass by value 방식의 템플릿에 연역되는 형식은 다음과 같습니다.
Call T ParamType function(arr) const char* const char* function(ptrArr) const char* const char* 배열 매개변수 선언은 포인터 매개변수로 취급되어 발생하는 결과입니다.
하지만 매개변수가 참조 형식일 경우 즉, 다음과 같은 형식일 경우
//Declaration template<typename T> void function(T& param); //Variable const char arr[] = "Array"; //Call function(arr);
T와 ParamType에 연역되는 형식은 다음과 같습니다.
Call T ParamType function(arr) const arr[6] const char(&)[6] 이것을 응용하면, 다음과 같이 배열의 원소의 개수를 연역하는 템플릿을 만들 수 있습니다.
#include <iostream> #include <array> template<typename T, std::size_t N> constexpr std::size_t arraySize(T(&)[N]) noexcept { return N; } int main() { const char str[] = "Array"; std::array<int, arraySize(str)> arr; std::cout << arr.size() << "\n"; }
6
5. 예외 : 함수에 대하여
더보기함수 또한 배열처럼 포인터로 연역되는 타입입니다.
예제를 살펴보도록 하겠습니다.
//Declaration template<typename T> void function1(T param); template<typename T> void function2(T& param); //Function void foo(int, double); //Call function1(foo); function2(foo);
다음처럼 Pass by value, reference 방식의 템플릿이 존재할 때
함수를 파라미터로 전달했을 경우 연역되는 형식은 아래와 같습니다.
Call T ParamType function1(foo) void (*)(int, double) void (*)(int, double) function2(foo) void (&)(int, double) void (&)(int, double)
전체적으로 특정한 규칙에 의거하여 연역이 이루어지는 것을 볼 수 있었습니다.
서론에 언급한 컴파일러가 연역한 형식과 실제 형식이 다른 부분은 Universal reference부분과
배열, 함수에 대한 포인터 연역 부분입니다.
위 특정한 경우 외에는 직관적으로 연역됨을 볼 수 있었습니다.
템플릿은 간편하고 강력한 기능입니다.
하지만, 간편하다고 대충 알고 넘어가면 문제가 발생할 수 있습니다.
이 글이 템플릿을 이해하는 데 조금이나마 도움이 되었으면 좋겠습니다.
감사합니다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] auto와 std::vector, 그리고 Proxy pattern (0) 2022.02.14 [Effective Modern C++] auto의 사용을 고려해야 할 상황들 (0) 2022.01.10 [Effective Modern C++] Type deduction : typeid, boost/type_index (0) 2022.01.06 [Effective Modern C++] Type deduction : decltype (0) 2022.01.04 [Effective Modern C++] Type deduction : auto (0) 2021.12.27