Effective Modern C++ Item 3 내용 정리

21504 단어 cppcpp

Chap 1. 형식 연역 (Type Deduction)

Item 3. decltype (declared type)

📌 Main Point

  • decltype은 항상 변수나 표현식의 형식을 아무 수정 없이 보고함
  • decltype은 형식이 T이고 이름이 아닌 왼값 표현식에 대해서는 항상 T& 형식을 보고함
  • C++14는 decltype(auto)를 지원함
    • decltype(auto)auto처럼 초기치로부터 형식을 연역하지만, 그 형식 연역 과정에서 decltype의 규칙들을 적용

💡 decltype 이란

  • decltype주어진 이름이나 표현식의 형식을 알려줌
  • 템플릿과 auto의 형식 연역 (Item 1Item 2)에서 일어나는 일과 달리, decltype주어진 이름이나 표현식의 구체적인 형식을 그대로 알려줌

Ex) decltype의 결과

const int i = 0; 		// decltype(i)는 const int

bool f(const Widget& w); 	// decltype(w)는 const Widget&
				// decltype(f)는 bool(const Widget&)

struct Point {
	int x, y; 		// decltype(Point::x)는 int
}; 				// decltype(Point::y)는 int

Widget w; 			// decltype(w)는 Widget

if (f(w))// decltype(f(w))는 bool

template<typename T> 		// std::vector를 단순화한 버전
class vector {
public:
…
T& operator[](std::size_t index);};

vector<int> v; 			// decltype(v)는 vector<int>if (v[0] == 0)// decltype(v[0])는 int&

👉 위 코드에서 decltype은 예상한대로의 형식을 알려줌

💡 decltype을 이용한 반환 형식

  • C++11에서 decltype함수의 반환 형식이 그 매개변수 형식들에 의존하는 함수 템플릿을 선언할 때 주로 사용됨
  • 예를 들어 컨테이너 하나와 색인 하나를 받고, 대괄호 색인화를 통해서 (= 컨테이너[색인]구문을 이용해서) 컨테이너의 한 요소를 돌려주는 함수를 작성할 때, 함수의 반환 형식은 반드시 색인화 연산의 반환 형식과 동일해야 함
  • 대체로 형식 T의 객체들을 담은 컨테이너에 대한 operator[] 연산은 T&를 돌려줌 (e.g. std::deque, std::vector)
  • 하지만 std::vector<bool>에 대한 operator[]bool&이 아니라 완전히 새로운 객체를 돌려줌 (자세한 내용은 Item 6에서 설명)
  • 여기서 중요한 점은, 컨테이너의 operator[]의 반환 형식이 컨테이너에 따라 다를 수 있음
  • decltype을 이용하면 그런 함수의 반환 형식을 손쉽게 표현할 수 있음

Ex) decltype을 이용해서 반환 형식을 계산하는 방법

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) 	// C++11
-> decltype(c[i])
{
	authenticateUser();
    	return c[i];
}

🔑 함수 이름 앞에 auto를 지정하는 것은 형식 연역과는 아무런 관련이 없음
🔑 함수 이름 앞의 auto는 C++11의 후행 반환 형식(trailing return type) 구문이 쓰인다는 점을 (즉, 함수의 반환 형식을 이 위치가 아니라 매개변수 목록 다음에 = "->" 다음 위치에) 선언하겠다는 점을 나타냄
👉 후행 반환 형식 구문에는 반환 형식을 매개변수들을 이용해서 지정할 수 있다는 장점이 있음
⛔ 위 코드에서 일반적인 방식으로 함수 이름 앞에서 반환 형식을 지정한다면, ci는 사용할 수 없음 (둘 다 아직 선언되지 않았기 때문)
🔑 C++11은 람다 함수가 한 문장으로 이루어져 있다면 그 반환 형식의 연역을 허용
🔑 C++14는 허용 범위를 더욱 확장해서 모든 람다와 모든 함수의 반환 형식 연역을 허용 (return 문이 여러 개인 함수도 허용 - 단, 모든 return 문의 형식 연역 결과가 일치해야 함)

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) 	// C++14
{
	authenticateUser();
	return c[i]; 				// 반환 형식은 c[i]로부터 연역됨
}

👉 authAndAccess의 경우 C++14에서 후행 반환 형식을 생략하고 함수 이름 앞의 auto만 남겨 두어도 됨
👉 이러한 형태의 선언에서는 실제로 auto가 형식 연역이 일어남을 뜻하는 용도로 쓰임 (auto는 컴파일러가 함수의 구현으로부터 함수의 반환 형식을 연역할 것임을 뜻함)
🔑 Item 2에서 설명했듯이, 함수의 반환 형식에 auto가 지정되어 있으면 컴파일러는 템플릿 형식 연역을 적용 (T 객체들을 담은 컨테이너에 대한 operator[] 연산은 대부분의 경우에는 T&를 돌려줌)
Item 1에서 설명했듯이, 템플릿 형식 연역 과정에서 초기화 표현식의 참조성이 무시된다는 점이 문제임

std::deque<int> d;authAndAccess(d, 5) = 10; 	// 사용자를 인증하고, d[5]를 돌려주고,
				// 그런 다음 10을 d[5]에 배정함
				// 이 코드는 컴파일 되지 않음!

