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

게시:     수정

카테고리:

태그: ,

이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역

📦 1. 형식 연역

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

  • auto는 템플릿 형식 연역 기반으로 작동한다.
  • 즉, auto를 활용하기 위해선 템플릿 형식 연역 규칙을 알아야 한다.
template<typename T>
void f(ParamType param)

f(expr);
  • 컴파일 중 컴파일러는 expr을 통해 T, ParamType에 대한 형식을 연역한다.
  • 보통 두 형식이 다르다.
    • ParamTypeconst나 참조 한정사(&, &&) 수식어가 붙은 경우
template<typename T>
void f(const T& param)

int x = 0;
f(x);
  • Tint, ParamTypeconst T&로 연역된다.
  • 전달된 인수와 T가 항상 같지는 않다.
    • T에 대해 연역된 형식은, expr의 형식과 ParamType의 형태에 의존한다.

📌 연역 형태에 대한 3가지 경우

  1. ParamType이 포인터/참조 형식이지만, 보편 참조는 아닌 경우
  2. ParamType이 보편 참조인 경우
  3. ParamType이 포인터/참조 형식이 아닌 경우

1️⃣ ParamType이 포인터/참조 형식이지만, 보편 참조는 아닌 경우

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

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // T = int, param = int&
f(cx); // T = const int, param = const int&
f(rx); // T = const int, param = const int&

연역 과정:

  1. expr이 참조 형식이면, 참조 부분을 무시한다.
    • T: const int&const int
    • 형식 연역 과정에서 참조성이 무시되기 때문
    • const는 유지된다.
  2. expr 형식을 ParamType에 대해 패턴 부합 방식으로 대응시켜, T의 형식을 결정한다.
    • ParamType: const intT&에 대응 → const int&
template<typename T>
void f(const T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // T = int, param = const int&
f(cx); // T = int, param = const int&
f(rx); // T = int, param = const int&
  • paramconst에 대한 참조이므로, constT의 일부로 연역될 필요가 없다.
  • 형식 연역 과정에서 참조성은 무시된다.
    • T: const int&int
    • ParamType: intconst int&

2️⃣ ParamType이 보편 참조인 경우

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

int x = 27;
const int cx = x;
const int& rx;

// x는 왼값
f(x); // ∴ T = int&, param = int&

// cx는 왼값
f(cx); // ∴ T = const int&, param = const int&

// rx는 왼값
f(rx); // ∴ T = const int&, param = const int&

// 27은 오른값
f(27); // ∴ T = int, param = int&&

연역 과정:

  • expr왼값이면, TParamType 모두 왼값 참조로 연역된다.
    • 유일하게 템플릿 형식 연역에서 T참조성을 갖는 경우이다.
    • ParamType의 선언 구문은 오른값 참조처럼 보이지만, 연역된 형식은 왼값 참조이다.
  • expr오른값이면, 정상적인(1의 경우) 규칙이 적용된다.

3️⃣ ParamType이 포인터/참조 형식이 아닌 경우

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

int x = 27;
const int cx = x;
const int& rx = x;

f(x); // T = int, param = int
f(cx); // T = int, param = int
f(rx); // T = int, param = int
  • 포인터/참조 형식이 아니라면, 값을 통한 전달 상황이다.
    • 즉, 새로운 객체(복사본) 이 전달된다.

연역 과정:

  1. expr이 참조 형식이면, 참조 부분을 무시한다.
  2. exprconst무시된다.
  3. exprvolatile이면 무시된다.
    • 참조, const, volatile이 무시되는 이유는, 복사본을 전달하는 것이기 때문이다.

예시: const 객체를 가리키는 const 포인터

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

const char* const ptr = "pointers";

f(ptr);
  • const 두 개의 의미에 대해서는 이곳을 참고하자.
    • 포인터 수정 불가, 포인터 null 배정 불가
    • 포인터가 가리키는 값 수정 불가
  • 포인터는 값으로 전달된다.
  • 연역 과정에 의해 const는 무시된다.
    • 즉, paramconst char*이 된다.

📐 배열 인수

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

// 배열은 배열의 첫 원소를 가리키는 포인터로 붕괴한다.
// const char name[] = const char* name
const char name[] = "J. P. Briggs";
const char* ptrToName = name;

f(name); // T = const char*
template<typename T>
void f(T& param)

f(name); // T = const char[13], param = const char(&)[13]
  • 1. ParamType이 포인터/참조 형식이지만, 보편 참조는 아닌 경우에 해당한다.
    • 이렇게 하면, 배열에 대한 참조로 선언할 수도 있다.
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T(&)[N]) noexcept {
	return N;
}

int keyVals[] = {1, 3, 7, 9, 11, 22, 35};
int mappedVals[arraySize(keyVals)];
std::array<int, arraySize(keyVals)> mappedVals;
  • constexpr로 선언하면, 함수 호출 결과를 컴파일 도중 사용할 수 있다.
  • 배열에 대한 참조로 선언한 경우, 배열 원소의 개수를 연역하는 템플릿을 만들 수 있다.

🔧 함수 인수

void someFunc(int, double);

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

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

// someFunc는 함수 포인터로 사용
f1(someFunc); // param = void(*)(int, double)
f2(someFunc); // param = void(&)(int, double)
  • 함수 형식도 함수 포인터 형태로 붕괴할 수 있다.

추가 내용:

  • 왼값 특별 규정, 포인터 붕괴 법칙으로 혼란이 가중되었다.
  • 컴파일러에게 어떤 형식을 연역했는지 얻는 방법은 항목 4에서 다룬다.

🧐 정리

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

Cpp 카테고리 내 다른 글 보러가기

댓글남기기