2017년 2월 27일 월요일

decltype에 대하여

 Effective Modern C++에는 decltype에 대한 설명이 잘 나와있다. 책을 읽기 전까지는 언제 이런 키워드가 사용되는지 궁금할 정도로 거의 쓰이지 않을 것 같았는데 이 책을 읽고나니 나름 쓰임새가 있다는 것을 알 수 있었고 그 동작에 대해서도 깊게 이해할 수 있었던 것 같다.

 decltype은 declared type의 줄임말이다. 즉 선언된 타입을 알려주는 키워드이다. 이것도 템플릿이나 auto 키워드 처럼 컴파일타임에 유효한 것이지 런타임에 동적으로 타입을 식별해서 알려주는 것은 아니니 decltype을 이용해서 런타임에 동적으로 뭔가 하려고 했다면 생각을 바꿔야할 것이다. 최근에 나오는 많은 프로그래밍 언어들이 런타임에 동적으로 타입을 체크할 수 있는 기능들을 대부분 제공하다보니 decltype에 대해서 오해할 소지가 있기에 컴파일 타임에 정적으로 타입이 결정되어 사용된다는 것을 확실히 하고 가는 것이 좋겠다.

 먼저 간단히 아래 코드의 주석을 참고하면 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의 동작은 별로 예상과 다르지 않다. 게다가 별로 쓰일만한 곳도 없어 보인다. 일반적으로 컴파일러가 알아서 타입을 추론하도록 할 때는 auto 키워드가 쓰이기 때문에 decltype으로 타입을 알아내야 할 때가 있을까 싶기도 하다.

 책에서 말하는 decltype의 대표적인 쓰임새는 함수 템플릿의 반환값이 템플릿 인자에 의존하여 타입이 결정되지 않은 값을 반환해야할 때이다. 아래 예제를 보자. 이 예제는 일반적인 컨테이너의 []연산자와 같은 동작을 하는 함수를 구현한 것인데 값을 반환하기 전에 사용자 인증을 수행한다는 가정하에 구현된 함수이다.
template<typename Container, typename Index> // 완벽한 구현은 아니지만 뒤에 보완할 것임.
auto authAndAccess(Container& c, Index i)
  -> decltype(c[i])
{
  authenticateUser();
  return c[i];
}
 함수의 이름 앞에 auto가 지정되어 있다. 사실 이런 auto는 타입 추론과 아무 상관이 없으며 trailing return type 문법이 사용된다는 표시일 뿐이다. 이렇게 trailing return type 방식을 사용하면 함수의 인자들을 return 타입을 지정할 때에 이용할 수 있다는 장점이 있다. 위 예에서는 return 타입으로 decltype(c[i])를 사용함으로써 템플릿 인자 Container와 Index의 타입에 상관없이 return 타입을 지정할 수 있었다. 이런 식으로 decltype이 사용되는 것이다. 참고로 일반적인 컨테이너 c의 원소 타입이 T라면 함수의 return 타입은 T&이 된다.

 여기서 전에 올린 auto 타입 추론에서 설명했던 것처럼 C++14에서는 함수의 return 타입으로 auto를 지정하여 타입 추론이 되도록 할 수 있으므로 C++14에서는 아래와 같이 수정할 수 있다.
template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)  // C++14. 사실 제대로된 구현은 아님.
{
  authenticateUser();
  return c[i]; // return 타입은 c[i]로부터 추론된다.
}
 이렇게 수정하면 C++14 컴파일러는 함수의 return문을 분석하여 타입 추론을 수행한다. auto 타입 추론에서 설명했듯이 이 경우 타입 추론은 템플릿 타입 추론과 같은 방식으로 이루어지며 따라서 c[i]의 referenceness가 제거되어 타입이 결정된다. 즉 c[i]의 타입은 T&지만 함수의 return 타입은 T로 추론되는 것이다. 일반적으로 []연산자는 원소의 참조형을 반환해야 하는데 위 구현은 원소의 복사본이 반환되게 된다.

 C++14에서는 이런 경우를 극복할 수 있도록 decltype(auto)를 사용하도록 하였다. 아래 예제를 보자.
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i)  // C++14.
{
  authenticateUser();
  return c[i]; 
}
 decltype(auto)를 return타입으로 지정하면 반환되는 표현식을 그대로 decltype에 넘긴 것과 같은 타입으로 추론된다. 따라서 decltype(c[i])의 타입 그대로 참조형(T&)을 반환하게 되는 것이다. 이런 식의 타입 추론을 "decltype 타입 추론"이라 한다.

