배고픈 개발자 이야기

[Effective Modern C++] Chapter 1 - Item 1 요약 본문

언어/Effective Mordern C++

[Effective Modern C++] Chapter 1 - Item 1 요약

이융희 2019. 10. 8. 14:49
728x90

 

 

Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14
Meyers, Scott

 

 

Chapter 1 : 형식 연역 (type deduction)

 

C++98에서는 형식 연역(type deduction)에 관한 규칙들이 한 종류밖에 없었는데, 바로 템플릿에 대한 것이었다. C++11에서는 그 규칙 집합을 조금 수정하고 새로운 규칙 집합을 두 개 더 추가했다.

 

하나는 auto를 위한 규칙들이고 또 하나는 decltype을 위한 규칙들이다. C++14에서는 auto와 decltype을 사용할 수 있는 문맥들이 확장되었다. 형식 연역의 적용 범위가 늘어난 덕분에, 이제는 자명한 또는 이미 언급된 형식들을 여러 번 일일이 지정해야 하는 경우가 크게 줄었다.

 

이제는 소스 코드의 한 지점에 있는 형식 하나를 변경하면 그 변화가 형식 연역을 통해서 다른 장소들로 자동으로 전파되며, 결과적으로 C++ 소프트웨어의 적응성이 높아졌다. 그러나 코드의 의미를 추론하기는 좀 더 어려워졌다. 컴파일러가 연역하는 형식이 언뜻 보고 추측한 것과는 다른 경우가 있기 때문이다.

 

형식 연역이 일어나는 방식을 확실하게 이해하지 않는다면, Modern C++에서 Effective한 프로그래밍이 거의 불가능하다.

사실 형식 연역이 일어나는 문맥은 너무 많다. 형식 연역은 함수 템플릿 호출 지점에서 항상 일어나며, auto가 등장하는 대부분의 상황과 decltype 표현식에서도 발생한다.

 

C++14의 경우에는 난해한 decltype(auto) construct가 쓰이는 곳에서도 형식 연역이 일어난다.

1장은 형식 연역에 관해 모든 C++개발자가 알아야 하는 정보를 제공한다.

 

형식연역의 작동 방식을 설명하고, 형식 연역에 기초한 auto와 decltype의 작동 방식들을 설명한다. 더 나아가서, 컴파일러 형식 연역 결과가 명시적으로 표시되게 만드는 방법도 이야기한다.

그런 결과를 눈으로 볼 수 있으면, 컴파일러가 연역한 형식이 독자가 애초에 원했던 것인지를 확인할 수 있다.

 

 

Item 1 : Understand template type deduction

 

템플릿 타입 추론은 모던 C++의 auto를 기반으로 한다. 따라서 auto를 이용하면 별다른 고민 없이 사용 가능하다. 문제는 템플릿 타입 추론 규칙을 auto에 적용할 때 직관적으로 이해하기 어려운 동작들이 존재한다는 것이다. 따라서 auto에 기반한 템플릿 타입 추론을 정확히 이해해야 한다. 

 

template<typename T>

void f(ParamType param);

f(expr);

 

컴파일하는 동안 컴파일러는 expr을 사용해 T와 ParamType, 두 타입을 추론한다. 하지만 ParamType은 const나 레퍼런스 타입과 같은 키워드를 포함하고 있기 때문에 T와 ParamType은 다르다.

 

template<typename T>

void f(Const T& param);        // ParamType은 const T&

 

int x = 0;

f(x);

 

예제에서는 T는 int로, ParamType은 const int&로 추론된다.
 

일반적으로, T에 대한 추론 타입은 함수에 전달되는 인수의 타입과 같다고 생각할 수 있다. 하지만, T에 대한 추론 타입은 expr 뿐만 아니라, ParamType에도 영향을 받는다.

 

ParamType에 따른 세가지 추론 시나리오

  • Case 1 : ParamType이 레퍼런스 또는 포인터 타입이지만, 유니버셜 레퍼런스 타입은 아닌 경우
  • Case 2 : ParamType이 유니버셜 레퍼런스인 경우
  • Case 3 : ParamType이 레퍼런스 또는 포인터가 아닌 경우

 

Case 1 : ParamType이 레퍼런스 또는 포인터 타입이지만, 유니버셜 레퍼런스 타입은 아닌 경우

 

