Effective Modern C++ Item 1 내용 정리

Minsu Kim·2021년 11월 25일
0

Effective Modern C++

목록 보기
1/6
post-thumbnail

Chap 1. 형식 연역 (Type deduction)

Item 1. 템플릿 형식 연역 규칙 (template type deduction)

📌 Main Point

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

💡 함수 템플릿의 선언 및 호출

  • Modern C++의 아주 강력한 기능 중 하나인 autotemplate에 대한 형식 연역을 기반으로 작동함
    • C++98에서는 template, C++11에서는 auto에 대한 형식 연역이 가능
    • template 형식 연역 규칙들이 auto의 문맥에 적용될 때에는 template에 비해 덜 직관적인 경우가 있음
    • auto를 잘 활용하려면 auto가 기초하고 있는 템플릿 형식 연역을 제대로 이해해야 함

Ex) 일반적인 함수 템플릿의 선언 및 호출 방식

template<typename T>
void f(ParamType param); 	// 함수 템플릿 선언

f(expr);			// 어떤 표현식(expr)으로 f를 호출

👉 위 코드에 대해 컴파일러는 expr을 이용해서 두 가지 형식(T에 대한 형식 + ParamType에 대한 형식)을 연역함
👉 ParamType에 흔히 const나 참조 한정사 같은 수식어들이 붙기 때문에 두 형식이 다른 경우가 많음

Ex) 템플릿 선언 및 호출 예시

template<typename T>
void f(const T& param);		// ParamType은 const T&

int x = 0;
f(x);				// int로 f를 호출, expr로부터 T와 ParamType을 연역

👉 이 경우 Tint로 연역되지만 ParamTypeconst int&로 연역됨
👉 T에 대해 연역된 형식이 함수에 전달된 인수의 형식과 같을 것 (= Texpr의 형식)이라고 예상하는 것은 당연함 (위 코드에서 xint이고 Tint로 연역됨)
⛔ 하지만 항상 그런 것은 아님
🔑 T에 대해 연역된 형식은 expr의 형식에 의존할 뿐만 아니라 ParamType의 형태에도 의존함

💡 ParamType의 형태에 따른 T의 형식 연역

✔ Case 1 : ParamType이 포인터 또는 참조인 경우 (보편 참조 제외)

  • 가장 간단한 상황은 ParamType이 포인터 형식이나 참조 형식이지만 보편 참조는 아닌 경우임
  • 이 경우 형식 연역은 다음과 같이 진행됨
    1. 만약 expr이 참조 형식이면 참조 부분을 무시
    2. expr의 형식을 ParamType에 대해 패턴 부합 (pattern-matching) 방식으로 대응시켜서 T의 형식을 결정

Ex) Case 1-1. param이 참조 형식인 경우

template<typename T>
void f(T& param);	// param은 참조 형식

int x = 27; 		// x는 int
const int cx = x; 	// cx는 const int
const int& rx = x; 	// rx는 const int인 x에 대한 참조

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

👉 2-3번째 호출에서 cxrxconst 값이 배정되었기 때문에 Tconst int로 연역되었고, 매개변수 param의 형식은 const int&가 되었음
🔑 이것은 호출자에게 중요한 문제인데, const 객체를 참조 매개변수에 전달하는 호출자는 그 객체가 수정되지 않을 것이라고 기대함 (= 매개변수가 const에 대한 참조일 것이라고 기대함)
🔑 T& 매개변수를 받는 템플릿에 const 객체를 전달해도 안전한 이유가 바로 이것임
🔑 객체의 const성(constness; 상수성)은 T에 대해 연역된 형식의 일부가 됨
👉 3번째 호출에서 rx의 형식이 참조이지만 T는 비참조로 연역되었음 (형식 연역 과정에서 rx의 참조성이 무시되기 때문)

Ex) Case 1-2. paramconst에 대한 참조 형식인 경우

template<typename T>
void f(const T& param);		// param은 const에 대한 참조

int x = 27; 			// x는 int
const int cx = x; 		// cx는 const int
const int& rx = x; 		// rx는 const int인 x에 대한 참조

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

🔑 f의 매개변수의 형식을 T&에서 const T&로 바꾸면 상황이 조금 달라지긴 하지만,cxrxconst성은 계속 유지됨
🔑 paramconst에 대한 참조로 간주되므로, constT의 일부로 연역될 필요는 없음
👉 Case 1-1처럼 형식 연역 과정에서 rx의 참조성은 무시됨

Ex) Case 1-3. param이 포인터 형식(const를 가리키는 포인터)인 경우

template<typename T>
void f(T* param);	// param은 포인터

int x = 27; 		// x는 int
const int *px = &x; 	// px는 const int로서의 x를 가리키는 포인터

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

👉 param이 참조가 아니라 포인터(또는 const를 가리키는 포인터)라도 형식 연역은 본질적으로 같은 방식으로 진행