책에서는 따로 설명하지 않았지만 위 예제의 경우 항상 참조형을 반환해야 한다면 decltype(auto)대신 auto&를 지정해도 문제가 되지 않는다. 하지만 혹시 c[i]가 참조형이 아니라 비-참조형 값이라면 또 다시 문제가 발생하게 된다. 왜냐하면 Container의 []연산자가 특별히 비-참조형을 반환하도록 설계되어 있다면 반환되는 값은 c[i]의 복사본이 될 텐데 복사본의 참조형을 반환하는 건 위험한 일이기 때문이다. 역시 decltype(auto)가 참조형을 반환하던 비-참조형을 반환하던 상관없이 문제를 일으키지 않는 유일한 해결책이다.

decltype(auto)는 함수의 리턴 타입에서만 사용되는 것은 아니다. 아래 예제에서 처럼 일반 변수의 선언에서도 사용될 수 있다.
Widget w;
const Widget& cw = w;

auto myWidget1 = cw;           // auto 타입 추론:
                               // myWidget1은 Widget

decltype(auto) myWidget2 = cw; // decltype 타입 추론:
                               // myWidget2는 const Widget&
 참고로 책에서는 위의 authAndAccess() 예제가 rvalue에 대해서도 완전하게 동작하려면 아래 예와 같이 Universal 참조와 std::forward를 사용하여 구현해야 한다고 설명하지만 decltype(auto)에 추가적인 의미를 부여하는 것은 아니기 때문에 설명은 따로 하지 않겠다.
template<typename Container, typename Index> // final C++14 version
decltype(auto) authAndAccess(Container&& c, Index i)
{
  authenticateUser();
  return std::forward<Container>(c)[i];
}

 decltype(auto)에 대해 한 가지 더 주의해야 할 것이 있다. 대부분 decltype이 개발자의 예상대로 동작하지만 개발자의 예상을 종종 빗나가는 경우가 있다. 일반적인 변수의 이름에 decltype을 적용하면 실제 그 변수가 선언된 타입이 도출된다. 이 경우는 아무런 문제가 없다. 그러나 단순 이름이 아닌 "lvalue 표현식"을 decltype에 넘겼을 때 나오는 타입이 문제가 된다. 이 경우 lvalue 표현식이 평가되는 타입은 lvalue 참조형이다. 실제로 lvalue 표현식의 타입은 lvalue 참조형이 맞기 때문에 사실 잘못된 건 아니다. 하지만 아래 예를 보면 충분히 헷갈릴만 한 경우가 있음을 알 수 있다.
int x = 0;

decltype(auto) a = x;   // a는 int
decltype(auto) b = (x); // b은 int&. "(x)"는 lvalue 표현식이기 때문에

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& 반환
}
위 예제에서 보듯이 단순한 이름이 아닌 lvalue 표현식의 타입은 lvalue 참조형으로 평가되기 때문에 "(x)"가 lvalue 참조형으로 평가되는 것이다. 이것이 f1과 f2의 커다란 차이를 만들게 된다. 이미 짐작하고 있겠지만 f2는 지역 변수의 참조형을 반환하는 위험한 동작을 하는 잘못된 함수라는 것이다. 이 처럼 decltype(auto)를 사용할 때 주어지는 표현식은 주의깊게 작성해야 함을 기억하기 바란다.

 이렇게 C++11, C++14 버전에서 새롭게 추가된 타입 추론의 동작에 대해서 정리를 해보았다. 혹시 이전 내용이 궁금하다면 아래 링크를 참고하기 바란다.

  1. 템플릿 타입 추론
  2. auto 타입 추론

2017년 2월 23일 목요일

auto Type Deduction (auto 타입 추론)

 이번 글은 C++11에 새롭게 추가된 auto 키워드에 관해 이야기해 보려고 한다. auto라는 단어가 뜻하는 것 처럼 누군가 자동으로 무엇인가를 해줄 것이라는 걸 예상할 것이다. 그렇다. auto 키워드는 타입이 들어갈 자리에 대신 들어가서 (템플릿에서 그랬던 것처럼) 컴파일러가 주변 코드를 이용해서 타입을 대신 결정하도록 하는 키워드이다. Effective Modern C++에서도 역시 심도있게 다루고 있으며 그 내용을 기반으로 내가 이해하는 바를 여기서 기록하고자 한다.

 auto가 수행하는 작업이 곧 템플릿 인자의 타입을 결정하는 것과 거의 같기 때문에 템플릿 타입 추론에 관해서 잘 모르는 상태라면 이번 글을 읽기 전에 이전에 올린 "템플릿 타입 추론"에 관한 글을 읽고 이해해 두는 것이 좋을 것 같다.

 그럼 먼저 auto의 타입 추론과 템플릿의 타입 추론이 얼마나 같은지(사실 약간 다르다) 간단한 설명을 보도록 하자. 먼저 템플릿 함수의 전형적인 예는 아래와 같다.