1. expr의 타입이 레퍼런스라면, 레퍼런스 부분을 무시함.

2. T의 타입을 결정하기 위해 expr과 ParamType을 비교해 패턴 매칭.

 

template<typename T> 

void f(T& param);     // param is a reference

 

int x = 27;                // x is an int 

const int cx = x;       // cx is a const int 

const int& rx = x;      // rx is a reference to x as a const int

 

f(x);                     // T is int, param's type is int&

f(cx);                   // T is const int, param's type is const int&

f(rx);                    // T is const int, param's type is const int&

 

f의 매개변수 T&에서 const T&가 되면, 변화가 일어난다.

- cx와 rx를 호출할 때 T의 타입은 const int가 되어야 하지만

  그럴 경우 param의 타입이 const const int 가 되기 때문에 

  패턴 매칭을 통해 T를 const int에서 int로 변경.

 

template<typename T> 

void f(const T& param);  // param is now a ref-to-const

 

int x = 27;                    // as before 

const int cx = x;           // as before 

const int& rx = x;          // as before

 

f(x);                       // T is int, param's type is const int&

f(cx);                     // T is int, param's type is const int&

f(rx);                      // T is int, param's type is const int&

 

param이 레퍼런스 타입이 아닌 포인터 타입이더라도, 같은 방식으로 동작한다.

 

template<typename T> 

void f(T* param);        // param is now a pointer

 

int x = 27;                   // as before 

const int *px = &x;      // px is a ptr to x as a const int

 

f(&x);                       // T is int, param's type is int*

f(px);                       // T is const int, param's type is const int

 

Case 2 : ParamType이 유니버셜 레퍼런스인 경우

 

유니버셜 레퍼런스는 rvalue 레퍼런스(&&)와 같이 선언한다. 유니버셜 레퍼런스는 lvalue과 rvalue 모두를 받을 수 있다. rvalue 인수들이 전달되면 rvalue 레퍼런스로 추론한다. 하지만, lvalue 인수들을이 전달되면 전혀 다르게 행동한다.

- expr이 lvalue라면, T와 ParamType은 lvalue 레퍼런스로 추론됨.

  1. 템플릿 타입 추론에서 T가 레퍼런스 타입일 때만 일어날 수 있는 상황
  2. ParamType이 rvalue 레퍼런스 문법을 사용해 선언됐더라도, 추론된 타입은 lvalue 레퍼런스가 됨.

 

template<typename T> 

void f(T&& param);       // param is now a universal reference

 

int x = 27;                  // as before 

const int cx = x;         // as before 

const int& rx = x;        // as before

 

f(x);                      // x is lvalue, so T is int&, param's type is also int&

f(cx);                    // cx is lvalue, so T is const int&, param's type is also const int&

f(rx);                    // rx is lvalue, so T is const int&, param's type is also const int&

f(27);                   // 27 is rvalue, so T is int, param's type is therefore int&&

 

Case 3 : ParamType이 레퍼런스 또는 포인터가 아닌 경우

 

ParamType이 레퍼런스 또는 포인터가 아닌 경우, 매개변수는 값에 의한 호출(pass-by-value)이 된다. param은 전달된 값의 복사본이므로  새로운 오브젝트가 된다. 따라서 T는 expr로부터 다음과 같이 추론된다.

- 이전과 같이 expr의 타입이 레퍼런스라면, 레퍼런스 부분을 무시함.

- 만약 expr의 레퍼런스 부분을 무시한 후, expr이 const라면 const 부분도 무시함.

- 만약 volatile이라면, 이 부분 또한 무시함.

 

int x = 27;              // as before 

const int cx = x;     // as before 

const int& rx = x;    // as before

 

f(x);                     // T's and param's types are both int

f(cx);                   // T's and param's types are again both int

f(rx);                   // T's and param's types are still both int

 

param은 cx 및 rx와 다른 오브젝트이므로 param이 무엇이든 cx와 rx는 수정할 수 없다.

const 및 volatile은 템플릿에서 매개변수가 값에 의한 호출로 전달할 때만 무시된다. 매개변수가 const 레퍼런스 및 const 포인터라면, expr의 const는 타입 추론 과정에서 보존된다. 만약 expr이 const 오브젝트를 가리키는 const 포인터이고, expr이 값에 의한 호출로 param에 전달되면 어떨까?

 