✔ Case 2 : ParamType이 보편 참조(universal reference)인 경우

  • 템플릿이 보편 참조 매개변수를 받는 경우 매개변수의 선언은 오른값 참조와 같은 모습(= 형식 매개변수 T를 받는 함수 템플릿에서 보편 참조의 선언 형식은 T&&임), 왼값 인수가 전달되면 오른값 참조와는 다른 방식으로 행동
  • 만약 exprlvalue(왼값)이면, TParamType 둘 다 왼값 참조로 연역됨 (이중으로 비정상적인 상황)
    • 템플릿 형식 연역에서 T가 참조 형식으로 연역되는 경우는 이것이 유일함
    • ParamType의 선언 구문은 오른값 참조와 같은 모습이지만, 연역된 형식은 왼값 참조임
  • 만약 exprrvalue(오른값)이면, 정상적인 (= Case 1의) 규칙들이 적용됨

Ex) Case 2. param이 보편 참조인 경우

template<typename T>
void f(T&& param);		// param은 보편 참조

int x = 27; 			// x는 int
const int cx = x; 		// cx는 const int
const int& rx = x; 		// rx는 const int인 x에 대한 참조

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

👉 이 예제에서는 보편 참조 매개변수에 관한 형식 연역 규칙들이 왼값 참조나 오른값 참조 매개변수들에 대한 규칙들과는 다르다는 점만 알고 넘어 감 (Item 24에서 자세한 이유를 설명)
🔑 보편 참조가 관여하는 경우에는 왼값 인수와 오른값 인수에 대해 서로 다른 연역 규칙들이 적용됨 (보편 참조가 아닌 매개변수들에 대해서는 그런 일이 절대 발생하지 않음)

✔ Case 3 : ParamType이 포인터도 참조도 아닌 경우

  • ParamType이 포인터도 아니고 참조도 아니라면, 인수가 함수에 값으로 전달 (pass-by-value)인 상황임
  • param은 주어진 인수의 복사본 (= 완전히 새로운 객체)임
  • param이 새로운 객체이기 때문에, exprT가 연역되는 과정에서 다음과 같은 규칙들이 적용
    1. 만약 expr의 형식이 참조이면, 참조 부분은 무시됨
    2. expr의 참조성을 무시한 후, 만약 exprconst이면 그 const 역시 무시함 (만약 volatile이면 그것도 무시함)

Ex) Case 3-1. param이 포인터도 참조도 아닌 경우

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

int x = 27; 			// x는 int
const int cx = x; 		// cx는 const int
const int& rx = x; 		// rx는 const int인 x에 대한 참조

f(x);				// T와 param의 형식은 둘 다 int
                    
f(cx);				// 여전히 T와 param의 형식은 둘 다 int
                    
f(rx); 				// 이번에도 T와 param의 형식은 둘 다 int

🔑 cxrxconst 값을 지칭하지만, 그래도 paramconst가 아님
👉 paramcxrx복사본이므로 (= paramcxrx와는 '완전히 독립적' 인 객체이므로) 당연한 결과임
👉 cxrx가 수정될 수 없다는 점은 param의 수정 가능 여부와는 전혀 무관함
👉 param의 형식을 연역하는 과정에서 exprconst성이(volatile 포함) 무시되는 이유임
👉 expr을 수정할 수 없다고 해서, 그 복사본까지 수정할 수 없는 것은 아님


Ex) Case 3-2. exprconst 객체를 가리키는 const 포인터이고 param에 값으로 전달되는 경우

template<typename T>
void f(T param);		// 인수는 param에 값으로 전달

const char* const ptr = 	// ptr는 const 객체를 가리키는 const 포인터
  "Fun with pointers"; 	

f(ptr); 			// const char * const 형식의 인수를 전달

🔑 ptr 선언의 별표(*) 오른쪽에 있는 const 때문에 ptr 자체는 const 가 됨
👉 즉, ptr를 다른 장소를 가리키도록 변경할 수 없으며, ptr에 널(null)을 배정할 수도 없음 (별표 왼쪽의 constptr이 가리키는 것, 즉 문자열이 const임을 뜻함 → 그 문자열은 변경할 수 없음)
👉 ptrf에 전달하면 그 포인터를 구성하는 비트들이 param에 복사됨
= 포인터 자체(ptr)는 값으로 전달됨 (형식 연역 과정에서 ptrconst성은 무시됨, 값 전달 방식의 매개변수에 관한 형식 연역 규칙과 일치함)
👉 결과적으로 param에 연역되는 형식은 const char*임 (= paramcosnt 문자열을 가리키는 수정 가능한 포인터)
🔑 형식 연역 과정에서 ptr가 가리키는 것의 const성은 보존되지만, ptr 자체의 const성은 ptr를 복사해서 새 포인터 param을 생성하는 도중에 사라짐

💡 배열, 함수 인수의 형식 연역