template<typename T>
void f(T param);

f(expr); // expr은 임의의 표현식
 일반적인 auto 선언문은 아래와 같은 형태이다.
auto x = expr; // expr은 임의의 표현식
 컴파일러는 템플릿 함수 f()에 넘기는 인자 expr의 타입에 기초하여 T의 타입 추론을 수행한다. 이 때는 "템플릿 타입 추론" 방식으로 동작하게 되고 그 동작에 대한 설명은 여기를 참조하기 바란다. 자 그 다음 auto의 예를 보면 바로 느낌이 올 것이다. auto에서도 역시 expr의 타입에 기초하여 타입 추론을 수행하는데 그 규칙은 템플릿의 그것과 동일하다. 즉, auto가 템플릿의 T역할을 하는 것이다.

 간단히 몇 가지 비교를 해보면 이해가 더 쉬울 것이다. 아래 예를 보자.
template<typename T>
void f1(T param);

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

template<typename T>
void f3(const T& param);

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

auto a1 = expr;        // f1(expr)과 같은 타입 추론 수행
auto& a2 = expr;       // f2(expr)과 같은 타입 추론 수행
const auto& a3 = expr; // f3(expr)과 같은 타입 추론 수행
auto&& a4 = expr;      // f4(expr)과 같은 타입 추론 수행
 위 예에서 auto와 T의 역할을 비교해보면 auto가 "템플릿 타입 추론" 방식으로 결정된다는 게 별로 이상한 일이 아님을 알 수 있을 것이다. auto의 타입 추론 방식은 심지어 배열과 함수가 지정됐을 때 조차도 템플릿과 같은 방식으로 타입을 결정한다. 아래 예를 보자.
const char name[] = "R. N. Briggs"; // name의 타입은 const char[13]
auto arr1 = name; // arr1의 타입은 const char*
auto& arr2 = name; // arr2의 타입은 const char(&)[13]

void someFunc(int, double); // someFunc는 function
                            // 타입은 void(int, double) 
auto func1 = someFunc; // func1의 타입은 void (*)(int, double)
auto& func2 = someFunc; // func2의 타입은 void (&)(int, double)
 템플릿 타입 추론에서 비-참조형과 참조형의 인자에 함수나 배열을 넘겼을 때의 동작은 auto 타입 추론에서도 그대로 적용된다. 즉 비-참조형 인자에 함수나 배열을 넘기면 포인터형으로 바뀌어 추론된다. 이 정도면 "auto 타입 추론"은 "템플릿 타입 추론"과 같다고 할 만 하다.

 하지만 auto는 템플릿 타입 추론과 한 가지 다른 점이 있는데 바로 braced initializer가 초기화값으로 주어졌을 때이다. 아래 예를 보면 auto의 타입이 예상과 다르게 결정되는 걸 볼 수 있다.
int x1 = 27;
int x2(27);
int x3 = { 27 }; // C++11, uniform initialization
int x4{ 27 }; // C++11, uniform initialization

auto a1 = 27; // 타입은 int, 값은 27
auto a2(27);  // 타입은 int, 값은 27
auto a3 = { 27 }; // 타입은 std::initializer_list<int>, 값은 {27}
auto a4{ 27 };    // 타입은 std::initializer_list<int>, 값은 {27}
 C++11에서 소개된 uniform initialization 덕분에 위의 x1, x2, x3, x4가 모두 가능한 선언이다. 하지만 똑같은 방식으로 auto 선언문으로 바꾸면 결과는 달라진다. 주석에서 볼 수 있듯이 a3, a4는 int형이 아니고 std::initializer_list<int>형으로 추론되기 때문이다. braced initializer를 이미 사용해 본 적이 있다면 컴파일러가 상황에 맞춰서 그 값들을 std::initializer_list형으로 생성한다는 걸 이미 알고 있을 것이다. auto 선언문에서도 컴파일러는 braced initializer를 std::initializer_list형으로 바꿔서 타입 추론을 수행한다. 컴파일러의 그런 동작을 이미 알고 있었다면 어느 정도 이해가 되는 상황일 것이다.

 위와 같이 auto는 braced initializer를 std::initializer_list형으로 인식하고 타입 추론을 수행하지만 템플릿에 braced initializer를 넘기면 타입 추론을 하지 못하고 컴파일에 실패한다. 이 점이 두 타입 추론의 차이점이다. 즉 아래 예에서 f()의 호출은 실패한다.
