STL
에 관한 장(제 18장)에서 이미 템플릿 메커니즘을 배웠다.
템플릿 메커니즘을 이용하면 최종적으로 사용될 실제 유형에 신경쓰지 않고 클래스와 알고리즘을 지정할 수 있다. 컴파일러는 템플릿이 사용될 때마다 특정한 데이터 유형에 맞게 재단하여 코드를 생성한다. 이 코드는 템플릿 정의로부터 컴파일 시간에 생성된다. 템플릿으로부터 실제 코드를 생성하는 것을 구체화라고 부른다.
이 장은 템플릿의 구문적 특이성을 다룬다. 템플릿 유형 매개변수와 템플릿 비-유형 매개변수 그리고 함수 템플릿을 소개하고 (이 장과 제 24장에) 몇 가지 템플릿 예제를 제공한다. 템플릿 클래스는 제 22장에 다룬다.
C++가 제공하는 템플릿으로는 추상 컨테이너(제 12장)와 문자열(제 5장) 그리고 스트림(제 6장)과 총칭 알고리즘(제 19장)이 있다. 그래서 템플릿은 오늘날 C++에 핵심적인 역할을 한다. 그러므로 괴이한 특징으로 보면 안된다.
템플릿을 총칭 알고리즘처럼 대해야 한다. C++ 프로그래머라면 반드시 갖추어야 할 도구이다. 적극적으로 템플릿을 사용할 기회를 엿보아야 한다. 처음에는 템플릿이 좀 복잡해 보이고 거북할 수 있다. 그렇지만 시간이 지날수록 그의 강력한 힘과 혜택에 점점 더 고마움을 느끼게 될 것이다. 결국 템플릿을 사용해야 하는 이유를 알 수 있을 것이다. 어느새 여러분은 더 이상 평범한 함수와 클래스에 초점을 두지 않고 템플릿을 만들기 위해 열중하고 있는 자신을 보게 될 것이다.
이 장은 함수 템플릿을 소개하면서 시작한다. 필수 구문에 중점을 둔다. 이 장은 템플릿을 위한 토대를 마련한다. 이 토대 위에 다른 장들이 건설된다.
add
함수는 두 개의 Type
인자를 기대하고 그의 합을 돌려준다.
Type add(Type const &lvalue, Type const &rvalue) { return lvalue + rvalue; }위의 함수 정의가 얼마나 유사하게 그의 기술을 따르고 있는지 눈여겨보라. 두 개의 인자를 받고 그의 합을 돌려준다. 이제 이 함수를
int
값에 대하여 정의하면 어떤 일이 일어나는지 연구해 보자. 다음과 같이 작성한다면:
int add(int const &lvalue, int const &rvalue) { return lvalue + rvalue; }지금까지는 그런대로 좋다. 그렇지만 배정도 수를 두 개 더해야 한다면 이 함수를 중복정의해야 한다.
double add(double const &lvalue, double const &rvalue) { return lvalue + rvalue; }중복정의 버전의 갯수는 어디까지 정의해야 할지 끝이 없다.
string
이나 size_t
등등에 대하여 .... 중복정의 버전은 일반적으로 모든 유형에 대하여 operator+
와 복사 생성자를 지원할 필요가 있다. 기본적으로 같은 함수인데도 불구하고 중복정의 버전이 모두 필요하다. 왜냐하면 강력하게 유형이 정의되는 C++의 본성 때문이다. 이 때문에 템플릿 메커니즘에 의존하지 않고서는 진정한 총칭 함수를 구성할 수 없다.
다행스럽게도 템플릿 함수의 중요한 부분을 이미 보았다. 최초 add
함수는 실제로 그런 함수의 구현이다. 물론 아직 완벽하게 템플릿으로 정의된 것은 아니다. 처음 add
함수를 컴파일러에게 주면 다음과 같은 에러 메시지를 보여줄 것이다.
error: `Type' was not declared in this scope error: parse error before `const' 에러: `Type'이 이 영역에 선언되어 있지 않습니다. 에러: `const' 앞에서 해석할 수 없습니다.당연하다.
Type
를 정의하지 않았기 때문이다. add
함수를 템플릿 정의로 완전히 바꾸면 에러가 사라진다. 이렇게 하기 위해 함수의 구현을 살펴보고 Type
이 실제로 공식적인 유형 이름인지 결정한다. 그것을 대안 구현에 비교하면 Type
을 int
로 바꾸어 첫 번째 구현을 얻을 수 있는지 아니면 Type
을 double
로 바꾸어 두 번째 구현을 얻을 수 있는지 확실하게 알 수 있다.
템플릿을 완벽하게 정의하면 Type
유형 이름의 이런 형식적인 본성을 만족시킬 수 있다. template
키워드를 사용하여 한 줄을 원래의 정의 앞에 둔다. 그러면 다음의 함수 템플릿 정의를 얻는다.
template <typename Type> Type add(Type const &lvalue, Type const &rvalue) { return lvalue + rvalue; }
이 정의에서 다음과 같은 사실을 알 수 있다.
template
키워드로 템플릿 정의 또는 선언을 시작한다.
template
다음에 온다. 이것은 원소를 쉼표로 분리하여 담고 있는 리스트이다. 옆꺽쇠로 둘러싸인 이 리스트를 템플릿 매개변수 리스트라고 부른다. 여러 원소를 사용하는 템플릿 리스트는 다음과 같이 보인다.
typename Type1, typename Type2
Type
이 보인다. 함수를 정의할 때의 형식 매개변수 이름에 해당된다. 지금까지 함수에서 형식 변수 이름만을 보아 왔다. 매개변수의 유형은 언제나 함수가 정의되는 순간까지는 이미 알려진다. 템플릿은 형식 이름의 인지 수준을 한 단계 더 위로 끌어 올린다. 템플릿은 유형 이름이 변수 이름 그 자체가 아니라 형식화되도록 허용한다. Type
이 형식 유형 이름이라는 사실은 typename
키워드로 나타낸다. 템플릿 매개변수 리스트의 Type
앞에 배치한다. Type
과 같은 형식 이름을 템플릿 유형 매개변수라고 부른다. 템플릿 비-유형 매개변수도 존재한다. 잠시 후에 소개한다.
다른 C++ 교재를 보면 class
키워드를 사용하는 경우가 있다. 이 교재는 typename
을 사용한다. 그래서 다른 교재에서 템플릿 정의는 다음과 같은 줄로 시작할 수도 있다.
template <class Type>이 책은
class
키워드보다 typename
키워드를 선호한다. 어쨌든 템플릿 유형 매개변수는 유형 이름이기 때문이다 (typename
보다 class
를 선호하는 저자도 있다. 결국 취향의 문제이다).
template
키워드와 템플릿 매개변수 리스트는 템플릿 머리부라고 불리운다.
add
템플릿의 정의에 사용되었다.
Type const &
매개변수로 지정된다. 이것은 예와 같은 의미를 지닌다. 매개변수는 함수로 변경할 수 없는 Type
객체나 값에 대한 참조이다.
add
템플릿의 몸체를 보면 복사 생성자는 물론이고 operator+
가 사용되는 것이 보인다. 함수가 값을 반환하기 때문이다. 이 덕분에 우리의 add
함수 템플릿에 사용되는 형식 유형 Type
에 대하여 다음 제한을 공식화할 수 있다.
Type
은 operator+
를 지원해야 한다.
Type
은 복사 생성자를 지원해야 한다.
Type
은 string
일 수는 있지만 절대로 ostream
은 될 수 없다. operator+
이나 복사 생성자 모두 스트림에는 사용할 수 없기 때문이다.
template <typename Type> Type add(Type const &lvalue, Type const &rvalue) { return lvalue + rvalue; }
다시 add
템플릿의 매개변수를 살펴보자. Type
이 아니라 Type const &
를 지정하면 과도한 복사가 방지된다. 동시에 원시 유형의 값을 인자로 함수에 건넬 수 있다. 그래서 add(3, 4)
를 호출하면 int{4}
가 Type const &rvalue
에 할당된다. 일반적으로 함수 매개변수는 Type const &
로 지정해야 불필요한 복사를 방지할 수 있다. 컴파일러는 이 경우 `참조에 대한 참조'를 처리할 정도로 똑똑하다. 이것은 보통 C++ 언어가 지원하지 않는 것이다. 다음 main
함수를 연구해 보자 (여기와 다음의 간단한 예제에서 템플릿과 필요한 헤더들 그리고 이름공간 선언이 이미 제공되어 있다고 가정한다):
int main() { size_t const &var = size_t{4}; cout << add(var, var) << '\n'; }여기에서
var
는 상수 size_t
에 대한 참조이다. add
템플릿에 인자로 건네어지고 거기에서 Type const &
유형의 lvalue
와 rvalue
를 size_t const &
값으로 초기화한다. 컴파일러는 Type
을 size_t
로 번역한다. 다른 방법으로서 Type const &
가 아니라 Type &
을 사용하여 매개변수들을 지정할 수도 있다. 이렇게 (비-상수로) 지정하면 단점이 있다. 임시 값들을 함수에 더 이상 건넬 수 없다. 그러므로 다음은 컴파일에 실패한다.
int main() { cout << add(string{"a"}, string{"b"}) << '\n'; }여기에서
string &
을 초기화하는 데 string const &
는 사용할 수 없다. add
에 Type &&
매개변수가 정의되어 있으면 위의 프로그램은 잘 컴파일되었을 것이다. 게다가 다음 예제는 올바르게 컴파일된다. 컴파일러가 Type
이 분명히 string const
라고 결정하기 때문이다.
int main() { string const &s = string{"a"}; cout << add(s, s) << '\n'; }이 예제로부터 무엇을 추론할 수 있을까?
Type const &
매개변수로 초기화한다. 불필요한 복사를 방지하기 위해서이다.
Type const &
로 정의되어 있다고 간주한다.
제공된 인자: | 실제로 사용된 유형: |
size_t const | size_t |
size_t | size_t |
size_t * | size_t * |
size_t const * | size_t const * |
template <typename Type, size_t Size> Type sum(Type const (&array)[Size]) { Type tp{}; // 주의: 기본 생성자는 존재해야 한다. for (size_t idx = 0; idx < Size; idx++) tp += array[idx]; return tp; }이 템플릿 정의는 다음의 새로운 개념과 특징을 이끌어 낸다.
size_t
라는 아주 구체적인 유형을 가진다. 템플릿 매개변수 리스트 안에 사용된 구체적 (즉, 비-형식적) 유형의 템플릿 매개변수를 템플릿 비-유형 매개변수라고 부른다. 템플릿 비-유형 매개변수는 상수 표현식의 유형을 정의한다. 템플릿이 구체화될 때까지 알려져야 한다. 그리고 size_t
와 같이 기존의 유형에 근거하여 정의되어야 한다.
Type const (&array)[Size]이 매개변수는
array
를 배열에 대한 참조로 정의한다. Type
유형의 원소를 Size
개 가질 수 있고 변경할 수 없다.
Type
과 Size
가 모두 사용된다. 물론 Type
은 템플릿 유형 매개변수 Type
이다. 그러나 Size
도 역시 템플릿 매개변수이다. size_t
이고 값은 실제로 sum
함수 템플릿을 호출할 때 컴파일러가 추론한다. 결론적으로 Size
는 const
값이다. 그런 상수 표현식을 템플릿 비-유형 매개변수(template non-type parameter)라고 부른다. 그의 유형은 템플릿의 매개변수 리스트에 따라 이름이 붙는다.
Type
의 구체적인 값 뿐만 아니라 Size
값도 추론할 수 있어야 한다. sum
함수는 매개변수가 하나 뿐이므로 컴파일러는 함수의 실제 인자로부터 Size
값만 추론할 수 있다. 추론할 수 있으려면 제공된 인자가 (크기가 고정된 이미 알려진) 배열이어야 한다. Type
유형의 원소를 가리키는 포인터는 안 된다. 그래서 다음 main
함수에서 첫 번째 서술문은 올바르게 컴파일되지만 두 번째 서술문은 실패할 것이다.
int main() { int values[5]; int *ip = values; cout << sum(values) << '\n'; // 컴파일 됨 cout << sum(ip) << '\n'; // 컴파일 불가 }
Type tp = Type()
서술문은 tp
를 기본 값으로 초기화한다. 여기에서 (0같은) 고정 값이 사용되지 않고 있음을 눈여겨보라. 어떤 유형이든 기본 값은 그의 기본 생성자를 사용하여 얻을 수 있다. 고정된 숫자 값을 사용하여 얻는 것이 아니다. 물론 어느 클래스나 자신의 생성자 중 하나에 숫자 값을 인자로 받는 것은 아니다. 그러나 모든 유형은 심지어 원시 유형일지라도 기본 생성자를 지원한다 (실제로 어떤 클래스는 기본 생성자를 구현하지 않는다. 또는 접근하지 못하도록 막기도 한다. 그러나 대부분은 기본 생성자를 지원한다). 원시 유형의 기본 생성자는 값을 0으로 (또는 false
로) 초기화한다. 게다가 Type tp = Type()
서술문은 진짜 초기화이다. tp
는 Type
의 기본 생성자로 초기화된다. Type
의 복사 생성자를 사용하여 Type
의 사본을 tp
에 할당하지 않는다.
여기에서 눈여겨볼 흥미로운 사실은 (직접적으로 현재 주제와 연관된 것은 아니지만) Type tp(Type())
구문을 생성할 수 없다는 것이다. 마치 적절한 초기화처럼 보인다. 보통 초기화 인자는 string s("hello")
처럼 객체의 정의에 제공할 수 있다. 그럼 왜 Type tp = Type()
는 받으면서 Type tp(Type())
는 거부하는가? Type tp(Type())
를 사용하면 에러 메시지를 보여주지 않는다. 그래서 그것이 Type
객체의 기본 초기화가 아니라는 사실을 재빨리 탐지하지 못한다. 대신에 컴파일러는 tp
가 사용되는 순간에 에러 메시지를 토해내기 시작한다.
이것은 C++에서 (그리고 C도 마찬가지로) 컴파일러는 가능하면 함수나 함수 포인터를 인지하려고 최선을 다하기 때문이다. 이것을 함수 우선 규칙(function prevalence rule)이라고 부른다. 이 규칙에 따라 Type()
은 (한 쌍의 괄호 때문에) 함수를 가리키는 포인터로 번역된다. 이 함수는 인자를 기대하지 않고 Type
을 돌려준다. 컴파일러는 확실하게 그렇게 할 수 없는 경우가 아니라면 함수로 번역한다. Type tp = Type()
의 초기화에서는 함수를 가리키는 포인터를 볼 수 없다. Type
객체에 함수 포인터의 값을 줄 수 없기 때문이다 (Type()
은 가능하면 Type (*)()
으로 번역된다는 것을 유념하라). 그러나 Type tp(Type())
에서 포인터 번역을 사용할 수 있다. tp
는 이제 함수로 선언된다. 이 함수는 Type
을 돌려주는 함수를 포인터로 기대한다. tp
자체도 역시 Type
을 돌려준다. 예를 들어 tp
는 다음과 같이 정의할 수 있다.
Type tp(Type (*funPtr)()) { return (*funPtr)(); }
sum
은 또 Type
의 클래스에 어떤 공개 멤버가 존재한다고 가정한다. 이 번에는 operator+=
연산자와 Type
의 복사 생성자가 있다고 간주한다.
클래스 정의처럼 템플릿 정의도 using
을 포함하면 안된다. 지시어인가 아니면 선언인가. 지시어가 프로그래머의 의도를 거스르는 상황에 템플릿을 사용할 수 있다. 템플릿의 저자와 프로그래머가 서로 다른 using
지시어를 사용하면 모호성이나 기타 충돌이 일어날 수 있다 (예를 들어 cout
변수가 std
이름공간에 정의되거나 아니면 프로그래머 만의 이름공간에 정의되는 것). 대신에 템플릿 정의 안에서는 필요한 모든 이름공간 지정을 비롯하여 완전하게 자격을 갖춘 이름만 사용해야 한다.
auto
키워드를 소개했다. decltype
키워드는 auto
키워드와 관련하여 조금 다른 행위를 보여준다. 이 항은 decltype
를 집중 연구한다. 따로 더 지정할 필요가 없는 auto
키워드와 다르게 decltype
키워드 다음에는 언제나 반괄호 사이에 표현식이 따라온다 (예를 들어 decltype(variable)
).
예를 들어 함수가 매개변수에 std::string const &text
를 정의하고 있다고 가정해 보자. 함수 안에서 다음과 같이 두 개의 정의를 마주할 가능성이 있다.
auto scratch1{text}; decltype(text) scratch2 = text;
auto
라면 컴파일러는 평범한 유형을 도출한다. 그래서 scratch1
은
string
이고, 복사 생성을 사용하여 `text
'로 초기화한다.
이제 decltype
을 연구해 보자. decltype
은 text
의 유형을 결정한다. string const &
가 그것으로서 scratch2
의 유형으로 사용된다. string const &scratch2
가 그 유형으로서, 문자열 text
가 참조하는 것을 참조한다. 이것이 decltype
의 표준 행위이다. 변수의 이름이 주어지면 그 변수의 유형으로 교체된다.
다른 방법으로서 decltype
을 사용할 때 표현식을 지정할 수 있다. 물론 변수 자체가 표현식이지만, decltype
의 문맥에서 `expression'은 단순히 평범한 변수 지정을 넘어서 더 복잡한 표현식을 정의한다. 변수의 이름을 반괄호 사이에 넣어 (variable)
와 같이 단순하게 표현할 수도 있다.
표현식이 사용될 때 컴파일러는 표현식의 유형에 참조를 추가할 수 있는지 알아본다. 그렇다면, decltype(expression)
은 그 lvalue 참조의 유형으로 교체된다 (그래서 expression-type &
을 얻는다). 그렇지 않다면 decltype(expression)
은 그 표현식의 평범한 유형으로 교체된다.
다음은 몇 가지 예이다.
int *ptr; decltype(ptr) ref = ptr; // decltype의 인자는 평범한 변수이다. 그래서 // ptr의 유형이 사용된다. int *ref = ptr // decltype(ptr)은 int *로 교체된다. // (초기화-되지 않은/사용되지 않은 변수라는 경고 두 개). int *ptr; decltype( (ptr) ) ref = ptr; // decltype 함수의 인자는 표현식이다. 그래서 // int *&ref = ptr이 사용된다. // decltype( (ptr))은 int *&로 교체된다. int value; decltype(value + value) var = value + value; // decltype의 인자는 표현식이다. 그래서 컴파일러는 // decltype(...)을 int & (int &var = value + value)로 교체하려고 한다. // value + value는 임시적이고, var의 유형은 int &일 수 없기 때문이다. // 그래서 decltype(...)은 int로 교체된다. // (즉, value + value의 유형으로 교체된다) string lines[20]; decltype(lines[0]) ref = lines[6]; // decltype의 인자는 표현식이다. 그래서 // string &ref = lines[6] 표현식은 OK이다. // decltype(...)은 string &으로 교체된다. string &&strRef = string{}; decltype(strRef) ref = std::move(strRef); // decltype의 인자는 평범한 변수이다. 그래서 그 변수의 유형이 // 사용된다. string &&ref = std::move(strRef) 표현식은 OK이다. // decltype(...)은 strRef의 유형 즉, string &&으로 교체된다. decltype((strRef)) ref2 = strRef // decltype의 인자는 표현식이다. 그래서 // string && &ref = strRef 표현식이 사용된다. // 자동으로 string &ref = strRef의 표현식이 되며 OK이다. // decltype은 string &으로 교체된다.
이것 말고도 decltype(auto)
로 지정할 수 있다. 이 경우 decltype
의 규칙이 auto
에 적용된다. 그래서 객체의 기본 유형을 결정하는 데 auto
가 사용된다. 그 다음, 표현식이 그저 변수에 불과하다면 그 표현식의 유형이 사용된다. 그렇지 않고 참조를 그 표현식에 추가할 수 있으면 decltype(auto)
은 그 표현식의 유형에 대한 참조로 교체된다. 다음은 몇 가지 예이다.
int *ptr; decltype(auto) ptr2 = ptr // auto는 int *를 생산하며, ptr은 평범한 변수이다. // 그래서 decltype(auto)는 int *로 교체된다. int value; decltype(auto) ret = value + value; // auto는 int를 생산하며, value + value는 표현식이다. // 그래서 int &를 시도한다. // 그렇지만 value + value는 참조에 할당할 수 없다. // 그래서 표현식의 유형이 사용된다. // decltype(auto)는 int로 교체된다. string lines[20]; decltype(auto) line = lines[0]; // auto는 string을 생산하며, lines[0]은 표현식이다. // 그래서 string &을 시도한다. // string &line = lines[0]은 OK이다. // 그래서 decltype(auto)은 string &로 교체된다. decltype(auto) ref = string{} // auto는 string를 생산하며, string{}은 표현식이다. // 그래서 string &를 시도한다. // 그렇지만 string &ref = string{}은 초기화가 올바르지 않다. // 그래서 string 자체가 사용된다. // decltype(auto)는 string으로 교체된다.
실전에서는 반환 유형을 정의하는 함수 템플릿에서 decltype(auto)
형태를 자주 만난다. 다음 구조체 정의를 한 번 살펴보자 (함수 템플릿을 사용하지는 않지만, decltype(auto)
의 작동 방식을 보여준다):
struct Data { vector<string> d_vs; string *d_val = new string[10]; Data() : d_vs(1) {} auto autoFun() const { return d_val[0]; } decltype(auto) declArr() const { return d_val[0]; } decltype(auto) declVect() const { return d_vs[0]; } };
autoFun
멤버는 auto
를 돌려준다. d_val[0]
이 auto
에 건네지고 auto
는 string
이라고 추론한다. 함수의 반환 유형은 string
이다.
declArr
멤버는 decltype(auto)
을 돌려준다. d_val[0]
은 표현식이고 string
을 나타낸다. decltype(auto)
는 string &
이라고 추론하며, 이것이 함수의 반환 유형이 된다.
declVect
멤버는 decltype(auto)
을 돌려준다. d_vs[0]
은 표현식이고 string
을 나타낸다. decltype(auto)
은 string &
이라고 추론한다. 그렇지만 declVect
는 불변 멤버이기도 하므로, 이 참조는 string const &
이 되어야 한다. 이것은 decltype(auto)
으로 인식된다. 그래서 함수의 반환 유형은 string const &
이 된다.
왜 declArr
의 반환 유형에는 const
가 없는데 declVect
의 반환 유형에는 있는지 궁금하다면 d_vs
와 d_val
을 살펴보자. 두 변수 모두 함수 안에서는 불변이다. 그러나 d_val
은 const *
이기 때문에 가변 string
객체를 가리킨다. 그래서 declArr
는 string const &
를 돌려줄 필요가 없지만 declVect
는 string const &
를 돌려주어야 한다.
int add(int lhs, int rhs) { return lhs + rhs; }위의 함수는 함수 템플릿으로 변환할 수 있다.
template <typename Lhs, typename Rhs> Lhs add(Lhs lhs, Rhs rhs) { return lhs + rhs; }불행하게도 함수 템플릿을 다음과 같이 호출하면
add(3, 3.4)의도했던 반환 유형은
double
이다. int
가 아니다. 이 문제는 템플릿 유형 매개변수를 추가하면 해결할 수 있다. 반환 유형을 명시적으로 지정해야 한다.
add<double>(3, 3.4);
decltype
을 사용하여 반환 유형을 정의하는 것은 작동하지 않는다 (3.3.5항). decltype
이 사용될 때까지 lhs
와 rhs
가 컴파일러에게 알려지지 않기 때문이다. 그래서 추가 템플릿 유형 매개변수를 제거하려는 다음 시도는 컴파일에 실패한다.
template <typename Lhs, typename Rhs> decltype(lhs + rhs) add(Lhs lhs, Rhs rhs) { return lhs + rhs; }
decltype
을 기반으로 함수의 반환 유형을 정의하려면 조금 복잡해질 수 있다. 늦게-지정되는 반환 유형 구문을 사용하면 이 복잡성을 줄일 수 있다. 이 구문과 함께 decltype
을 사용하여 함수의 반환 유형을 정의할 수 있다. 주로 함수 템플릿과 함께 사용된다. 그러나 보통의 (비-템플릿) 함수에도 사용할 수 있다.
template <typename Lhs, typename Rhs> auto add(Lhs lhs, Rhs rhs) -> decltype(lhs + rhs) { return lhs + rhs; }이 함수를
cout << add(3, 3.4)
와 같은 서술문에 사용하면 결과 값은 6이 아니라 6.4가 될 것이다. 이것이 바로 의도한 결과이다. 늦게-지정되는 반환 유형이 어떻게 함수의 반환 유형 정의의 복잡성을 줄여 주는지 다음 예제를 연구해 보자:
template <typename T, typename U> decltype((*(T*)0)+(*(U*)0)) add(T t, U u);이해하기가 좀 어렵지 않은가?
(*(T*)0)
과 같은 표현식은 0을 정의한다. C 유형 변환을 유형 T
를 가리키는 포인터처럼 사용한 다음에 그 포인터를 역참조한다. 그리하여 유형 T
의 값을 산출한다 (값 자체가 변수로 존재하지 않음에도 불구하고 말이다). 마찬가지로 두 번째 용어도 decltype
표현식에 사용된다. 결과 유형은 그 다음에 add
함수의 반환 유형으로 사용된다. 늦게-지정되는 반환 유형을 사용하면 다음과 같은 코드를 얻는다.
template <typename T, typename U> auto add(T t, U u) -> decltype(t+u);대부분의 사람들이 읽기에 쉽다고 생각할 것이다.
decltype
으로 지정된 표현식이라고 해서 반드시 lhs
와 rhs
를 사용할 필요는 없다. 다음 함수 정의는 lhs
대신에 lhs.length
가 사용된다.
template <typename Class, typename Rhs> auto add(Class lhs, Rhs rhs) -> decltype(lhs.length() + rhs) { return lhs.length() + rhs; }
decltype
이 컴파일되는 순간에 보이는 변수는 무엇이든 decltype
표현식 안에 사용할 수 있다. 그렇지만 현재는 멤버를 포인터로 통하여 멤버를 선택할 수 없다. 다음 코드는 멤버 함수의 주소를 add
의 첫 인자로 지정하고 그의 반환 값을 사용하여 함수 템플릿의 반환 유형을 결정하는 것이 목적이다.
std::string global{"hello world"}; template <typename MEMBER, typename RHS> auto add(MEMBER mem, RHS rhs) -> decltype((global.*mem)() + rhs) { return (global.*mem)() + rhs; } int main() { std::cout << add(&std::string::length, 3.4) << '\n'; // 출력: 14.4 }
위의 함수는 잘 컴파일되지만 현재 상태로는 사용할 수 없다. 다음 unimplemented: mangling dotstar_expr과 같은 컴파일 에러를 일으키기 때문이다 (cout << add(&string::length, 3.4)
와 같은 서술문에 의해 야기된다).
<functional>
헤더를 포함해야 한다.
함수 템플릿에 값이 아니라 참조가 건네어진다는 것을 컴파일러가 추론할 수 없는 상황이 존재한다. 다음 예제에서 outer
함수 템플릿은 int x
를 첫 인자로 받는다. 컴파일러는 Type
이 int
라고 추론한다.
template <typename Type> void outer(Type t) { t.x(); } void useInt() { int arg; outer(arg); }
물론 컴파일은 실패한다. 그러나 멋지게도 컴파일러는 추론한 유형을 보고한다.
In function 'void outer(Type) [with Type = int]': ...
불행하게도 다음 예제에서 call
템플릿을 사용하면 같은 에러가 일어난다. call
은 템플릿으로서 함수 하나와 그 함수에 건넬 값을 기대한다. 함수는 인자를 하나 기대하는데 인자 그 자체가 변경된다. 그 함수는 sqrtArg
이다. 인자로 double
유형을 참조로 기대한다. 이 인자는 std::sqrt
를 호출하여 변경된다.
void sqrtArg(double &arg) { arg = sqrt(arg); } template<typename Fun, typename Arg> void call(Fun fun, Arg arg) { fun(arg); cout << "In call: arg = " << arg << '\n'; }
double value = 3
이라고 간주하면 call(sqrtArg, value)
는 value
를 변경하지 않는다. 컴파일러가 Arg
를 double
이라고 추론하고 그러므로 value
를 값으로 건네기 때문이다.
value
자체를 변경되도록 하려면 컴파일러에게 value
를 참조로 건네야 한다고 알려야 한다. call
의 템플릿 인자를 Arg &
로 정의하는 것은 받아들이기 힘들다는 사실에 주목하라. 어떤 상황에서는 실제 인자를 변경하지 않는 것이 적절할 수도 있기 때문이다.
ref(arg)
와 cref(arg)
참조 포장자를 사용할 수 있다. 이 함수들은 인자를 하나 받고 그것을 (상수) 참조 유형의 인자로 돌려준다. 실제로 value
를 변경하기 위해 다음 main
함수에 보여주는 바와 같이 ref(value)
를 사용하여 call
에 건넬 수 있다.
int main() { double value = 3; call(sqrtArg, value); cout << "Passed value, returns: " << value << '\n'; call(sqrtArg, ref(value)); cout << "Passed ref(value), returns: " << value << '\n'; } /* 출력: In call: arg = 1.73205 Passed value, returns: 3 In call: arg = 1.73205 Passed ref(value), returns: 1.73205 */
enum { V1, V2, V3 };여기에서
enum
은 이름없는 유형, 다시 말해 익명 유형을 정의한다.
함수 템플릿을 정의할 때 컴파일러는 템플릿 유형 매개변수의 유형을 인자로부터 추론한다.
template <typename T> void fun(T &&t); fun(3); // T는 int 유형이다. fun('c'); // T는 char 유형이다.그렇지만 다음도 사용할 수 있다.
fun(V1); // T는 위의 열거 유형의 값이다.
fun
안에서 T
변수를 정의할 수 있다. 익명의 유형이라도 상관이 없다.
template <typename T> void fun(T &&t) { T var(t); }
지역적으로 정의된 유형의 값이나 객체도 함수 템플릿에 인자로 건넬 수 있다. 예를 들어,
void definer() { struct Local { double dVar; int iVar; }; Local local; // 지역 유형 사용 fun(local); // 좋다. T는 'Local' 유형이다. }
컴파일러는 템플릿 유형 매개변수에 대하여 실제 유형을 추론할 때 실제로 사용된 인자의 유형만 고려한다. 이 과정에 지역 변수나 함수의 반환 값은 전혀 고려 대상이 아니다. 이것은 이해할 만하다. 함수가 호출될 때 컴파일러는 함수 템플릿의 인자 유형에 관해서만 알기 때문이다. 호출 시점에 함수의 지역 변수의 유형은 확실히 볼 수 없다. 함수의 반환 값은 실제로 사용이 안될 수도 있고 아니면 추론된 템플릿 유형 매개변수의 하위 (또는 상위) 유형의 변수에 할당될 수도 있다. 그래서 다음 예제에서 컴파일러는 fun()
함수를 호출할 수 없다. Type
템플릿 유형 매개변수에 대하여 실제 유형을 추론할 수 없기 때문이다.
template <typename Type> Type fun() // 절대 `fun()' 형태로 호출 불가능 { return Type{}; }컴파일러는 `
fun()
'에 대한 호출을 처리하지는 못하지만 유형을 명시적으로 지정하여 fun()
함수를 호출하는 것은 가능하다. 예를 들어 fun<int>()
는 fun
을 호출해 int
로 초기화한다. 물론 이것은 컴파일러의 인자 추론과 같지 않다.
일반적으로 함수에 동일한 템플릿 유형의 매개변수가 여럿 있을 때 실제 유형은 반드시 정확하게 같아야 한다. 그래서 다음과 같은 함수라면
void binarg(double x, double y);
int
와 double
을 사용하여 호출할 수 있는데 int
인자로 호출하면 조용하게 double
로 승격된다. 비슷하지만 함수 템플릿은 int
와 double
인자를 사용하여 호출할 수 없다. 컴파일러 자체에서 Type
이 double
이어야 한다고 판단하고 int
를 double
로 승격하지 않기 때문이다.
template <typename Type> void binarg(Type const &p1, Type const &p2) {} int main() { binarg(4, 4.5); // ?? 컴파일 불가: 실제 유형이 다르기 때문이다. }
그러면 컴파일러는 템플릿 유형 매개변수의 실제 유형을 추론할 때 어떻게 변형하는가? 매개변수 유형 변형에 대하여 딱 세 가지 그리고 함수 템플릿 비-유형 매개변수에 대하여 변형이 하나 있다. 이런 변형을 사용하여 실제 유형을 추론할 수 없으면 그 함수 템플릿은 고려 대상에서 제외된다. 컴파일러가 수행하는 변형 방법은 다음과 같다.
lvalue으로부터 rvalue를 만든다.
const
수식자를 비-상수 인자 유형에 삽입한다.
템플릿으로부터 파생된 클래스 유형의 인자로 호출하면 템플릿 바탕 클래스를 사용한다.
이것은 템플릿 유형 매개변수 변형은 아니다. 그러나 함수 템플릿의 나머지 템플릿 비-유형 매개변수에 적용된다. 이런 함수 매개변수들에 대하여 컴파일러는 가능하면 표준 변형을 수행한다 (int
를size_t
로,int
를double
로, 등등 변형).
다양한 템플릿 매개변수 유형을 추론하여 변형하는 목적은 함수 인자를 함수 매개변수에 맞추어 보는 것이 아니다. 그게 아니라 템플릿 유형 매개변수의 실제 유형을 결정하는 것이 목적이다.
rvalue
가 필요하지만lvalue
가 제공될 때 lvalue를-rvalue로-변형이 적용된다. 이런 일은 값 매개변수를 지정한 함수에 변수를 인자로 사용하면 일어난다. 예를 들어,template<typename Type> Type negate(Type value) { return -value; } int main() { int x = 5; x = negate(x); // lvalue (x)를 rvalue로 변형 (x를 복사함) }
배열의 이름을 포인터 변수에 할당할 때 배열을-포인터로-변형이 적용된다. 이것은 포인터 매개변수를 정의한 함수에 자주 사용된다. 배열을 인자로 받으면 그 주소가 포인터 매개변수에 할당된다. 그리고 그의 유형은 상응하는 템플릿 매개변수의 유형을 추론하는 데 사용된다. 예를 들어,template<typename Type> Type sum(Type *tp, size_t n) { return accumulate(tp, tp + n, Type()); } int main() { int x[10]; sum(x, 10); }이 예제에서 배열x
의 위치가sum
에 건네진다.sum
은 어떤 유형의 포인터를 기대한다. 배열을-포인터로-변형을 사용하여x
의 주소는tp
에 할당된 포인터 값으로 간주된다. 처리 과정 중Type
이int
라는 사실을 추론한다.
이 변형은 함수를 가리키는 포인터가 매개변수로 정의한 함수 템플릿에 자주 사용된다. 함수의 이름을 인자로 건네면 함수의 주소가 그 포인터-매개변수에 할당된다. 그 과정 중에 템플릿 유형 매개변수를 추론한다. 이것을 함수를-포인터로-변형이라고 부른다. 예를 들어,#include <cmath> template<typename Type> void call(Type (*fp)(Type), Type const &value) { (*fp)(value); } int main() { call(sqrt, 2.0); }이 예제에서
sqrt
함수의 주소는call
함수에 건네진다.call
함수는Type
을 돌려주는 함수를 포인터로 기대하고Type
을 인자로 기대한다. 함수를-포인터로 변형을 사용하여sqrt
의 주소는fp
에 할당된다. 그 과정 중에Type
은double
이라는 사실을 추론한다 (sqrt
는 함수의 주소라는 사실을 명심하라. 함수를 가리키는 포인터 변수가 아니다. 그러므로 lvalue 변형이라고 부른다).인자
2.0
은2
처럼 지정할 수 없다.int sqrt(int)
원형이 없기 때문이다. 게다가 함수의 첫 번째 매개변수는Type (*fp)(Type)
를 지정하지Type (*fp)(Type const &)
를 지정하는 것이 아니다. 이전의 연구로부터 충분히 예상할 만한 것이다. 함수 템플릿의 매개변수 유형을 지정할 때 값보다 참조를 더 선호한다는 사실을 연구한 바 있다. 그렇지만fp
의 인자Type
은 함수 템플릿 매개변수가 아니라fp
가 가리키는 함수의 매개변수이다.sqrt
는 원형이double sqrt(double)
이지double sqrt(double const &)
이 아니기 때문에call
의 매개변수fp
는Type (*fp)(Type)
으로 지정해야 한다. 그 만큼 엄격하다.
const
나 volatile
자격을 포인터에 추가한다. 이 변형은 함수 템플릿의 유형 매개변수가 명시적으로 const
(또는 volatile
)을 지정하지만 그 함수의 인자가 const
또는 volatile
개체가 아닐 경우에 적용된다. 이 경우 const
나 volatile
을 컴파일러가 제공한다. 이어서 컴파일러는 템플릿 유형 매개변수를 추론한다. 예를 들어,
template<typename Type> Type negate(Type const &value) { return -value; } int main() { int x = 5; x = negate(x); }여기에서 함수 템플릿의
Type const &value
매개변수를 볼 수 있다. const Type
에 대한 참조이다. 그렇지만 인자는 const int
가 아니라 변경될 수 있는 int
이다. 자격 변형을 적용하면 컴파일러는 const
를 x
의 유형에 추가한다. 그리고 int const x
에 일치시킨다. 다음으로 이것을 다시 Type const &value
에 일치시켜 컴파일러가 Type
를 int
라고 추론할 수 있도록 만든다.
22.11절에 클래스 템플릿을 또다른 클래스 템플릿으로부터 어떻게 파생시킬 수 있는지 보여주었다.
클래스 템플릿 파생은 여전히 다룰 것이 있기 때문에 다음 연구는 좀 이른 감이 있다. 물론 독자는 가뿐하게 건너 뛰어 22.11절로 갔다가 다시 돌아와도 좋다.
이 절에서는 연구를 위해 Vector
클래스 템플릿을 std::vector
로부터 파생시켰다고 가정한다. 게다가 벡터를 정렬하기 위해 obj
함수객체를 사용하여 다음 함수 템플릿을 생성했다고 간주한다.
template <typename Type, typename Object> void sortVector(std::vector<Type> vect, Object const &obj) { sort(vect.begin(), vect.end(), obj); }대소문자를 구분하지 않고
std::vector<string>
객체를 정렬하기 위해 class Caseless
를 다음과 같이 생성할 수 있다.
class CaseLess { public: bool operator()(std::string const &before, std::string const &after) const { return strcasecmp(before.c_str(), after.c_str()) < 0; } };이제
sortVector()
를 사용하여 다양한 벡터를 정렬할 수 있다.
int main() { std::vector<string> vs; std::vector<int> vi; sortVector(vs, CaseLess()); sortVector(vi, less<int>()); }클래스 템플릿을 구체화된 바탕 클래스로 변형하면
sortVector
함수 템플릿은 이제 Vector
객체를 정렬하는 데에도 사용이 가능해진다. 예를 들어,
int main() { Vector<string> vs; // `std::vector' 대신에 `Vector'를 사용한다. Vector<int> vi; sortVector(vs, CaseLess()); sortVector(vi, less<int>()); }이 예제는 인자로
Vector
를 sortVector
에 건넨다. 클래스 템플릿으로부터 파생된 바탕 클래스로 변형을 적용하여 컴파일러는 Vector
를 std::vector
로 간주한다. 덕분에 컴파일러는 템플릿 유형 매개변수를 추론할 수 있다. Vector vs
는 std::string
로 Vector vi
는 int
로 추론한다.
int x
이면 Type
이 int
이다. 그리고 그 함수의 매개변수는 Type &value
이다).
int
와 double
인자로 호출할 수 없다.
template <typename Type> Type add(Type const &lvalue, Type const &rvalue) { return lvalue + rvalue; }이 함수 템플릿을 호출할 때 두 개의 동일한 유형을 사용해야 한다 (물론 세 가지 표준 변형을 허용한다고 하더라도 마찬가지다). 템플릿 추론 메커니즘이 동일한 템플릿 유형에 대하여 동일한 실제 유형을 찾지 못하면 그 함수 템플릿은 구체화되지 않을 것이다.
그런 경우에 컴파일러는 유형을 축약한다. 이중 동일 참조 유형은 간단하게 축약된다. 예를 들어 템플릿 매개변수 유형이 Type &&
으로 지정되어 있고 실제 매개변수는 int &&
라면 Type
은 int &&
가 아니라 int
로 추론된다.
상당히 직관적이다. 그러나 실제 유형이 int &
라면 어떻게 될까? int & &¶m
와 같은 것은 전혀 있을 수 없으므로 컴파일러는 rvalue 참조를 제거함으로써 이중 참조를 축약한다. lvalue 참조는 유지한다. 다음 규칙이 적용된다.
- lvalue 참조 인자를 받는 템플릿 유형 매개변수에 (예를 들어
Type &
에) 대한 lvalue 참조로 정의된 함수 템플릿 매개변수는 결과적으로 하나의 lvalue 참조가 된다.- 종류에 상관없이 참조 인자를 받는 템플릿 유형 매개변수에 (예를 들어
Type &&
에) 대한 rvalue 참조로 정의된 함수 템플릿 매개변수는 인자의 참조 유형을 사용한다.
예를 들어,
Actual &
인자를 제공하면 Type &
는 Actual &
이 되고 Type
는 Actual
로 추론된다.
Actual &
을 제공하면 Type &&
은 Actual &
이 되고 Type
은 Actual
로 추론된다.
Actual &&
을 제공하면 Type &
도 역시 Actual &
이 되고 Type
은 Actual
로 추론된다.
Actual &&
을 제공하면 Type &&
은 Actual &&
이 되고 Type
은 Actual
로 추론된다.
축약이 일어나는 구체적인 예제를 살펴보자. 다음 함수 템플릿을 연구해 보자. 함수 매개변수가 어떤 템플릿 유형 매개변수에 대한 rvalue 참조로 정의되어 있다.
template <typename Type> void function(Type &¶m) { callee(static_cast<Type &&>(param)); }이 상황에서
function
이 TP &
유형의 (lvalue) 인자를 가지고 호출될 때 템플릿 유형 매개변수 Type
은 Tp &
로 추론된다. 그러므로 Type &¶m
은 Tp ¶m
으로 구체화되고 Type
은 Tp
가 된다. 그리고 rvalue 참조는 lvalue 참조로 교체된다.
마찬가지로 static_cast
를 사용하여 callee
가 호출될 때도 똑같이 축약이 일어난다. 그래서 Type &¶m
은 Tp ¶m
에 작동한다. 그러므로 (축약을 사용하면) 정적 형변환도 역시 Tp ¶m
유형을 사용한다. param
이 어쩌다가 유형이 Tp &&
이면 정적 유형변환은 Tp &¶m
유형을 사용한다.
이 특징 덕분에 유형을 바꿀 필요 없이 함수 인자를 내포 함수에 건넬 수 있다. lvalue는 그대로 lvalue이고 rvalue는 그대로 rvalue이다. 그러므로 이 특징은 완벽한 전달(perfect forwarding)이라고도 알려져 있다. 이에 관해서는 22.5.2항에 더 자세하게 연구한다. 완벽한 전달 덕분에 템플릿 저자는 함수 템플릿의 중복정의 버전을 여러 벌 정의하지 않아도 된다.
필자의 구형 랩탑에서 algorithm
같은 템플릿 헤더를 컴파일하면 cmath
같은 평범한 헤더를 컴파일하는데 비해 약 네 배의 시간이 더 든다. iostream
헤더 파일은 처리하기가 더 힘들다. cmath
를 컴파일하는 시간에 비해 약 15배의 시간이 더 든다. 템플릿을 처리하는 것은 컴파일러에게 가볍지 않은 일임은 확실하다.
그렇지만 이 단점을 너무 심각하게 받아 들이면 안 된다. 컴파일러는 계속해서 템플릿 처리 능력을 개선해 가고 있는 중이다. 그리고 컴퓨터는 나날이 빨라지고 있다. 몇 년 전이라면 고통이었던 것도 오늘 날에는 아무것도 아닌 일이다.
링커는 과도한 실체들을 (즉, 구체화된 템플릿의 동일한 정의들을) 싹 없애 버린다. 최종 프로그램에서는 실제 템플릿 유형 매개변수의 특정 집합에 대하여 하나의 실체만 남는다 (예제는 21.6절 참고). 그러므로 링커는 수행할 추가 작업이 있다 (다시 말해 여러 실체들을 솎아 낸다). 그 때문에 링크 속도가 약간 떨어진다.
템플릿을 선언하면 컴파일러는 템플릿의 정의를 반복해서 처리할 필요가 없다. 템플릿 선언만으로는 구체화되지 않는다. 실제로 요구되는 구체화는 다른 곳에서 사용할 수 있어야 한다 (물론 일반적으로 이 사실은 선언에도 마찬가지로 유효하다). 보통 라이브러리에 저장되어 있는, 평범한 함수에서 마주치는 상황과 다르게 현재로는 템플릿을 라이브러리에 저장하는 것은 불가능하다 (물론 컴파일러가 미리 컴파일된 헤더 파일을 생성할 수는 있다). 결론적으로 템플릿 선언을 사용하면 프로그래머의 어깨 위에 큰 부담을 지우는 셈이다. 프로그래머는 요구된 실체가 존재하는지 확인할 의무가 있다. 아래에 이를 쉽게 확인할 수 있는 방법을 소개한다.
함수 템플릿을 선언하려면 그냥 함수의 몸체를 쌍반점으로 교체하면 된다. 이것은 평범한 함수를 선언하는 방식과 정확하게 똑 같다는 것을 주목하라. 그래서 이전에 정의된 add
함수 템플릿은 다음과 같이 간단하게 선언할 수 있다.
template <typename Type> Type add(Type const &lvalue, Type const &rvalue);이미 템플릿 선언을 만나 본 바 있다.
ios
클래스와 그의 파생 클래스로부터 원소들의 실체화를 요구하지 않는 소스 파일에는 iosfwd
헤더를 포함해도 된다. 다음 선언을 컴파일하기 위해 string
과 istream
헤더를 포함할 필요는 없다.
std::string getCsvLine(std::istream &in, char const *delim);다음 한 줄이면 충분하다.
#include <iosfwd>
iosfwd
헤더를 처리하려면 시간이 약간 더 걸린다. string
과 istream
헤더를 처리해야 하기 때문이다.
이를 위하여 이른바 명시적인 구체화 선언이라는 템플릿 선언의 변형을 사용할 수 있다. 명시적인 구체화 선언은 다음 요소들로 구성된다.
template
으로 시작하고 템플릿의 매개변수 리스트는 생략한다.
명시적으로 구체화를 선언하면 프로그램이 요구하는 템플릿 함수의 실체를 모두 파일 하나에 모을 수 있다. 이 파일은 보통의 소스 파일로서 템플릿 정의 헤더를 포함해야 하고 이어서 필요한 명시적인 구체화 선언을 지정해야 한다. 소스 파일이기 때문에 다른 소스에 포함되지 않는다. 그래서 필요한 헤더를 포함했다면 이름공간 using
지시어와 선언을 안전하게 사용할 수 있다.
다음은 이전의 add
함수 템플릿에 대하여 요구한 구체화를 보여주는 예이다. 각각 double
과 int
그리고 std::string
유형에 대하여 구체화를 한다.
#include "add.h" #include <string> using namespace std; template int add<int>(int const &lvalue, int const &rvalue); template double add<double>(double const &lvalue, double const &rvalue); template string add<string>(string const &lvalue, string const &rvalue);혹시 프로그램이 요구한 구체화를 언급하는 것을 깜박 잊어 버리더라도 쉽게 보완할 수 있다. 빠진 구체화 선언을 위 리스트에 추가하기만 하면 된다. 파일을 다시 컴파일하고 링크하면 완성이다.
그래서 함수 템플릿은 실제로 언제 구체화되는가? 컴파일러가 템플릿을 구체화하기로 결정하는 상황이 두 가지 있다.
add
함수를 한 쌍의 size_t
값을 가지고 호출 할 때);
char (*addptr)(char const &, char const &) = add;
컴파일러가 템플릿 유형 매개변수를 언제나 명확하게 추론할 수 있는 것은 아니다. 컴파일러가 모호하다고 보고하면 그것은 프로그래머가 해결해야 한다. 다음 코드를 연구해 보자:
#include <iostream> #include "add.h" size_t fun(int (*f)(int *p, size_t n)); double fun(double (*f)(double *p, size_t n)); int main() { std::cout << fun(add); }
이 작은 프로그램을 컴파일하면 컴파일러는 모호성을 해결할 수 없다고 불평한다. 후보 함수가 두 개이기 때문이다. fun
의 중복정의 버전 각각에 대하여 add
함수를 구체화할 수 있기 때문이다.
error: call of overloaded 'fun(<unknown type>)' is ambiguous note: candidates are: int fun(size_t (*)(int*, size_t)) note: double fun(double (*)(double*, size_t)) 에러: 중복정의 'fun(<unknown type>)' 함수를 호출하기가 애매합니다. 고지: 후보 함수: int fun(size_t (*)(int*, size_t)) 고지: double fun(double (*)(double*, size_t))물론 이런 상황은 피해야 한다. 함수 템플릿은 모호성이 없을 경우에만 구체화될 수 있다. 모호성은 컴파일러의 함수 선택 메커니즘에 여러 함수가 출현할 때 일어난다 (21.14절). 그 모호성을 해결하는 것은 프로그래머의 책임이다. 무딘
static_cast
를 사용하여 해결할 수도 있다 (가능한 모든 대안 중에서 하나를 선택한다):
#include <iostream> #include "add.h" int fun(int (*f)(int const &lvalue, int const &rvalue)); double fun(double (*f)(double const &lvalue, double const &rvalue)); int main() { std::cout << fun( static_cast<int (*)(int const &, int const &)>(add) ); }
그러나 가능하면 유형을 강제로 변환하는 것은 피하는 것이 좋다. 그 방법은 다음 21.7절에 설명한다.
source1.cc
소스는 fun
함수를 정의하고 int
-유형의 인자에 대하여 add
를 구체화한다. add
의 템플릿 정의를 포함한다. add
의 주소를 보여준다.
union PointerUnion { int (*fp)(int const &, int const &); void *vp; };
#include <iostream> #include "add.h" #include "pointerunion.h" void fun() { PointerUnion pu = { add }; std::cout << pu.vp << '\n'; }
source2.cc
소스도 같은 함수를 정의한다. 그러나 그저 템플릿 선언을 사용하여 적절한 add
템플릿을 선언만 한다 (실체 선언이 아님). 다음은 source2.cc
이다.
#include <iostream> #include "pointerunion.h" template<typename Type> Type add(Type const &, Type const &); void fun() { PointerUnion pu = { add }; std::cout << pu.vp << '\n'; }
main.cc
에도 add
의 템플릿 정의가 들어 있다. fun
함수를 정의하고 main
을 정의하며 int
-유형의 인자에 대하여 add
를 정의한다. 뿐만 아니라 add
의 함수 주소를 보여준다. 또 fun
함수를 호출한다. 다음은 main.cc
이다.
#include <iostream> #include "add.h" #include "pointerunion.h" void fun(); int main() { PointerUnion pu = { add }; fun(); std::cout << pu.vp << '\n'; }
source1.o
와 (1912 바이트) source2.o
가 (1740 바이트) 크기가 다른 것에 주목하라 (g++
버전 4.3.4 사용 (이 절에서 보고된 오브젝트 모듈의 크기는 컴파일러와 실행 시간 라이브러리에 따라 다를 수 있다)). source1.o
에는 add
의 실체가 들어 있기 때문에 템플릿 선언만 들어 있는 source2.o
보다 약간 더 크다. 이제 작은 실험을 해 볼 준비가 되었다.
main.o
와 source1.o
를 링크하면 두 개의 오브젝트 모듈을 함께 링크하는 것이다. 각 모듈마다 같은 템플릿 함수의 실체가 들어 있다. 결과 프로그램은 다음 출력 결과를 생산한다.
0x80486d8 0x80486d8게다가 결과 프로그램의 크기는 6352 바이트이다.
main.o
와 source2.o
를 링크한다. 이제 두 개를 모아 하나의 오브젝트 모듈을 만든다. 한 모듈은 안에 add
템플릿의 실체가 들어 있고 다른 모듈은 안에 그냥 같은 템플릿 함수의 선언만 들어 있다. 그래서 결과 프로그램은 요구한 함수 템플릿의 실체를 하나 밖에 가질 수 없다. 이 프로그램은 정확하게 크기가 같다. 첫 번째 프로그램과 정확하게 똑 같이 출력한다.
fun
)가 있었는데, 함수 템플릿을 구체화할 때 두 인자가 동일하게 제공되었기 때문에 모호성이 생겼다. 이런 모호성을 해결하는 직관적인 방법은 static_cast
를 사용하는 것이다. 그러나 강제 유형변환은 되도록이면 피하는 것이 좋다.
함수 템플릿에서 정적 유형 변환은 실제로 피할 수 있다. 명시적인 템플릿 유형 인자를 사용하면 된다. 템플릿을 구체화할 때 사용해야 할 실제 유형에 관하여 컴파일러에게 알려 줄 수 있다. 이를 사용하려면 함수의 이름 다음에 실제 템플릿 유형 인자 리스트가 오고 그 다음에 또 함수의 인자 리스트가 올 수 있다. 실제 템플릿 인자 리스트에 언급된 실제 유형은 컴파일러가 사용하여 템플릿을 구체화할 때 어떤 유형을 사용해야 할지 `추론한다'. 다음은 이전 절에서 가져온 예제이다. 이제는 명시적인 템플릿 유형 인자를 사용한다.
#include <iostream> #include "add.h" int fun(int (*f)(int const &lvalue, int const &rvalue)); double fun(double (*f)(double const &lvalue, double const &rvalue)); int main() { std::cout << fun(add<int>) << '\n'; }
명시적인 템플릿 유형 인자는 어느 유형을 실제로 사용해야 할지 컴파일러가 탐지할 방법이 없는 상황에 사용할 수 있다. 예를 들어 21.4절에서 함수 템플릿 Type fun()
을 정의했다. double
유형에 대하여 이 함수를 구체화하려면 fun<double>()
으로 호출하면 된다.
add
템플릿을 다시 한 번 더 살펴보자. 이 템플릿은 두 개체의 합을 돌려주도록 설계되었다. 세 개의 개체의 합을 계산하고 싶다면 다음과 같이 작성할 수 있다.
int main() { add(add(2, 3), 4); }이것은 가끔씩 있는 상황에는 받아들일 만한 해결책이다. 그렇지만 개체 세 개를 자주 사용할 필요가 있다면 세 개의 인자를 기대하는 중복정의 버전의
add
함수가 유용한 함수가 될 것이다. 이 문제에 대한 간단한 해결책이 있다. 함수 템플릿을 중복정의할 수 있다.
함수 템플릿을 중복정의하려면 그냥 템플릿의 여러 정의를 헤더 파일에 배치하면 된다. add
함수에 대하여 이것은 다음과 같이 요약될 것이다.
template <typename Type> Type add(Type const &lvalue, Type const &rvalue) { return lvalue + rvalue; } template <typename Type> Type add(Type const &lvalue, Type const &mvalue, Type const &rvalue) { return lvalue + mvalue + rvalue; }중복정의 함수는 간단한 값의 관점에서 정의해야 할 필요가 없다. 다른 모든 중복정의 함수처럼, 유일한 집합의 함수 매개변수만 있으면 중복정의 함수 템플릿을 정의하기에 충분하다. 예를 들어 다음은 중복정의 버전이다. 벡터의 원소들의 합을 계산하는 데 사용할 수 있다.
template <typename Type> Type add(std::vector<Type> const &vect) { return accumulate(vect.begin(), vect.end(), Type()); }
함수 템플릿을 중복정의할 때 함수의 매개변수 리스트에 얽매일 필요가 없다. 템플릿 유형 매개변수 리스트 자체가 또 중복정의가 가능하다. add
템플릿의 마지막 정의로 vector
를 첫 인자로 지정할 수 있다. 그러나 deque
나 map
은 지정할 수 없다. 물론 그런 유형의 컨테이너에 대하여 중복정의 버전을 생성할 수 있다. 그러나 얼마나 더 멀리 가야 하는가?
더 좋은 접근법은 이런 컨테이너들의 공통적 특성을 살펴보는 것이다. 공통적 특성을 발견하면 거기에 근거하여 중복정의 함수 템플릿을 정의할 수 있다. 위에 언급한 컨테이너들의 공통적 특성 하나는 그들 모두 begin
과 end
멤버를 지원하고 반복자를 돌려준다는 것이다.
이것을 이용하면 이런 멤버들을 지원해야 하는 컨테이너를 나타내는 템플릿 유형 매개변수를 정의할 수 있다. 그러나 순수하게 `컨테이너 유형'을 언급하고 있더라도 구체화된 데이터 유형이 무엇인지는 알 수 없다. 그래서 컨테이너의 데이터 유형을 나타내는 두 번째 템플릿 유형 매개변수가 필요하다. 그래야 템플릿 유형 매개변수 리스트를 중복정의할 수 있다. 다음은 결과로 나온 add
템플릿의 중복정의 버전이다.
template <typename Container, typename Type> Type add(Container const &cont, Type const &init) { return std::accumulate(cont.begin(), cont.end(), init); }
init
매개변수를 매개변수 리스트에서 빼버려도 되는지 궁금하실 것이다. init
이 종종 기본 초기화 값을 가지기 때문에 그 대답은 `된다'이다. 그러나 거기에는 복잡한 문제가 관련되어 있다. 다음과 같이 add
함수를 정의할 수 있다:
template <typename Type, typename Container> Type add(Container const &cont) { return std::accumulate(cont.begin(), cont.end(), Type()); }그렇지만 템플릿 유형 매개변수의 순서가 바뀐 것에 주목하라. 컴파일러가 다음과 같은 호출에서
Type
을 결정할 수 없기 때문에 꼭 필요하다.
int x = add(vectorOfInts);템플릿 유형 매개변수의 순서를 바꾸어서
Type
을 맨 앞에 두면 첫 번째 템플릿 유형 매개변수에 명시적인 템플릿 유형 인자를 제공할 수 있다.
int x = add<int>(vectorOfInts);이 예제에서
vector<int>
인자를 제공했다. 컴파일러가 템플릿 유형 매개변수 Type
을 결정하도록 하기 위해 왜 int
를 명시적으로 지정해야 하는지 궁금할 것이다. 실제로는 그럴 필요가 없다. 세 번째 종류의 템플릿 매개변수가 존재한다. 템플릿 템플릿 매개변수가 그것으로서 컴파일러는 실제 컨테이너 인자로부터 직접 Type
을 결정할 수 있다. 템플릿 템플릿 매개변수는 다음 23.4절에 논의한다.
using namespace std; int main() { vector<int> v; add(3, 4); // 1 (해설 참고) add(v); // 2 add(v, 0); // 3 }
int
이다. 그러므로 add<int>
를 구체화한다. add
템플릿의 첫 번째 정의이다.
add
의 중복정의 버전을 찾는다. 그리고 std::vector
를 기대하는 중복정의 함수 템플릿을 선택한다. 템플릿 유형 매개변수는 int
이어야 한다고 추론했기 때문이다. 다음과 같이 구체화한다.
add<int>(std::vector<int> const &)
add
템플릿의 첫 번째 정의는 사용할 수 없다. 그러나 유형이 다른 두 개의 개체를 기대하는 마지막 정의를 사용할 수 있다. std::vector
가 begin
과 end
를 지원하기 때문에 컴파일러는 이제 함수 템플릿을 구체화할 수 있다.
add<std::vector<int>, int>(std::vector<int> const &, int const &)
두 개의 같은 그리고 두 개의 다른 템플릿 유형 매개변수에 대하여 add
함수 템플릿을 정의했으므로 두 개의 템플릿 유형 매개변수를 가진 add
함수 템플릿을 사용해 볼 가능성은 없어졌다.
add
함수 템플릿을 정의할 수는 있지만 이렇게 하면 모호성이 생긴다. 컴파일러가 유형이 서로 다른 함수 매개변수를 정의한 중복정의 버전 두 개 중에 어느 것을 사용해야 할지 결정할 수 없기 때문이다. 예를 들어 다음과 같이 정의하면:
#include "add.h" template <typename T1, typename T2> T1 add(T1 const &lvalue, T2 const &rvalue) { return lvalue + rvalue; } int main() { add(3, 4.5); }컴파일러는 다음과 같이 모호하다고 불평한다.
error: call of overloaded `add(int, double)' is ambiguous error: candidates are: Type add(const Container&, const Type&) [with Container = int, Type = double] error: T1 add(const T1&, const T2&) [with T1 = int, T2 = double]이제 세 개의 인자를 받는 중복정의 함수 템플릿을 떠올려 보자:
template <typename Type> Type add(Type const &lvalue, Type const &mvalue, Type const &rvalue) { return lvalue + mvalue + rvalue; }이 함수가 동등한 유형의 인자만 받는다는 것은 단점으로 간주할 수도 있다 (
int
세 개, double
세 개, 등등.). 이를 바로 잡기 위하여 중복정의 함수 템플릿을 또하나 정의한다. 이 번에는 유형에 상관없이 인자를 받는다. 이 함수 템플릿은 operator+
가 함수의 실제 사용된 유형 사이에 정의되어 있지만 그 말고는 문제가 없어 보일 경우에만 사용할 수 있다. 다음은 유형을 가리지 않고 인자를 받는 중복정의 버전이다.
template <typename Type1, typename Type2, typename Type3> Type1 add(Type1 const &lvalue, Type2 const &mvalue, Type3 const &rvalue) { return lvalue + mvalue + rvalue; }이제 인자를 세 개 기대하는 위의 두 중복정의 함수 템플릿을 정의했으므로 다음과 같이
add
를 호출해 보자:
add(1, 2, 3);여기에서 모호성을 기대할 수 있을까? 결국, 컴파일러는
Type == int
이라고 추론하고서 앞의 함수를 선택할 수 있지만 Type1 == int
와 Type2 == int
그리고 Type3 == int
라고 추론하고서 뒤의 함수를 선택할 수도 있다. 놀랍게도 컴파일러는 모호하다고 불평하지 않는다.
다음과 같은 이유 때문에 모호성이 보고되지 않는다. 특정화가 덜 된 또는 특정화가 더 된 템플릿 유형 매개변수를 사용하여 중복정의 템플릿 함수가 정의되어 있으면 (예를 들어 특정화가 덜 된 경우는 유형이 모두 다름 vs. 특정화가 더 된 경우는 유형이 모두 같음) 그러면 컴파일러는 가능하면 특정화가 더 된 함수를 선택한다.
제일 규칙: 구체화할 중복정의 함수 템플릿을 선택할 때 모호성을 방지하기 위하여 중복정의 함수 템플릿은 템플릿의 유형 인자를 유일하게 조합하여 지정하도록 허용해야 한다. 함수 템플릿의 유형 매개변수 리스트에서 템플릿 유형 매개변수의 순서는 중요하지 않다. 다음 함수 템플릿 중 하나를 구체화하려고 시도하면 모호성이 발생한다.
template <typename T1, typename T2> void binarg(T1 const &first, T2 const &second) {} template <typename T1, typename T2> void binarg(T2 const &first, T1 const &second) {}당연히 놀라울 것이 없다. 결국 템플릿 유형 매개변수는 그저 형식적인 이름에 불과하다. 그 이름만으로는 (
T1
이든 T2
이든 또는 그 무엇이든) 구체적인 의미는 없는 것이다.
add
를 선언:
template <typename Container, typename Type> Type add(Container const &container, Type const &init);
template int add<std::vector<int>, int> (std::vector<int> const &vect, int const &init);
std::vector<int> vi; int sum = add<std::vector<int>, int>(vi, 0);
add
템플릿은 operator+
와 복사 생성자를 지원하는 모든 유형에 대하여 잘 작동한다. 그렇지만 이 가정이 언제나 충족되는 것은 아니다. 예를 들어 char *
에서 operator+
이나 `복사 생성자'를 사용하는 것은 무의미하다. 컴파일러는 그 함수 템플릿을 구체화하려고 시도한다. 그러나 컴파일에 실패한다. 포인터에 대하여 operator+
가 정의되어 있지 않기 때문이다.
이런 상황에 컴파일러는 템플릿 유형 매개변수를 해결할 수 있지만 표준 구현이 의미가 없으며 에러를 야기한다는 사실을 탐지할 수도 있다.
이 문제를 해결하기 위하여 템플릿의 명시적인 특정화를 정의할 수 있다. 템플릿의 명시적인 특정화는 특정한 실제 템플릿 유형 매개변수를 사용하여 총칭 정의가 이미 존재하는 함수 템플릿을 정의한다. 이전 절에서 보았듯이 컴파일러는 언제나 특정화가 덜 된 함수보다 더된 함수를 선호한다. 그래서 가능하면 템플릿의 명시적인 특정화가 선택된다.
템플릿의 명시적인 특정화는 템플릿 유형 매개변수(들)에 대하여 특정화를 제공한다. 그 특별한 유형은 일관성 있게 함수 템플릿의 코드에 있는 템플릿 유형 매개변수로 교체된다. 예를 들어 명시적으로 특정화된 유형이 char const *
이라면 다음 템플릿 정의에서
template <typename Type> Type add(Type const &lvalue, Type const &rvalue) { return lvalue + rvalue; }
Type
는 char const *
으로 교체될 것이며 결과적으로 원형이 다음과 같은 함수가 탄생한다.
char const *add(char const *const &lvalue, char const *const &rvalue);이제 이 함수를 사용해 보자:
int main(int argc, char **argv) { add(argv[0], argv[1]); }그렇지만 컴파일러는 우리의 특정화를 무시하고 최초의 함수 템플릿을 구체화하려고 시도한다. 이것은 실패한다. 결과적으로 도데체 왜 컴파일러가 명시적인 특정화를 선택하지 않았는지 궁금해진다.
여기에서 무슨 일이 일어났는지 알아보기 위해 한 단계씩 컴파일러의 조치를 감상해 보자:
char *
인자를 가지고 add
를 호출한다.
Type
이 char *
이라고 추론한다.
char *
템플릿의 유형 인자가 char const *const &
템플릿 매개변수에 부합할까? 여기에서 21.4절에 다루었던 변형을 사용할 기회가 일어난다. 자격 변형이 유일하게 사용이 가능해 보인다. 컴파일러는 이를 이용하여 상수-매개변수를 비-상수 인자에 묶을 수 있다.
Type
의 관점에서 컴파일러는 Type
유형의 인자 또는 Type const
유형의 인자를 Type const &
에 일치시킬 수 있다.
Type
자체는 변경되지 않는다. 그래서 Type
은 char *
이다.
char const *
에 대하여 특정화된 것을 하나 찾는다.
char const *
는 char *
이 아니므로 컴파일러는 그 명시적인 특정화를 거부한다. 그리고 총칭적 형태를 사용한다. 결과적으로 컴파일 에러가 일어난다.
add
함수 템플릿이 char *
를 템플릿 유형 인자로 다룰 수 있으려면 char *
에 대하여 또다른 명시적인 특정화가 요구된다. 결과적으로 그 원형은 다음과 같다.
char *add(char *const &lvalue, char *const &rvalue);
또다른 명시적인 특정화를 정의하는 대신에 포인터를 기대하는 중복정의 함수 템플릿을 설계할 수 있다. 다음 함수 템플릿 정의는 상수 Type
값을 가리키는 두 개의 포인터를 기대하고 비-상수 Type
을 포인터로 돌려준다.
template <typename Type> Type *add(Type const *t1, Type const *t2) { std::cout << "Pointers\n"; return new Type; }실제로 어떤 유형이 위의 함수 매개변수에 묶일까? 이 경우
Type const *
만 묶이는데, 인자로 char const *
이라는 유형을 건넬 수 있다. 여기에는 자격 변형에게 기회가 없다. const
또는 const &
의 관점에서 (Type
이 아닌) 매개변수 자체가 지정된다면 컴파일러는 자격 변형을 활용하여 const
를 비-상수 인자에 추가할 수 있다. t1
을 보면 Type const *
로 정의되어 있는 것을 볼 수 있다. 여기에 그 매개변수를 참조하는 const
라는 것은 전혀 없다 (이 경우라면 Type const *const t1
또는 Type const *const &t1
이 되었을 것이다). 결론적으로 자격 변형은 여기에 적용할 수 없다.
위의 중복정의 함수 템플릿은 char const *
만 인자로 받기 때문에 (reinterpret cast
없이는) char *
를 인자로 받지 않을 것이다. 그래서 main
의 argv
인자는 우리의 중복정의 함수 템플릿에 건넬 수 없다.
Type *
인자를 기대하는 또다른 중복정의 함수 템플릿을 선언해야 하는가? 가능하기는 하지만 우리의 접근법이 유연하지 못하다는 사실이 결국 드러날 것이다. 평범한 함수와 클래스처럼 함수 템플릿도 개념적으로 확실한 한 가지 목적을 가져야 한다. 중복정의 함수 템플릿에 중복정의 함수 템플릿을 추가하려는 시도는 즉시 템플릿의 인터페이스를 조잡하게 만들어 버린다. 이런 접근법을 사용하지 마라. 더 좋은 접근법은 원래의 목적에 충실하도록 템플릿을 생성하는 것이다. 문서에 그의 목적을 명료하게 기술하고 가끔씩 특정한 사례를 허용하기 위한 목적으로만 템플릿을 생성하는 것이 좋다.
템플릿을 생성하는 상황에서 명시적인 특정화는 물론 합당할 수 있다. 우리의 add
함수 템플릿에 문자를 가리키는 const
그리고 비-const
포인터에 대하여 두 개의 특정화가 적절할 것이다. 다음은 그 생성 방법이다.
template
키워드로 시작한다.
error: template-id `add<char*>' for `char* add(char* const&, char* const&)' does not match any template declaration
다음은 char *
와 char const *
인자를 기대하는 함수 템플릿 add
에 대한 명시적인 특정화 두 가지이다.
template <> char *add<char *>(char *const &p1, char *const &p2) { std::string str(p1); str += p2; return strcpy(new char[str.length() + 1], str.c_str()); } template <> char const *add<char const *>(char const *const &p1, char const *const &p2) { static std::string str; str = p1; str += p2; return str.c_str(); }템플릿의 명시적 특정화는 보통 다른 함수 템플릿의 구현이 들어있는 파일 안에 포함된다.
템플릿의 명시적 특정화를 선언할 때 template
키워드 다음의 옆꺽쇠 한 쌍이 중요하다. 생략하면 템플릿의 구체화를 선언한다. 컴파일러는 조용하게 처리한다. 컴파일 시간이 약간 더 길어지는 희생은 감수한다.
템플릿의 명시적인 특정화를 선언할 때 (또는 구체화를 선언할 때) 컴파일러가 함수의 인자로부터 이런 유형을 추론할 수 있으면 템플릿 유형 매개변수를 명시적으로 지정하지 않아도 된다. char (const) *
특정화의 경우라면 다음과 같이 선언할 수도 있다.
template <> char *add(char *const &p1, char *const &p2) template <> char const *add(char const *const &p1, char const *const &p2);게다가
template <>
을 생략할 수 있다면 템플릿 문자(쌍반점)는 선언으로부터 제거될 것이다. 결과로 나온 선언은 이제 단순히 함수 선언에 불과하다. 이것은 에러가 아니다. 함수 템플릿과 평범한 (비-템플릿) 함수는 서로 중복정의할 수 있다. 평범한 함수는 함수 템플릿에 비해 유형 변환에 제한이 없다. 이 때문에 이따금씩 평범한 함수로 템플릿을 중복정의하기도 한다.
함수 템플릿의 명시적 특정화는 그냥 함수 템플릿의 또다른 중복정의 버전에 불과하다. 중복정의 버전은 완전히 다른 집합의 템플릿 매개변수를 정의할 수 있는 반면에, 특정화는 비-특정화된 변형과 똑 같은 템플릿 매개변수의 집합을 사용해야 한다. 컴파일러는 실제 템플릿 인자가 특정화로 정의된 유형에 부합하는 상황에 특정화를 사용한다 (인자 집합에 부합하여 매개변수가 가장 특정화된 집합이 사용된다는 규칙을 따른다). 다양한 매개변수 집합에 대하여 중복정의 버전의 함수를 (또는 함수 템플릿을) 사용해야 한다.
std::string
변환 연산자를 정의할 때 무슨 일이 일어나는지 연구해 보자 (11.3절).
변환 연산자는 rvalue로 사용될 것이라고 보장된다. 이것은 string
변환 연산자가 정의된 클래스의 객체를 string
객체에 할당할 수 있다는 뜻이다. 그러나 string
변환 연산자를 정의한 객체를 스트림에 삽입하려고 시도하면 컴파일러는 부적절한 유형을 ostream
에 삽입하려 한다고 불평한다.
반면에 이 클래스가 int
변환 연산자를 정의하고 있으면 문제없이 삽입된다.
이렇게 구분하는 이유는 operator<<
가 기본 유형을 (예를 들어 int
유형을) 삽입할 때는 평범한 (자유) 함수로 정의되어 있지만 string
을 삽입할 때는 함수 템플릿으로 정의되어 있기 때문이다. 그러므로 string
변환 연산자를 정의한 클래스의 객체를 삽입하려고 할 때 컴파일러는 ostream
객체로 삽입하는 삽입 연산자의 모든 중복정의 버전을 방문한다.
기본 유형의 변환이 없으므로 기본 유형의 삽입 연산자는 사용할 수 없다. 템플릿 인자에 대한 변환은 변환 연산자를 찾아 보도록 컴파일러에게 허용하지 않기 때문에 string
변환 연산자를 정의한 우리의 클래스는 ostream
에 삽입할 수 없다.
그런 클래스의 객체를 ostream
객체에 삽입한다고 하더라도 클래스는 (string
인자에서 클래스의 객체를 rvalue로 사용하는 데 요구되는 string
변환 연산자 말고도) 반드시 자신만의 중복정의 삽입 연산자를 정의해야 한다.
static_assert(constant expression, error message)이 서술문으로 템플릿 정의 안에 (프로그래머의 의도를) 표명할 수 있다. 다음은 두 가지 사용법이다.
static_assert(BUFSIZE1 == BUFSIZE2, "BUFSIZE1 and BUFSIZE2 must be equal"); template <typename Type1, typename Type2> void rawswap(Type1 &type1, Type2 &type2) { static_assert(sizeof(Type1) == sizeof(Type2), "rawswap: Type1 and Type2 must have equal sizes"); // ... }첫 번째 예제는 또다른 전처리기 지시어를 피하는 법을 보여준다 (이 경우
#error
지시어).
두 번째 예제는 어떻게 static_assert
를 사용하여 템플릿이 올바른 조건에서 작동하는지 확인하는 방법을 보여준다.
static_assert
의 첫 번째 인자에 정의된 조건이 false
이면 static_assert
의 두 번째 인자에 정의된 문자열이 화면에 보여지고 컴파일은 멈춘다.
#error
전처리기 지시어처럼 static_assert
는 컴파일 시간에 관련된다. 코드에 사용되더라도 실행 시간의 효율성에 영향을 전혀 주지 않는다.
<climits>
헤더 파일은 다양한 유형의 상수를 정의한다. 예를 들어 INT_MAX
는 int
에 저장할 수 있는 최대 값을 정의한다.
climits
에 정의된 한계의 단점은 제한이 고정되어 있다는 것이다. 어떤 유형의 인자를 받는 함수 템플릿을 작성한다고 해 보자. 예를 들어,
template<typename Type> Type operation(Type &&type);이 함수는
Type
에 대하여 type
이 음의 값이면 가장 큰 음의 값을 돌려주어야 하고 type
이 양의 값이면 가장 큰 양의 값을 돌려주어야 한다. 그렇지만 정수가 아니면 0을 돌려주어야 한다.
어떻게 처리할 것인가?
climits
의 상수들은 사용할 유형을 이미 알고 있을 때만 사용할 수 있기 때문에 유일한 접근법은 다양한 정수 유형에 대하여 함수 템플릿 특정화를 사용하는 것이다. 예를 들어,
template<> int operation<int>(int &&type) { return type < 0 ? INT_MIN : INT_MAX; }
numeric_limits
가 제공하는 편의기능에 대안이 있다. 이 편의기능을 사용하려면 <limits>
헤더를 포함해야 한다.
numeric_limits
클래스 템플릿에는 숫치 유형에 관련하여 온갖 질문에 답하는 다양한 멤버가 들어있다. 이들을 소개하기 전에 operation
함수 템플릿을 단 하나의 함수 템플릿으로 어떻게 구현할 수 있을지 살펴보자:
template<typename Type> Type operation(Type &&type) { return not numeric_limits<Type>::is_integer ? 0 : type < 0 ? numeric_limits<Type>::min() : numeric_limits<Type>::max(); }이제 모든 원시 유형에 대하여
operation
을 사용할 수 있다.
다음은 numeric_limits
이 제공하는 편의기능을 개관한다. numeric_limits
이 정의하는 멤버 함수들은 constexpr
값을 돌려주는 것에 주목하라. Type
유형에 대하여 numeric_limits
에 정의된 `member
' 멤버는 다음과 같이 사용할 수 있다.
numeric_limits<Type>::member // 데이터 멤버 numeric_limits<Type>::member() // 멤버 함수모든
numeric_limits
멤버 함수는 constexpr
값을 돌려준다.
Type denorm_min()
:Type
에 대하여 사용할 수 있으면 비정규화된 최소 양의 값을 돌려준다. 그렇지 않으면numeric_limits<Type>::min()
을 돌려준다.
int digits
:
Type
값에 사용된 부호-비트가 아닌 비트의 갯수 또는 (부동소수점수 유형이라면) 가수의 비트 갯수를 돌려준다.
int digits10
:
Type
값을 변경하지 않고 표현하는 데 요구되는 자릿수.
Type constexpr epsilon()
:
Type
에 대하여 1을 초과하는 가장 작은 값과 1 자체 사이의 차.
float_denorm_style has_denorm
:비정규화된 부동소수점 값을 표현하기 위해 가변 갯수의 지수 비트를 사용한다.has_denorm
멤버는Type
유형에 대하여 비정규화된 값에 대한 정보를 돌려준다.
denorm_absent
:Type
은 비정규화된 값을 허용하지 않는다.denorm_indeterminate
:Type
는 비정규화된 값을 사용할 수도 있고 안 할 수도 있다. 컴파일러는 컴파일 시간에 이를 결정할 수 없다.denorm_present
:Type
이 비정규화된 값을 사용한다.
bool has_denorm_loss
:
비정규화를 사용한 결과로 정확도에 손실이 있으면 (부정확한 결과를 돌려주기보다) true
를 돌려준다.
bool has_infinity
:Type
에 양의 무한대 표현이 있으면true
를 돌려준다.
bool has_quiet_NaN
:Type
이 quiet_NaN에 대하여 표현이 있으면 비-신호처리 `숫자-아님' 값에 대하여true
를 돌려준다.
bool has_signaling_NaN
:Type
이 signaling_NaN에 대하여 표현이 있으면 신호처리 `숫자-아님' 값에 대하여true
를 돌려준다.
Type constexpr infinity()
:
Type
에 대하여 양의 무한 값을 사용할 수 있으면 양의 무한대 값을 돌려준다.
bool is_bounded
:Type
에 정해진 갯수의 값이 들어 있으면true
를 돌려준다.
bool is_exact
:Type
이 정밀한 표현을 사용하면true
를 돌려준다.
bool is_iec559
:Type
이 IEC-559 (IEEE-754) 표준을 사용하면true
를 돌려준다. 그런 유형은 언제나has_infinity
와has_quiet_NaN
그리고has_signaling_NaN
대하여true
를 돌려준다. 반면에infinity()
와quiet_NaN()
그리고signaling_NaN()
에 대해서는 0-아닌 값을 돌려준다.
bool is_integer
:Type
이 정수 유형이면true
를 돌려준다.
bool is_modulo
:Type
이 `modulo' 유형이면true
를 돌려준다. modulo 유형의 값은 언제든지 더할 수 있지만, `원래대로 빙빙 돌아오기'가 되므로 결과적으로Type
이 두 피연산자를 각각 더한 것보다 더 작게된다.
bool is_signed
:Type
에 부호가 있으면true
를 돌려준다.
bool is_specialized
:Type
이 특정화이면true
를 돌려준다.
Type constexpr lowest()
:Type
이 표현할 수 있는 가장 작은 유한 값이다.lowest
가 돌려주는 값보다 더 작은 유한 값은 존재하지 않는다. 이 값은min
이 돌려주는 값과 같다. 단, 부동 소수점 유형은 제외한다.
T constexpr max()
:
Type
의 최대 값.
T constexpr min()
:
Type
의 최소 값. 비정규 부동 소수점 수 유형이라면 정규화된 최대 양의 값을 돌려준다.
int max_exponent
:유효한Type
값을 생산하는 부동 소수점 수 유형Type
에 대하여 최대 양의 정수 값을 돌려준다.
int max_exponent10
:
밑수가 10인 지수에 대하여 최대 정수 값을 돌려준다. 유효한 Type
값을 생산한다.
int min_exponent
:유효한Type
값을 생산하는 부동 소수점 수Type
유형에 대하여 최대 음의 정수 값을 돌려준다.
int min_exponent10
:
밑수가 10인 지수에 대하여 최소 음의 정수 값을 돌려준다. 유효한 Type
값을 생산한다.
Type constexpr quiet_NaN()
:
Type
에 대하여 사용할 수 있으면 비-신호처리 `Not-a-Number' 값을 돌려준다.
int radix
:Type
이 정수 유형이라면 표현의 밑수를 돌려준다.Type
이 부동 소수점 수 유형이라면 표현의 지수의 밑수를 돌려준다.
Type constexpr round_error()
:
Type
에 대하여 최대 반올림 오차를 돌려준다.
float_round_style round_syle
:Type
에 사용된 반올림 스타일. 다음enum
float_round_style
값 중 하나를 가진다.
round_toward_zero
: 값이 0쪽으로 절삭된다.round_to_nearest
: 값이 표현 가능한 가장 가까운 값으로 절삭된다.round_toward_infinity
: 값이 무한대쪽으로 절삭된다.round_toward_neg_infinity
: 음의 무한대 쪽으로 절삭된다.round_indeterminate
: 컴파일 시간에 반올림 스타일을 결정할 수 있는가.
Type constexpr signaling_NaN()
:
Type
에 대하여 사용할 수 있으면 신호처리 `숫자-아닌' 값.
bool tinyness_before
:Type
반올림하기 전에 최소 정규화 값으로 표현 가능한지 탐지하도록 허용하면true
를 돌려준다.
bool traps
:Type
이 예외처리(trapping)를 구현했으면true
를 돌려준다.
이 문제를 해결하기 위하여 다형적 포장자(함수객체)를 사용할 수 있다. 다형적 포장자는 함수 포인터나 멤버 함수 또는 함수객체를 참조한다. 매개변수가 유형과 갯수에 부합하기만 하면 된다.
다형적 함수 포장자를 사용하기 전에 `<functional>
' 헤더를 포함해야 한다.
다형적 포장 함수는 std::function
클래스 템플릿을 통하여 사용할 수 있다. 그의 템플릿 인자는 포장자를 두를 함수의 원형이다. 다음은 다형적 포장 함수를 정의하는 예이다. 두 개의 int
값을 기대하고 int
하나를 돌려주는 함수를 가리킨다.
std::function<int (int, int)> ptr2fun;여기에서 템플릿의 매개변수는
int (int, int)
이다. 함수가 두 개의 int
인자를 기대하고 하나의 int
를 돌려준다는 뜻이다. 다른 원형은 부합하는 다른 함수 포장자를 돌려준다.
그런 함수 포장자는 이제 자신이 포장한 어떤 함수도 가리키는 데 사용할 수 있다. 예를 들어 `plus<int> add
'는 int operator()(int, int)
함수 호출 멤버를 정의하는 함수객체를 생성한다. 이것은 원형이 int (int, int)
인 함수의 자격이 있기 때문에 우리의 ptr2fun
는 add
를 가리킬 수 있다.
ptr2fun = add;
ptr2fun
이 아직 함수를 가리키지 않는다면 (그저 정의되어 있을 뿐이라면) 그리고 그것을 통하여 함수를 호출하려고 시도했다면 `std::bad_function_call
' 예외가 던져진다. 함수의 주소가 아직 할당되어 있지 않은 다형적 함수 포장자는 (마치 값이 0인 포인터처럼) 논리 표현식에서 false
값을 나타낸다.
std::function<int(int)> ptr2int; if (not ptr2int) cout << "ptr2int is not yet pointing to a function\n";
다형적 함수 포장자는 또한 함수나 함수객체 또는 다른 다형적 함수 포장자를 참조하는 데 사용할 수 있다. 그 원형에 매개변수나 반환 값에 대하여 표준 변환이 존재하기만 하면 된다. 예를 들어,
bool predicate(long long value); void demo() { std::function<int(int)> ptr2int; ptr2int = predicate; // OK, 매개변수 변환 가능. 유형을 반환함 struct Local { short operator()(char ch); }; Local object; std::function<short(char)> ptr2char(object); ptr2int = object; // OK, object는 함수객체로서 // 변환가능한 매개변수를 가진다. // 유형을 반환함. ptr2int = ptr2char; // OK, 이제 다형성과 함수객체 그리고 포장자를 사용함 }
add
함수의 템플릿 정의를 연구해 보자:
template <typename Container, typename Type> Type add(Container const &container, Type init) { return std::accumulate(container.begin(), container.end(), init); }여기에서
std::accumulate
가 호출된다. container
의 begin
멤버와 end
멤버를 사용한다.
container.begin()
과 container.end()
에 대한 호출은
템플릿 유형 매개변수에 의존한다고 말한다. 컴파일러는 container
의 인터페이스를 보지 못했기 때문에 container
의 begin
멤버와 end
멤버가 실제로 입력 반복자를 돌려주는지 확신할 수 없다.
반면에 std::accumulate
자체는 템플릿 매개변수의 유형에 완전히 독립적이다. 그의 인자는 템플릿 매개변수에 달려 있지만 함수 호출 자체는 거기에 의존하지 않는다. 템플릿 매개변수의 유형에 독립적인 템플릿 몸체의 서술문을 템플릿 매개변수의 유형에 의존하지 않는다라고 말한다.
템플릿 정의를 만나면 컴파일러는 템플릿 매개변수에 의존하지 않는 서술문의 구문적 정확성을 모두 검증한다. 즉, 모든 클래스 정의와 모든 유형 정의 그리고 모든 함수 선언 등등을 이미 다 보았어야 한다. 컴파일러가 필요한 선언과 정의를 다 보지 못했다면 템플릿의 정의를 거부할 것이다. 그러므로 위의 템플릿을 컴파일러에게 제출할 때 numeric
헤더를 먼저 포함했어야 한다. 이 헤더 파일에 std::accumulate
알고리즘이 선언되어 있기 때문이다.
템플릿 매개변수에 의존하는 서술문에 컴파일러는 그런 광범위한 구문적 점검을 수행할 수 없다. 컴파일러는 아직 지정되지 않은 유형인 Container
에 대하여 begin
멤버의 존재를 검증할 방법이 없다. 이 경우 컴파일러는 필요한 멤버와 연산자 그리고 유형이 결국 사용가능하게 될 것이라고 간주하고서 형식적으로 점검한다.
템플릿이 초기화되는 소스의 위치를 구체화 시점이라고 부른다. 구체화 시점에 컴파일러는 템플릿 매개변수의 실제 유형을 추론한다. 그 시점에 템플릿 유형 매개변수에 의존하는 템플릿의 서술문의 구문이 올바른지 점검한다. 이것은 컴파일러가 필요한 선언을 구체화 시점에 이미 보았어야 한다는 것을 암시한다. 제일 규칙으로서 컴파일러가 템플릿을 구체화하는 시점에 필요한 모든 선언을 (보통은 헤더를) 읽었는지 확인해야 한다. 템플릿의 정의 자체에 대해서는 좀 더 느슨하게 요구할 수 있다. 정의를 읽을 때는 템플릿 유형 매개변수에 의존하지 않는 서술문에 요구되는 선언만 제공하면 된다.
컴파일러에게 다음 main
함수를 컴파일해 달라고 요구한다고 가정하자.
int main() { process(3, 3); }또 컴파일러가
main
을 컴파일하려고 할 때 다음 함수 선언을 만났다고 가정하자.
template <typename T> void process(T &t1, int i); // 1 template <typename T1, typename T2> void process(T1 const &t1, T2 const &t2); // 2 template <typename T> void process(T const &t, double d); // 3 template <typename T> void process(T const &t, int i); // 4 template <> void process<int, int>(int i1, int i2); // 5 void process(int i1, int i2); // 6 void process(int i, double d); // 7 void process(double d, int i); // 8 void process(double d1, double d2); // 9 void process(std::string s, int i) // 10 int add(int, int); // 11컴파일러는
main
의 서술문을 이미 읽었으므로 이제 어느 함수를 실제로 호출해야 하는지 결정해야 한다. 다음과 같이 처리된다.
이것은 적어도 인자의 갯수는 생존 가능한 함수의 매개변수의 갯수에 일치해야 한다는 사실을 암시한다. 함수 10에서 첫 인자는 string
이다. string
은 int
값으로 초기화할 수 없으므로 적절한 변환이 존재하지 않는다. 그래서 함수 10은 후보에서 제외된다. double
매개변수는 유지할 수 있다. 표준으로 int
를 double
로 변환할 수 있다. 그래서 double
매개변수를 가진 모든 함수들은 생존한다. 그러므로 생존 가능한 함수의 집합은 1부터 9까지이다.
실제 유형을 템플릿 유형 매개변수에 맞추어 볼때 세 가지 표준 템플릿 매개변수 변형 절차 중 무엇이든 사용할 수 있다 (21.4절). 이렇게 처리하다가 함수 1의 T &t1
매개변수에 있는 T
에 대하여 어떤 유형도 결정할 수 없다고 결론을 내린다. 인자 3
은 상수 int
값이기 때문이다. 그래서 함수 1은 생존 가능한 함수 리스트에서 제외된다. 이제 컴파일러는 잠재적으로 구체화가 가능한 함수 템플릿과 평범한 함수들을 마주하게 되었다.
void process(T1 [= int] const &t1, T2 [= int] const &t2); // 2 void process(T [= int] const &t, double d); // 3 void process(T [= int] const &t, int i); // 4 void process<int, int>(int i1, int i2); // 5 void process(int i1, int i2); // 6 void process(int i, double d); // 7 void process(double d, int i); // 8 void process(double d1, double d2); // 9
컴파일러는 직접 일치 갯수 값을 각각의 생존가능한 함수에 연관짓는다. 직접적 일치 갯수는 (자동) 유형 변환 없이 함수 매개변수에 일치하는 인자의 갯수를 센다. 예를 들어 함수 2에 대하여 이 갯수는 2이고 함수 7에 대해서는 1이며 함수 9는 0이다. 함수들은 이제 직접 일치 갯수를 기준으로 (내림차순으로) 정렬된다.
부합 횟수 void process(T1 [= int] const &t1, T2 [= int] const &t2); 2 // 2 void process(T [= int] const &t, int i); 2 // 4 void process<int, int>(int i1, int i2); 2 // 5 void process(int i1, int i2); 2 // 6 void process(T [= int] const &t, double d); 1 // 3 void process(int i, double d); 1 // 7 void process(double d, int i); 1 // 8 void process(double d1, double d2); 0 // 9최상위 값에 무승부가 없다면 상응하는 함수가 선택되고 함수 선택 과정은 완료된다.
상단에 여러 함수가 나타나면 컴파일러는 모호성이 없는지 검증한다. 유형 변환이 요구되(지 않)는 매개변수의 순서가 다르면 모호성에 봉착한다. 예를 들어 함수 3과 8을 연구해 보자. `직접 일치'에는 D를 사용하고 `변환'에는 C를 인자로 사용한다고 하자. 그러면 함수 3에는 인자가 D와 C가 되고 그리고 함수 8에는 C와 D가 된다. 2와 4 그리고 5와 6을 사용할 수 없다고 가정하면 컴파일러는 모호하다고 보고할 것이다. 함수 3과 8에 대하여 인자/매개변수의 부합 절차가 다르기 때문이다. 함수 7과 8을 비교하면 똑 같이 차이가 있다. 그러나 함수 3과 7을 비교하면 그런 차이가 없다.
이 시점에서 최상위 값에 무승부가 있고 컴파일러는 관련 함수들을 처리한다 (함수 2와 4 그리고 5와 6). 이 함수들의 비-템플릿 매개변수의 갯수를 세어 이 평범한 매개변수 갯수를 각각의 함수에 연관짓는다. 함수들은 이 갯수를 기준으로 내림차순으로 정렬된다. 그 결과는 다음과 같다.
평범한 매개변수의 갯수 void process(int i1, int i2); 2 // 6 void process(T [= int] const &t, int i); 1 // 4 void process(T1 [= int] const &t1, T2 [= int] const &t2); 0 // 2 void process<int, int>(int i1, int i2); 0 // 5이제 최상위 값에 무승부가 없다. 상응하는 함수가 (
process(int, int)
, 함수 6) 선택되고 함수 선택 처리는 완료된다. 함수 6이 main
의 함수 호출 서술문에 사용된다.
함수 6이 정의되어 있지 않았다면 함수 4가 사용되었을 것이다. 함수 4도 함수 6도 정의되어 있지 않았다면 함수 2와 5에 선택 처리가 계속되었을 것이다.
평범한 매개변수의 갯수 void process(T1 [= int] const &t1, T2 [= int] const &t2); 0 // 2 void process<int, int>(int i1, int i2); 0 // 5
이 상황에서 다시 무승부를 만난다. 그리고 선택 절차는 계속된다. `함수 유형' 값을 평범한 매개변수 갯수가 가장 높은 각각의 함수에 연관짓는다. 그리고 이 함수들은 함수 값의 유형을 기준으로 내림차순으로 정렬된다. 값 2는 평범한 함수에 연관되고 값 1은 템플릿의 명시적인 특정화에 연관되며 그리고 값 0은 평범한 함수 템플릿에 연관된다.
최상위 값에 무승부가 없다면 상응하는 함수가 선택되고 함수 선택은 완료된다. 무승부가 있다면 컴파일러는 모호성을 보고하고 어느 함수를 호출할지 결정하지 못한다. 함수 2와 5만 존재한다고 간주하면 이 선택 과정은 다음과 같은 순서가 되었을 것이다.
함수 유형 void process<int, int>(int i1, int i2); 1 // 5 void process(T1 [= int] const &t1, T2 [= int] const &t2); 0 // 2템플릿을 명시적으로 특정화한 함수 5가 선택되었을 것이다. 다음은 함수 템플릿 선택 메커니즘을 요약한 것이다 (그림 25 참고):
struct Int { typedef int type; };이 시점에서
typedef
를 구조체 안에 내장하는 것이 이상하게 보일지 모르겠지만 제 23장에서 이것이 실제로 무척 유용한 상황을 만나보게 될 것이다. 템플릿이 요구하는 유형의 변수를 정의할 수 있다. 예를 들어 (다음 함수 매개변수 리스트에 typename
을 사용한 것은 무시한다. 자세한 것은 22.2.1항을 참고하라):
template <typename Type> void func(typename Type::type value) { }
func(10)
을 호출하려면 Int
를 명시적으로 지정해야 한다. 많은 구조체에서 type
을 정의하고 있기 때문이다. 컴파일러는 도움이 필요하다. 올바른 호출은 func<Int>(10)
이다. 이제 Int
를 의도한다는 것이 명백하므로 컴파일러는 value
가 int
라고 올바르게 추론한다.
그러나 템플릿은 중복정의가 될 수 있고 다음 정의를 보면:
template <typename Type> void func(Type value) {}이제 이 함수를 확실하게 사용하기 위해
func<int>(10)
를 지정하면 역시 문제없이 컴파일된다.
그러나 이전 절에서 보았듯이 컴파일러는 어느 템플릿을 구체화할지 결정할 때 함수 원형의 매개변수 유형과 실제로 제공된 인자 유형을 맞추어 보고 가능한 함수 리스트를 만든다. 그렇게 하려면 컴파일러는 매개변수의 유형을 결정할 수 있어야 하는데 바로 여기에 문제가 있다.
Type = int
를 평가할 때 컴파일러는 (첫 번째 템플릿 정의인) func(int::type)
의 원형과 (두 번째 템플릿 정의인) func(int)
의 원형을 만난다. 그러나 int::type
은 없다. 그래서 에러가 자주 일어난다. 그렇지만 그 에러는 제공된 템플릿 유형 인자를 다양한 템플릿 정의로 교체했기 때문이다.
템플릿 정의에서 유형을 교체함으로써 야기되는 유형-문제는 에러로 간주되지 않는다. 그저 그 특별한 유형이 특별한 템플릿에 사용될 수 없다는 사실을 알려줄 뿐이다.
이 원칙은 교체 실패는 에러가 아니다라는 것으로 알려져 있다 (SFINAE). 그리고 컴파일러가 (여기에 보여주듯이) 간단한 중복정의 함수를 선택하는 데 뿐만 아니라 여러 가능한 특정화 템플릿 중에서 고를 때에도 사용한다 (제 22장과 23장).
if constexpr (cond)
구문이 있다. if
선택 서술문이 사용되는 모든 곳에 사용할 수 있지만, 특정한 사용법은 함수 템플릿에 있다. if constexpr
으로 컴파일러는 컴파일 시간에 if constexpr (cond)
절을 평가하여 템플릿 함수를 부분적으로 구체화할 수 있다.
다음은 예제이다.
1: void positive(); 2: void negative(); 3: 4: template <int value> 5: void fun() 6: { 7: if constexpr (value > 0) 8: positive(); 9: else if constexpr (value < 0) 10: negative(); 11: } 12: 13: int main() 14: { 15: fun<4>(); 16: }
if constexpr
서술문이 시작한다. value
는 템플릿 비-유형 매개변수이므로 그의 값은 컴파일 시간에 사용할 수 있다. 조건 구역에 있는 값들도 마찬가지이다.
fun<4>()
가 호출된다. 그러므로 7번 줄의 조건은 true
이고 9번 줄의 조건은 false
이다.
fun<4>()
를 다음과 같이 구체화한다.
void fun<4>() { positive(); }
if constexpr
서술문 자체는 실행 코드가 되지 않음을 눈여겨보라. 컴파일러가 구체화할 부분을 선택하는 데 사용될 뿐이다. 이 경우 positive
만 적절하게 구체화된다. 프로그램이 링크 단계에 들어 가기 전에 사용가능하기 때문이다.
템플릿 선언이 모두 템플릿 정의로 변환되는 것은 아니다. 정의가 제공된다면 명시적으로 선언된 것이다.
template <typename Type1, typename Type2> void function(Type1 const &t1, Type2 const &t2);
template void function<int, double>(int const &t1, double const &t2);
void (*fp)(double, double) = function<double, double>; void (*fp)(int, int) = function<int, int>;
template <> void function<char *, char *>(char *const &t1, char *const &t2);
friend void function<Type1, Type2>(parameters);
변수 템플릿은 익숙한 template
헤더로 시작하여 다음에 변수 자체의 정의가 따라온다. 템플릿 헤더는 유형을 지정한다. 거기에 기본 유형을 지정해도 된다. 예를 들어,
template<typename T = long double> constexpr T pi = T(3.1415926535897932385);
이 변수를 사용하려면 유형을 지정해야 한다. 그리고 초기화된 그 값을 지정된 유형으로 변환할 수 있으면 컴파일러가 조용하게 변환해 준다.
cout << pi<> << ' ' << pi<int>;두 번째 삽입에서
long double
초기화 값은 int
로 변환되고 3을 화면에 보여준다.
특정화도 지원한다. 예를 들어 텍스트 `pi'를 보여주기 위해 char const *
유형에 대한 특정화는 다음과 같이 정의할 수 있다.
template<> constexpr char const *pi<char const *> = "pi";이 특정화로
cout << pi<char const *>
을 하면 pi
를 보여줄 수 있다.