✔ 배열 인수

  • 비록 배열과 포인터를 구분하지 않고 사용할 수 있는 경우가 있긴 하지만, 배열 형식은 포인터 형식과 다름
  • 배열과 포인터를 맞바꿔 쓸 수 있는 것처럼 보이는 것의 원인은, 배열배열의 첫 원소를 가리키는 포인터붕괴한다(decay)는 점임

Ex) 배열과 배열의 첫 원소를 가리키는 포인터

const char name[] = "J. P. Briggs"; 	// name의 형식은 const char[13]

const char * ptrToName = name; 		// 배열이 포인터로 붕괴

👉 const char* 형식의 포인터 ptrToNamename으로 초기화하는데, name 자체는 const char[13] 형식의 배열임
👉 두 형식(const char*const char[13])은 서로 같지 않지만, 배열에서 포인터로의 붕괴 규칙 때문에 이 코드가 형식 불일치 오류 없이 컴파일됨

Ex) 값 전달 매개변수를 받는 템플릿에 배열을 전달하면?

template<typename T>
void f(T param); 			// 값 전달 매개변수가 있는 템플릿

const char name[] = "J. P. Briggs"; 	// name의 형식은 const char[13]

f(name); 				// T와 param에 대해 연역되는 형식들은?
					// name은 배열이지만 T는 const char*로 연역됨

🔑 배열 형식의 함수 매개변수라는 것은 없음!

void myFunc(int param[]); 	// 이 구문 자체는 문제가 없지만, 
				// 이 경우 배열 선언은 하나의 포인터 선언으로 취급됨
                
void myFunc(int* param); 	// 위와 동일한 함수

👉 배열 매개변수 선언이 포인터 매개변수처럼 취급되므로, 템플릿 함수에 값으로 전달되는 배열의 형식은 포인터 형식으로 연역됨 (즉, 템플릿 f의 호출에서 형식 배개변수 Tconst char*로 연역됨)

Ex) 함수의 매개변수를 진짜 배열로 선언할 수는 없지만, 배열에 대한 참조로 선언할 수는 있음

template<typename T>
void f(T& param); 			// 참조 전달 매개변수가 있는 템플릿

const char name[] = "J. P. Briggs"; 	// name의 형식은 const char[13]

f(name); 				// 배열 name을 f에 전달

🔑 위의 코드처럼 템플릿 f가 인수를 참조로 받도록 수정하고, 함수에 배열을 전달하면 T에 대해 연역된 형식은 배열의 실제 형식이 됨
👉 그 형식은 배열의 크기를 포함하므로, 위 코드에서 Tconst char [13]으로 연역되고, f의 매개변수의 형식은 const char (&)[13]으로 연역됨

Ex) 배열에 대한 참조를 선언하는 능력을 이용하면 배열에 담긴 원소들의 개수를 연역하는 템플릿을 만들 수 있음

// 배열의 크기를 컴파일 시점 상수로서 돌려주는 템플릿 함수
// (배열 매개변수에 이름을 붙이지 않은 것은, 이 템플릿에
// 필요한 것은 배열에 담긴 원소의 개수뿐이기 때문)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
	return N;
}

🔑 Item 15에서 설명하겠지만, 이 함수를 constexpr로 선언하면 함수 호출의 결과를 컴파일 도중에 사용할 수 있게 됨 (arraySizenoexcept로 선언한 것은 컴파일러가 더 나은 코드를 산출하는 데 도움을 주려는 것임, Item 14에서 자세한 내용 알 수 있음)
👉 그러면 아래 코드처럼 중괄호 초기화 구문으로 정의된, 기존 배열과 같은 크기(원소 개수)의 새 배열을 선언하는 것이 가능해짐

int keyVals[] = {1, 3, 7, 9, 11, 22, 35}; 	// keyVals의 원소 개수는 7

int mappedVals[arraySize(keyVals)]; 		// mappedVals의 원소 개수 역시 7
std::array<int, arraySize(keyVals)> mappedVals 	// 내장 배열 대신 std::array를 사용한 경우

✔ 함수 인수

  • C++에서 함수 형식도 함수 포인터로 붕괴할 수 있으며, 배열에 대한 형식 연역과 관련해서 다룬 모든 것은 함수에 대한 형식 연역에, 함수 포인터로의 붕괴에 적용됨

Ex) 함수에 대한 형식 연역에 배열에 대한 형식 연역을 적용

void someFunc(int, double); 	// someFunc는 하나의 함수
				// 형식은 void(int, double)

template<typename T>
void f1(T param); 		// f1의 param은 값 전달 방식

template<typename T>
void f2(T& param); 		// f2의 param은 참조 전달 방식

f1(someFunc); 			// param은 함수 포인터로 연역됨
				// 형식은 void (*)(int, double)
f2(someFunc); 			// param은 함수 참조로 연역됨
				// 형식은 void (&)(int, double)

👉 배열에서 포인터로의 붕괴를 알고 있다면 함수에서 포인터로의 붕괴도 알아 두는 것이 좋음

profile
📚 TIL & MISC

0개의 댓글