// auto 타입 추론
auto x = { 11, 23, 9 }; // x의 타입은 std::initializer_list형으로 추론됨

// 템플릿 타입 추론
template
void f(T param);

f({ 11, 23, 9 }); // error! 타입 추론을 할 수 없음.

 C++14에서는 한 가지 더 짚고 가야할 것이 있다. C++14부터는 auto를 함수의 return 타입과 람다의 인자에서도 사용이 가능한데 이 때는 braced initializer를 std::initializer_list형으로 인식하지 않고 컴파일을 실패하게 된다. 즉 아래 두 가지 경우에 대해서는 (auto 타입 추론이 아닌) "템플릿 타입 추론"을 수행한다.
auto createInitList() {
    return { 1, 2, 3 }; // error: { 1, 2, 3 }을 타입 추론할 수 없음.
}
std::vector<int> v;
...

auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
...

resetV({ 1, 2, 3 }); // error! { 1, 2, 3 }을 타입 추론할 수 없음.
 위의 두 예에서 모두 auto는 에러가 난다. "템플릿 타입 추론"과 똑같이 braced initializer를 통해서 타입 추론을 하지 못하는 것을 볼 수 있다. 이렇게 보면 auto가 타입 추론되는 방식은 변수를 선언할 때만 braced initializer를 인식(std::initializer_list)한다는 점을 제외하면 템플릿 타입 추론과 완전히 동일하다.

 마지막으로 간단히 요약해 보자면...
  1. auto 타입 추론은 템플릿 타입 추론과 거의 같지만 braced initializer를 std::initializer_list형으로 인식해서 추론을 수행한다는 점이 다르다. (템플릿 타입 추론은 braced initializer를 통해서는 타입 추론을 실패함)
  2. auto가 함수의 return 타입, 람다의 인자에 사용되었을 때는 템플릿 타입 추론과 완전히 동일하게 동작한다.(즉 braced initializer를 통한 타입 추론은 실패한다)

2017년 2월 21일 화요일

Template Type Deduction (템플릿 타입 추론)

 수많은 C++관련 도서가 있고 그 수 만큼 많은 저자들이 있지만 그 중에 내가 인상깊게 본 도서들을 꼽자면 Effective 시리즈를 꼽을 수 있겠다. 전부 다 본건 아니지만 그래도 Effective 시리즈 작가인 Scott Meyers의 책을 볼 기회가 생긴다면 기꺼이 시간을 내서 볼 용의가 있다.

 최근에 보게된 Effective Modern C++도 상당히 인상적이다. C++11부터 크게 발전해가는 C++을 제대로 사용해 보려고 여기 저기 인터넷을 뒤져가며 새로운 문법을 익히다보니 머리속이 복잡해지는 걸 느끼고 있던 참에 여러가지 복잡하게 얽힌 생각들을 대부분 깔끔하게 정리할 수 있도록 도와준 책이었다.

 그래서 정리된 것들을 내 머리속에 오래 유지할 수 있도록 몇 번에 걸쳐 포스팅을 해볼까 한다. 그 첫 번째로 C++이 수행하는 타입 추론(Type Deduction)에 관해서 정리해보자.

 C++11이 나오기 전에는 템플릿(Template)말고는 타입 추론이 수행되는 경우가 없었지만 C++11에 auto와 decltype이 추가되면서 두 가지가 더 늘었다. 조금 정확히 얘기하자면 타입 추론은 C++ 컴파일러가 개발자가 지정하지 않은 타입을 주변의 다른 코드를 이용해서 결정하는 작업이다. 따라서 컴파일러가 개발자의 의도대로 움직여주지 않으면 엉뚱한 결과를 초래하는 경우도 종종 발생한다. 사실 컴파일러가 오동작하는 건 아니고 개발자가 컴파일러의 규칙을 제대로 알지 못하기 때문에 생기는 문제이며 그 때문에 그 규칙을 여기에 정리하려고 하는 것이다.

 우선 이번 글에서는 템플릿의 타입 추론에 대해서 얘기해 보고자한다.

 일반적인 함수 템플릿은 아래와 같은 형식으로 선언될 것이다.
