KoreanFoodie's Study

Effective Modern C++ | 항목 1 : 템플릿 형식 연역 규칙을 숙지하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 1 : 템플릿 형식 연역 규칙을 숙지하라

GoldGiver 2022. 10. 26. 09:48

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 1 : 템플릿 형식 연역 규칙을 숙지하라

핵심 :

1. 템플릿 형식 연역 도중 참조 형식의 인수들은 비참조로 취급된다. 즉, 참조성이 무시된다.
2. 보편 참조 매개변수에 대한 형식 연역 과정에서 왼값 인수들은 특별하게 취급된다.
3. 값 전달 방식의 매개변수에 대한 형식 연역 과정에서 const 또는 volatile(또는 그 둘 다인) 인수는 비 const, 비 volatile 인수로 취급된다.
4. 템플릿 형식 연역 과정에서 배열이나 함수 이름에 해당하는 인수는 포인터로 붕괴한다. 단, 그런 인수가 참조를 초기화하는데 쓰이는 경우, 포인터로 붕괴하지 않는다.


C++ auto 는 템플릿에 대한 형식 연역을 기반으로 작동한다. 예시를 보자.

// 템플릿 선언의 형태
template<typename T>
void f(ParamType param) {}
...
// expr 로부터 T 와 ParamType 을 연역
f(expr);

// 예시코드
template<typename T>
void f(const T& param) {}

int main()
{
	int x = 0;
	// const int& 가 아닌 int 로 f 호출
	f(x);
}

이는, T 에 대해 연역된 형식은 expr 의 형식뿐만 아니라 ParamType 형태에도 의존한다. 형식 연역 시나리오를 3 가지로 나누어 살펴보자.

  • ParamType 이 포인터 또는 참조 형식이지만 보편 참조(universal reference) 는 아닌 경우
  • ParamType 이 보편 참조인 경우
  • ParamType 이 포인터도 아니고 참조도 아닌 경우

 

경우 1 : ParamType 이 포인터 또는 참조 형식이지만 보편 참조(universal reference) 는 아님

이 경우, 형식 연역은 다음과 같이 진행된다.

  1. 만일 expr 이 참조 형식이면 참조 부분을 무시
  2. 그 다음 expr 의 형식을 ParamType 에 대해 패턴 부합(pattern-matching) 방식으로 대응시켜 T 의 형식을 결정
template<typename T>
void f(T& param) {}

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

int main()
{
	int x = 27;
	const int cx = x;
	const int& rx = x;

	// T 는 int, param 의 형식은 int&
	f(x);
	// T 는 const int, param 의 형식은 const int&
	f(cx);
	// T 는 const int, param 의 형식은 const int&
	f(rx);

	// T 는 int, param 의 형식은 const int&
	f2(x);
	// T 는 int, param 의 형식은 const int&
	f2(cx);
	// T 는 int, param 의 형식은 const int&
	f2(rx);
}

위에서 T& 대신 T* 를 사용해도 타입은 같은 원리로 연역된다.

경우 2 : ParamType 이 보편 참조임

만일 expr 이 왼값이면, T 와 ParamType 둘 다 왼값 참조로 연역된다(매우 어색할 수 있다). 만약 expr 이 오른값이면, '정상적인'(경우 1의) 규칙들이 적용된다.

template<typename T>
void f(T&& param);

int main()
{
	int x = 27;
	const int cx = x;
	const int& rx = x;

	// x 는 왼값. 따라서 T 는 int&,
	// param 의 형식 역시 int&
	f(x);
	// cx 는 왼값. 따라서 T 는 const int&,
	// param 의 형식 역시 const int&
	f(cx);
	// rx 는 왼값. 따라서 T 는 const int&,
	// param 의 형식 역시 const int&
	f(rx);
	// x 는 오른값. 따라서 T 는 int,
	// param 의 형식은 int&&
	f(27);
}

이 예들이 각각 해당 형식으로 연역되는 이유는 항목 24에서 후술하도록 한다.

경우 3 : ParamType 이 포인터도 아니고 참조도 아님

// param 이 값으로 전달
template<typename T>
void f(T param);

위의 경우, expr 형식의 참조성과 const 는 둘 다 무시된다. 심지어 volatile 도 무시된다!

template<typename T>
void f(T param);

int main()
{
	int x = 27;
	const int cx = x;
	const int& rx = x;

	// T 와 param 의 형식은 둘 다 int
	f(x);
	// T 와 param 의 형식은 둘 다 int
	f(cx);
	// T 와 param 의 형식은 둘 다 int
	f(rx);
}


그럼 다음과 같은 경우는 어떨까?

template<typename T>
void f(T param);

...

const char* const ptr = "Fun";
f(ptr);

위 경우, 포인터 자체(ptr) 가 값으로 전달되어, param 은 const char* 의 형식을 가지게 된다. 즉, ptr 가 가리키는 것의 const 성은 보존되나, ptr 자체의 const 성은 사라지는 것이다.

배열 인수

초보 프로그래머는 흔히 배열이 배열의 첫 원소를 가리키는 포인터로 붕괴(decay) 하는 현상은, 템플릿에도 마찬가지로 적용된다.

template<typename T>
void f(T param) {}

int main()
{
	const char name[] = "Tony";
	const char* pName = name;

	// T 는 const char[4] 가 아닌 const char* 로 연역됨
	f(name);
}

아래와 같이 T 에 참조자를 달아 주면, f(name) 을 사용할 때 T 를 const char[4] 로 연역할 수 있다.

template<typename T>
void f(T& param) {}

이를 응용하면, 컴파일 타임 도중에 배열에 담긴 원소들의 갯수를 알아낼 수 있다! 물론 std::array 를 사용하는 것이 더 편하다.

template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
	return N;
}

...

int keyVals[] = {1, 2, 3, 4};

// 템플릿을 이용한 방식
int mappedVals[arraySize(keyVals)];

// 좀 더 모던한 방식
std::array<int, arraySize(keyVals)> mappedVals;

 

함수 인수

배열처럼, 함수 형식도 함수 포인터로 붕괴할 수 있다. 예시를 보자.

void someFunc(int, double);

template<typename T>
void f1(T param);

template<typename T>
void f2(T& param);

// param 은 void (*)(int, double)
f1(someFunc);
// param 은 void (&)(int, double)
f2(someFunc);

 

Comments