template<typename T> 

void f(T param);                                         // param is still passed by value

const char* const ptr = "Fun with pointers";  // ptr is const pointer to const object

f(ptr);                                                       // pass arg of type const char * const

 

*의 오른쪽에 const : ptr의 상수화! (ptr이 다른 위치를 가리킬 수 없음)

*의 왼쪽에 const : ptr이 가리키는 값의 상수화! (문자열을 수정할 수 없음)

ptr이 가리키고 있는 것의 const는 타입 추론을 하는 동안 보존된다. 하지만, ptr 자체의 const는 새 포인터 param의 생성을 위해 복사할 때 무시된다.

 

Array Arguments

 

배열과 포인터는 서로 번갈아가며 사용할 수 있지만, 분명히 다른 타입이다. 배열은 첫 번째 요소를 가리키는 포인터로 붕괴될 수 있다.

 

const char name[] = "J. P. Briggs";   // name's type is const char[13]

const char * ptrToName = name;       // array decays to pointer

 

여기서 const char* 타입인 ptrToName은 const char[13] 타입인 name으로 초기화 된다. const char*와 const char[13]은 서로 같은 타입은 아니지만 배열 - 포인터 붕괴 규칙으로 인해 컴파일 된다.

만약 배열이 값의 의한 호출을 통해 템플릿의 매개변수로 전달된다면 무슨 일이 일어날까?

함수의 매개변수가 배열인 경우는 없다. 문법적으로 가능하지만 배열의 선언은 포인터의 선언으로 취급된다. 배열과 포인터의 동등성은 C++의 토대인 C에서 비롯된 결과이다.

 

void myFunc(int param[]);

void myFunc(int* param);            // 위와 동일

 

배열 매개변수 선언이 포인터 매개변수로 취급되기에 템플릿 함수에 값에 의한 호출로 전달된 배열 타입은 포인터 타입으로 추론된다.

 

f(name);                        // name은 배열이지만, T는 const char*로 추론됨.

 

하지만 배열을 참조하도록 매개변수를 선언한다면 어떻게 추론될까?

 

template<typename T> 

void f(T& param);       // template with by-reference parameter

f(name);                   // pass array to f

 

이 때 T는 const char[13]으로 추론되고,  f의 매개변수 타입은 const char(&)[13]이 된다.

배열의 레퍼런스 타입을 선언할 수 있는 기능은 템플릿이 배열에 포함된 요소들의 개수를 추론할 수 있도록 만들어 준다.

 

// return size of an array as a compile-time constant. (The 

// array parameter has no name, because we care only about 

// the number of elements it contains.) 

template<typename T, std::size_t N>                           // see info 

constexpr std::size_t arraySize(T (&)[N]) noexcept      // below on

{                                                                            // constexpr

  return N;                                                               // and 

}                                                                           // noexcept

 

Function Arguments

 

배열 뿐만 아니라 함수 타입들도 함수 포인터로 붕괴된다.

 

void someFunc(int, double);                              // someFunc is a function; type is void(int, double)

 

template<typename T> void f1(T param);             // in f1, param passed by value

template<typename T> void f2(T& param);           // in f2, param passed by ref

 

f1(someFunc);                 // param deduced as ptr-to-func; type is void (*)(int, double)

f2(someFunc);                 // param deduced as ref-to-func; type is void (&)(int, double)

 

Summary

  • 템플릿 타입 추론을 하는 동안 레퍼런스 타입의 인수들은 레퍼런스가 아닌 타입으로 취급된다.
  • 유니버셜 레퍼런스에 대한 타입 추론을 할 때 lvalue 인수들은 특별한 취급을 받게 된다.
  • 매개변수들은 대한 타입 추론을 할 때 const 및 volatile 인수들은 non-const 및 non-volatile으로 취급된다.
  • 템플릿 타입 추론을 하는 동안, 배열 및 포인터를 사용하는 인수들은 초기화할 때 레퍼런스 타입을 사용하지 않으면 포인터로 붕괴된다.

 

 

 

reference by https://devsong.tistory.com/121

Comments