template<typename T>
void f(ParamType param);
 여기서 ParamType은 'const T&'같은 T를 이용하는 타입이 될 것이다.

 그리고 이 함수를 호출할 때는 아래와 같은 형태로 호출될 것이다.
f(expr);
 이 때 컴파일러는 expr의 타입에 기초하여 ParamType과 T의 타입추론을 수행한다.

ParamType이 다음의 3가지 경우에 대해서 다르게 동작하므로 개발자는 이 규칙을 정확히 이해하고 있어야 타입 추론이 엉뚱하게 되는 경우를 피할 수 있을 것이다.
  1. ParamType이 Universal 참조가 아닌 참조형(Reference)인 경우 (T&)
  2. ParamType이 Universal 참조형인 경우 (T&&)
  3. ParamType이 참조형이 아닌 경우 (T)
차례대로 하나 씩 살펴보자.

1. ParamType이 Universal 참조가 아닌 참조형(T& 또는 const T&)인 경우
 이 경우가 가장 간단한 경우로 책에서는
  1) expr의 타입에서 참조를 제거(있다면)하고
  2) 그 타입을 ParamType과 패턴 매칭시켜서 T의 타입을 결정(추론)
하면 된다.

 패턴 매칭이라는 용어를 썼지만 패턴 매칭이 어떻게 이루어지는 지에 대한 설명이 없다. 내가 아는 일반적인 패턴 매칭으로는 사실 아래 예제의 모든 경우를 만족시키지 않는다.  주석을 보면 무엇이 매칭되는 대상들인지 적어놓았으므로 자세히 보기 바란다.

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&

f(x);  // int와 T& 매칭,       T는 int,       param은 int&
f(cx); // const int와 T& 매칭, T는 const int, param은 const int&
f(rx); // const int와 T& 매칭, T는 const int, param은 const int&
template<typename T>
void f(const T& param);

int x = 27;        // x의 타입은 int
const int cx = x;  // cx의 타입은 const int
const int& rx = x; // rx의 타입은 const int&

f(x);  // int와 const T& 매칭,       T는 int, param은 const int&
f(cx); // const int와 const T& 매칭, T는 int, param은 const int&
f(rx); // const int와 const T& 매칭, T는 int, param은 const int&
 여기서 패턴 매칭이라 함은 참조 제거된 expr의 타입을 ParamType과 매칭 시키는 데 적절히 매칭된 부분을 제외하고 남아있는 타입을 T에 매칭하는 방식인가 보다.  두 번째 예제의 마지막 함수 호출 f(rx)를 예로 들면 아래와 같이 const는 매칭되므로 제거하고 남은 건 int이므로 T는 int가 된다. 즉 매칭된 건 제거하고 남은 것을 T에 매칭하면 되는 것이다.

참조 제거된 expr : const int
    ParamType : const T   &

 패턴 매칭이라는 용어때문에 좀 찜찜한 느낌을 지울 수 없다. 저자가 매칭 규칙을 정확히 설명을 해주었다면 좋았을 것 같은데 그렇지 않다. 누구나 알 수 있을 것이라 생각했는지 따로 설명을 하지 않고 있다.

 그리고 책에서는 포인터형에 대해서도 같은 방식으로 타입 추론이 일어난다고 되어있지만 추후에 인터넷에 공개된 도서의 정오표를 통해서 포인터형은 사실 뒤에 나올 3번의 경우에 속한다고 수정하였다. 내가 보기에도 맞는 말이다. 따라서 여기서는 언급하지 않도록 하겠다.

2. ParamType이 Universal 참조형(T&&)인 경우
 이 규칙은 C++11에 rvalue 참조형이 추가되면서 템플릿 타입 추론에도 영향을 미치게된 경우이다. 아래 예제와 같이 ParamType은 rvalue 참조형 같이 생겼지만 실제 전달되는 expr의 타입에 따라 다음과 같은 타입 추론이 수행된다.
  • expr의 타입이 lvalue이면 T와 ParamType은 lvalue 참조형으로 추론된다.
  • expr의 타입이 rvalue이면 1번과 같은 방식으로 추론된다.