👉 위 코드에서 d[5]int&를 돌려주나, authAndAccess에 대한 auto 반환 형식 연역 과정에서 참조가 제거되기 때문에 결국 반환 형식은 int가 됨
👉 함수의 반환값으로서의 이 int는 하나의 오른값이며, 결과적으로 위 코드는 오른값 int10을 배정하려 함 (이 것은 C++에서 금지되어 있기 때문에, 코드는 컴파일되지 않음)

💡 decltype(auto)을 이용한 반환 형식

  • authAndAccess가 원하는 대로 작동하려면, 함수의 반환 형식에 decltype 형식 연역이 적용되게 만들어야 함 (authAndAccessc[i]의 반환 형식과 정확히 동일한 형식을 반환하게 만들어야 함)
  • C++14의 decltype(auto) 지정자를 통해서 형식 추론이 일어나는 일부 경우에서 decltype 형식 연역 규칙들이 적용되도록 함 (auto해당 형식이 연역되어야 함을, decltype그 연역 과정에서 decltype 형식 연역 규칙들이 적용되어야 함을 뜻함)

Ex) decltype(auto)을 이용해서 반환 형식을 계산하는 방법

template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container& c, Index i) 	// C++14
{
	authenticateUser();
	return c[i];
}

👉 authAndAccess의 반환 형식은 실제로 c[i]의 반환 형식과 일치함
🔑 decltype(auto)를 함수 반환 형식 외에도 변수 선언 및 초기화 표현식에 decltype 형식 연역 규칙들을 적용하고 싶은 경우 사용 가능

Widget w;

const Widget& cw = w;

auto myWidget1 = cw; 			// auto 형식 연역: myWidget1의 형식은 Widget

decltype(auto) myWidget2 = cw; 		// decltype 형식 연역:  myWidget2의 형식은 const Widget&

Ex) authAndAccess의 정련 방법

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);

👉 컨테이너 c는 비const 객체에 대한 왼값 참조로서 함수에 전달됨 → 함수가 돌려준 컨테이너 요소를 클라이언트가 수정할 수 있게 하기 위한 것임 (이로 인해 함수에 오른값 컨테이너는 전달할 수 없다는 것이 문제)

std::deque<std::string> makeStringDeque(); 	// 팩터리 함수
// makeStringDeque가 돌려준 deque의
// 5번째 원소의 복사본을 만듦
auto s = authAndAccess(makeStringDeque(), 5);

👉 이런 용법을 지원하려면 authAndAccess가 왼값뿐만 아니라 오른값도 받아들이도록 선언을 고쳐야 함
⛔ 왼값 참조 매개변수를 받는 버전, 오른값 참조 매개변수를 받는 버전을 따로 만들어서 사용할 수도 있지만 이것은 별로임
🔑 왼값과 오른값 모두에 묶일 수 있는 참조 매개변수를 authAndAccess에 도입해서 위의 방법을 피함 (Item 24에서 다루는 보편 참조가 이러한 용도에 사용됨)

// 보편 참조 매개변수를 받도록 authAndAccess 선언 수정
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i); 	// c가 보편 참조

// 수정된 선언에 맞게 템플릿의 구현도 수정해야 함 (Item 25에서 다룰 std::forward 적용)
template<typename Container, typename Index>
decltype(auto)
authAndAccess(Container&& c, Index i) // C++14 최종 버전
{
	authenticateUser();
	return std::forward<Container>(c)[i];
}

🔑 위의 코드를 사용하려면 C++14 컴파일러가 필요하기 때문에, C++11 버전에서는 반환 형식을 명시적으로 지정하는 것으로 구현 부분을 수정

template<typename Container, typename Index>
auto
authAndAccess(Container&& c, Index i) // C++11 최종 버전
-> decltype(std::forward<Container>(c)[i])
{
	authenticateUser();
	return std::forward<Container>(c)[i];
}

💡 decltype의 예외 사항

  • 이름보다 복잡한 왼값 표현식에 대해서는 일반적으로 decltype이 항상 왼값 참조를 보고함 (이름이 아닌, 그리고 형식이 T인 어떤 왼값 표현식에 대해 decltypeT&를 보고함)
  • 대부분의 왼값 표현식에는 왼값 참조가 포함되어 있기 때문에 이 점으로 인해 달라지는 경우는 드물지만 간혹 있음

Ex) 왼값 표현식에 따른 decltype 차이

int x = 0;

👉 위 코드에서 x는 변수의 이름이기 때문에 decltype(x)int
👉 x를 괄호로 감싸서 (x)를 만들면 이름보다 복잡한 표현식이 됨 (이름으로서의 x는 하나의 왼값이며, C++은 (x)라는 표현식도 왼값으로 정의함)
👉 따라서 decltype((x))int&
⛔ C++11에서는 이러한 현상이 큰 문제를 일으키진 않지만, decltype(auto)를 지원하는 C++14에서는 return 문 작성 방식에 따라 함수의 반환 형식 연역 결과가 달라질 수 있음

decltype(auto) f1()
{
	int x = 0;return x; 	// decltype(x)는 int → f1은 int를 반환
}
decltype(auto) f2()
{
	int x = 0;return (x); 	// decltype((x))는 int& → f2은 int&를 반환
}

f2f1과는 다른 형식을 돌려준다는 점뿐만 아니라, 자신의 지역 변수에 대한 참조를 돌려준다는 점도 주의해야 함

좋은 웹페이지 즐겨찾기