template<typename T>
void f(T&& param); // 여기서 param은 Universal 참조형

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

f(x);  // x는 lvalue,  따라서 T는 int&,       param은 int&
f(cx); // cx는 lvalue, 따라서 T는 const int&, param은 const int&
f(rx); // cx는 lvalue, 따라서 T는 const int&, param은 const int&
f(27); // 27은 rvalue, 따라서 T는 int,        param은 int&&
 lvalue의 경우는 그냥 모두 lvalue 참조형으로 추론되므로 추가 설명이 필요 없을 것 같고 rvalue의 경우에는 1번 규칙처럼 상수 27의 타입 int와 T&&가 패턴매칭해서 T는 int가 되고 param은 int&&가 된 것이다.

3. ParamType이 참조형이 아닌 경우(T)
 이 경우에는 인자가 그냥 pass-by-value 방식으로 전달되는 것과 같다. 즉 param의 타입은 expr의 복사본의 타입이 되는 것이다. 다음 예제를 보도록 하자
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&
const char* const ptr = "Fun with pointers"; // ptr은 const char* const

f(x);   // T와 param의 타입은 모두 int
f(cx);  // T와 param의 타입은 모두 int
f(rx);  // T와 param의 타입은 모두 int
f(ptr); // T와 param의 타입은 모두 const char*
 위 예제에서 "const int형인 cx를 넘겼는데 왜 T가 const int가 아니고 int일까?"라고 의문을 제기할 수 있다. 바로 전에 말한 것과 같이 ParamType이 참조형이 아닌 경우에는 값의 복사본이 전달되므로 지정된 expr의 타입이 참조형이던 const형이던 상관없이 그 복사본이 전달되고 그 복사본은 수정되지 못할 이유가 없으므로 원래 붙어있던 const성질은 사라지게 되는 것이다. 마찬가지로 volatile 같은 성질들도 모두 사라지게 된다.

 다만 ptr의 경우에 const가 그대로 있는 것이 이상해 보일 수 있지만 f(ptr)에서 복사되는 건 ptr그 자체이지 ptr이 가리키는 객체가 복사되는 게 아니기 때문에 그 객체의 const성질은 그대로 유지되어야 하고 ptr 자신의 const성질만 사라지는 게 맞다. 따라서 ptr이 가리키는 객체의 const인 왼쪽의 const는 그대로 남고 ptr자신의 const는 사라져야 하기 때문에 오른쪽의 const만 사라지게 되는 것이다.

 위의 3가지 경우 외에 생각해 보아야 할 예외 케이스가 2가지가 있다. 바로 함수 인자로 배열이 전달될 때와 함수가 전달되는 경우이다. 이런 경우를 실제 앞으로 만나게 될지는 모르겠지만 정리하는 김에 같이 해보도록 하겠다.

* 배열을 인자로 넘겼을 때
 배열을 템플릿 함수에 인자로 넘기면 타입 추론은 어떻게 될까? 아래 예제를 보자.
template<typename T>
void f(T param); // 여기서 param은 비-참조형

const char arr[] = "Array";

f(arr); // arr은 const char[6]이지만 T는 const char*
 ParamType이 비-참조형일 때는 const char[6]을 const char*로 바꿔서 타입 추론을 수행한다. 하지만 아래 예제의 경우는 좀 다르다.
template<typename T>
void f(T& param); // 여기서 param은 참조형

const char arr[] = "Array";

f(arr); // arr은 const char[6], T는 const char[6], param은 const char(&)[6]
 ParamType이 참조형일 때는 배열을 포인터로 바꾸지 않고 배열 그대로 받아들인다.

* 함수를 인자로 넘겼을 때
 크게 쓸 일은 없지만 함수를 템플릿 함수에 인자로 넘겼을 때도 배열과 비슷한 방식의 동작이 수행된다는 것도 알아두자.
void someFunc(int, double); // 함수의 타입은 void(int, double)

template<typename T>
void f1(T param); // 여기서 param은 비-참조형

template<typename T>
void f2(T& param); // 여기서 param은 참조형

f1(someFunc); // T는 void(*)(int, double, param은 void(*)(int, double)
f2(someFunc); // T는 void(int, double),   param은 void(&)(int, double)
 ParamType이 참조형일 때만 배열에서 처럼 함수타입이 유지되고 비-참조형인 경우에는 함수 포인터로 바꿔서 타입 추론을 수행한다.