21.5: 함수 템플릿 선언하기
template <typename T>
T add(T a, T b);
선언만 하는 경우에는 프로그래머가 요구된 실체가 존재하는지 확인하는 책임
21.5.1: 구체화 선언
함수 템플릿을 사용하면 컴파일 속도와 링크 속도가 향상,
최종 링크 시에 실제 템플릿 구체화 존재를 확인하기 위해 명시적 구체화 선언 사용
template
으로 시작하고 템플릿의 매개변수 리스트는 생략// 함수 템플릿 정의
template <typename T>
void myFunction(T parameter) {
// 내용
}
// 템플릿 구체화 선언
template void myFunction<int>(int parameter);
선언이지만 컴파일러는 함수 템플릿의 특별한 변형을 구체화해 달라는 요청으로 이해
명시적으로 구체화를 선언하면 프로그램이 요구하는 템플릿 함수의 실체를 모두 파일 하나에 모을 수 있음
이 파일은 보통의 소스 파일로서 템플릿 정의 헤더를 포함해야 하고 이어서 필요한 명시적인 구체화 선언 지정
소스 파일이기 때문에 다른 소스에 포함되지 않음
add
함수 템플릿에 대하여 요구한 구체화를 보여주는 예
#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);
빠진 구체화 선언을 위 리스트에 추가하기만 하면 된다. 파일을 다시 컴파일하고 링크하면 완성
21.6: 함수 템플릿을 구체화하기
컴파일러가 정의를 읽기만 하면 코드가 되는 평범한 함수와 다르게 템플릿은 정의를 읽어도 구체화되지 않음
컴파일러가 템플릿을 구체화하기로 결정하는 상황 두 가지
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()
{
cout << fun(add);
}
후보 함수가 두 개이기 때문에 모호 : fun
의 중복정의 버전 각각에 대하여 add
함수를 구체화할 수 있기 때문
함수 템플릿은 모호성이 없을 경우에만 구체화될 수 있다.
#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()
{
cout << fun(static_cast<int (*)(int const &, int const &)>(add));
}
static_cast
를 사용하여 해결할 수도 있으나 유형을 강제로 변환하는 것은 피하는 것이 좋다
21.6.1: 구체화: `코드 비만' 없음
이 코드는 fun
함수를 통해 add
함수의 주소를 출력하는 간단한 예제
// source1.cc _ 함수 포인터와 void 포인터를 담는 구조체 정의
union PointerUnion
{
int (*fp)(int const &, int const &); // int-유형 인자를 받아들이는 함수 포인터
void *vp; // void 포인터
};
#include <iostream>
#include "add.h"
#include "pointerunion.h"
// fun 함수 정의
void fun()
{
PointerUnion pu = { add }; // PointerUnion 구조체를 선언하고, add 함수의 주소를 대입
std::cout << pu.vp << '\n'; // 함수 포인터의 주소 출력
}
PointerUnion
구조체:union
키워드를 사용하여 PointerUnion
이라는 구조체를 정의fp
: int
형식의 두 인자를 받아들이는 함수 포인터vp
: void
포인터<iostream>
헤더: C++의 표준 입출력 스트림을 사용하기 위해 포함"add.h"
헤더: add
템플릿 함수의 정의가 있는 헤더 파일을 포함"pointerunion.h"
헤더: PointerUnion
구조체의 정의가 있는 헤더 파일을 포함fun
함수 정의:fun
함수는 아무런 인자도 받지 않고 반환값도 없는 함수PointerUnion
타입의 변수 pu
를 선언하고, 이를 add
함수의 주소로 초기화PointerUnion
구조체를 사용하여 함수 포인터와 void 포인터를 다루고 있음
source2.cc
파일은 source1.cc
와 유사하게 fun
함수를 정의하고 있지만, add
템플릿에 대한 선언만 사용해 템플릿 선언
// source2.cc
#include <iostream>
#include "pointerunion.h"// 템플릿 선언: add 템플릿의 정의가 아닌 선언만 포함
template<typename Type>
Type add(Type const &, Type const &);
void fun()
{
// PointerUnion 구조체를 선언하고, add 템플릿의 주소를 대입
PointerUnion pu = { add };
// 함수 포인터의 주소 출력
std::cout << pu.vp << '\n';
}
"pointerunion.h"
헤더: PointerUnion
구조체의 정의가 있는 헤더 파일 포함template<typename Type> Type add(Type const &, Type const &);
와 같이 add
템플릿에 대한 선언만 포함fun
함수 정의:fun
함수는 아무런 인자도 받지 않고 반환값도 없는 함수PointerUnion
타입의 변수 pu
선언하고, 이를 add
템플릿의 주소로 초기화source2.cc
는 add
템플릿에 대한 선언만 제공, 해당 템플릿의 실체는 이 파일에서 정의되지 않음
이 파일은 다른 파일에서 정의된 add
템플릿의 실체를 사용하고 있음을 나타내는 역할
main.cc
파일은 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';
}
설명:
"add.h"
헤더: add
템플릿 함수의 정의가 있는 헤더 파일을 포함"pointerunion.h"
헤더: PointerUnion
구조체의 정의가 있는 헤더 파일을 포함void fun();
: fun
함수의 선언을 제공, 함수의 정의는 다른 파일에서 이루어짐main
함수 정의:int main()
: 프로그램의 시작점인 main
함수를 정의PointerUnion
타입의 변수 pu
를 선언하고, 이를 add
함수의 주소로 초기화fun
함수를 호출main.cc
에서는 add
템플릿에 대한 정의가 제공되고 있으며, 이 정의된 함수의 주소를 PointerUnion
사용하여 출력
fun
함수를 호출하여 프로그램의 실행 흐름을 다룸
두 가지 다른 소스 파일로부터 생성된 오브젝트 모듈을 링크한 최종 프로그램
템플릿의 동작과 링커 동작에 대한 일부 특징
source1.o
와 source2.o
는 템플릿 함수 add
의 구체화 여부에 따라 크기가 다름source1.o
에는 add
템플릿의 실체가 구체화되어 있으므로 크기가 큼source2.o
에는 add
템플릿의 선언만 있어서 크기가 작음main.o
와 source1.o
링크한 경우, 두 모듈에 같은 템플릿 함수의 실체가 존재해 결과 프로그램은 해당 함수의 주소 두 번 출력main.o
와 source2.o
링크한 경우, 하나에만 템플릿 함수의 실체가 있어 결과 프로그램은 해당 함수의 주소 한 번만 출력source2.o
와 템플릿 정의가 있는 source1.o
를 링크하더라도 결과 프로그램은 하나의 템플릿 함수의 실체만 가짐이 실험을 통해 템플릿이 어떻게 동작하며, 링커가 최종 프로그램에 어떤 영향을 미치는지에 대한 통찰을 얻을 수 있음
21.7: 명시적인 템플릿 유형 사용하기
두 개의 중복 정의된 함수 버전이 서로 다른 유형의 인자를 기대하고 있었는데, 함수 템플릿을 구체화할 때 동일한 유형의 인자가 제공되면서 모호성 발생
해당 모호성을 해결하는 방법 중 하나는 static_cast
를 사용하는 것이었으나, 강제 형변환은 가능하면 피하는 것이 좋음
명시적인 템플릿 유형 인자를 사용하여 이러한 모호성을 해결할 수 있음
이를 통해 컴파일러에게 함수 템플릿을 구체화할 때 사용해야 할 실제 유형을 알려줄 수 있습니다.
아래는 명시적인 템플릿 유형 인자를 사용하여 모호성을 해결하는 예제 코드
#include <iostream>
#include "add.h"
// int 유형의 함수를 기대하는 버전
int fun(int (*f)(int const &lvalue, int const &rvalue));
// double 유형의 함수를 기대하는 버전
double fun(double (*f)(double const &lvalue, double const &rvalue));
int main()
{
// 명시적인 템플릿 유형 인자를 사용하여 add<int>를 전달
std::cout << fun(add<int>) << '\n';
}
컴파일러에게 fun
함수를 호출할 때 사용해야 하는 실제 템플릿 유형을 알려주는 것이 가능
-(*f)
는 함수 포인터를 나타냅니다. 여기서 f
는 함수 포인터 변수이며, (*f)
는 해당 함수 포인터가 가리키는 함수를 호출하는 것을 의미
21.8: 함수 템플릿 중복정의하기
int main()
{
add(add(2, 3), 4);
}
세 개의 개체의 합을 계산하고 싶을 경우 다음과 같이 사용할 수 있다.
객체 세 개를 자주 사용할 필요가 있다면 세 개의 인자를 받는 중복 정의 버전의 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());
}
std::accumulate
를 사용하여 벡터의 시작과 끝 지정, 초기값으로 Type()
사용
//std::vector
의 원소들의 합 계산하여 반환
원래의 함수 정의:
cppCopy code
template <typename Container, typename Type>
Type add(Container const &cont, Type const &init)
{
return std::accumulate(cont.begin(), cont.end(), init);
}
//init
매개변수를 매개변수 리스트에서 빼버리고 기본 초기화 값을 사용할 수 있는데, 이렇게 변경하면 함수 호출 시 init
값을 생략할 수 있게 됩니다. 그러나 순서를 바꾸어서 Type
을 맨 앞에 두면서 함수 호출 시에 Type
을 명시적으로 지정해주는 형태가 됩니다.
변경된 함수 정의:
cppCopy code
template <typename Type, typename Container>
Type add(Container const &cont)
{
return std::accumulate(cont.begin(), cont.end(), Type());
}
//순서를 바꾸어서 Type
이 앞에 오면, 예를 들어 add<int>(vectorOfInts)
처럼 템플릿 유형 매개변수 Type
을 명시적으로 지정해주어야 합니다. 그렇지 않으면 컴파일러가 Type
을 결정할 수 없는 상황이 발생합니다.
그리고 더 나아가, 템플릿 템플릿 매개변수를 사용하면 컴파일러가 컨테이너로부터 직접 Type
을 결정할 수 있게 됩니다. 이 부분은 뒤에서 논의될 템플릿 템플릿 매개변수(Template Template Parameter)에 관한 내용입니다.
21.8.1: 중복정의 함수 템플릿을 사용하는 예제
int main()
{
vector<int> v;
add(3, 4); // 1
add(v); // 2
add(v, 0); // 3
}
1) 두 개 모두 int
그러므로 add<int>
구체화
2) std::vector
를 기대하는 중복정의 함수 템플릿을 선택
add<int>(std::vector<int> const &)
3) 유형이 다른 두 개의 개체 기대하는 정의, std::vector
가 begin
과 end
지원
add<std::vector<int>, int>(std::vector<int> const &, int const &)
21.8.2: 함수 템플릿을 중복정의할 때의 모호성에 관하여
유형에 상관없이 인자를 받는다. 이 함수 템플릿은 operator+
가 함수의 실제 사용된 유형 사이에 정의되어 있지만 그 말고는 문제가 없어 보일 경우에만 사용할 수 있다. 다음은 유형을 가리지 않고 인자를 받는 중복정의 버전이다.
template <typename Type1, typename Type2, typename Type3>
Type1 add(Type1 const &lvalue, Type2 const &mvalue, Type3 const &rvalue)
{
return lvalue + mvalue + rvalue;
}
main()
{
add(1, 2, 3);
}
컴파일러는 Type == int
이라고 추론하고서 앞의 함수를 선택할 수 있지만 Type1 == int
와 Type2 == int
그리고 Type3 == int
라고 추론하고서 뒤의 함수를 선택할 수도 있다. 놀랍게도 컴파일러는 모호하다고 불평하지 않는다.
21.9: 유형을 우회하기 위하여 템플릿 특정화하기
최초의 add
템플릿은 대부분 잘 작동하지만, char *
와 같이 일부 유형에서는 무의미한 경우가 있음
함수 템플릿이나 클래스 템플릿에 대해 특정한 유형이나 값에 대한 명시적인 정의를 제공하는 것
template <typename Type>
Type *add(Type const *t1, Type const *t2)
{
std::cout << "Pointers\n";
return new Type;
}
컴파일러는 인자의 일치성과 형 변환을 고려하여 가장 적절한 함수 템플릿 선택
명시적인 특정화는 특수한 경우에 사용되지만, 일반적인 경우에서는 원래의 템플릿이 선택되는 것이 일반적
~~
21.9.1: 너무 많은 특정화를 피하기
add
함수 템플릿에 문자를 가리키는 const
그리고 비const
포인터에 대하여 두 개의 특정화char const *
에 대한 것이고 다른 하나는 char *
에 대한 것// 원래의 함수 템플릿
template <typename Type>
Type add(Type const &lvalue, Type const &rvalue)
{
return lvalue + rvalue;
}
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();
}
21.9.2: 특정화 선언하기
템플릿의 명시적 특정화를 선언할 때에는 보통 함수 템플릿과 비슷한 방식으로 선
몸체를 ;으로 대체
템플릿의 명시적인 특정화 선언 시, 컴파일러가 유형을 추론할 수 있으면 템플릿 유형 매개변수를 명시적으로 지정하지 않아도 됨
char (const) *
특정화의 경우라면 다음과 같이 선언
template <> char *add(char *const &p1, char *const &p2)
template <> char const *add(char const *const &p1, char const *const &p2);
게다가 template <>
을 생략할 수 있다면 템플릿 문자(쌍반점)는 선언으로부터 제거될 것이다.
결과로 나온 선언은 이제 단순히 함수 선언에 불과하다. 이것은 에러가 아니다.
함수 템플릿과 평범한 (비-템플릿) 함수는 서로 중복정의할 수 있다.
평범한 함수는 함수 템플릿에 비해 유형 변환에 제한이 없다. 이 때문에 이따금씩 평범한 함수로 템플릿을 중복정의하기도 한다.
함수 템플릿의 명시적 특정화는 그냥 함수 템플릿의 또다른 중복정의 버전에 불과하다.
중복정의 버전은 완전히 다른 집합의 템플릿 매개변수를 정의할 수 있는 반면에, 특정화는 비-특정화된 변형과 똑 같은 템플릿 매개변수의 집합을 사용해야 한다.
컴파일러는 실제 템플릿 인자가 특정화로 정의된 유형에 부합하는 상황에 특정화를 사용한다 (인자 집합에 부합하여 매개변수가 가장 특정화된 집합이 사용된다는 규칙을 따른다).
다양한 매개변수 집합에 대하여 중복정의 버전의 함수를 (또는 함수 템플릿을) 사용해야 한다.
컴파일러는 함수의 매개변수로부터 템플릿 인자를 추론할 수 있음
template <>
생략하면 컴파일러는 그냥 일반 함수 중복 정의로 처리 하지만 명시적으로 적어주는 것이 가독성 높이고 명시적으로 함수가 명시적인 특수화라는 것 나타냄중복 정의된 함수 템플릿과 동일한 매개변수 세트를 사용해야 함
명시적 특수화를 사용하면 특정한 유형에 대해 다른 구현을 제공할 수 있어 유용
명시적인 특정화와 중복정의를 다루어 보았으므로 이제 클래스가 std::string
변환 연산자를 정의할 때
변환 연산자는 rvalue로 사용될 것이라고 보장된다. 이것은 string
변환 연산자가 정의된 클래스의 객체를 string
객체에 할당할 수 있다는 뜻이다. 그러나 string
변환 연산자를 정의한 객체를 스트림에 삽입하려고 시도하면 컴파일러는 부적절한 유형을 ostream
에 삽입하려 한다고 불평한다.
반면에 이 클래스가 int
변환 연산자를 정의하고 있으면 문제없이 삽입된다.
이렇게 구분하는 이유는 operator<<
가 기본 유형을 (예를 들어 int
유형을) 삽입할 때는 평범한 (자유) 함수로 정의되어 있지만 string
을 삽입할 때는 함수 템플릿으로 정의되어 있기 때문이다. 그러므로 string
변환 연산자를 정의한 클래스의 객체를 삽입하려고 할 때 컴파일러는 ostream
객체로 삽입하는 삽입 연산자의 모든 중복정의 버전을 방문한다.
기본 유형의 변환이 없으므로 기본 유형의 삽입 연산자는 사용할 수 없다. 템플릿 인자에 대한 변환은 변환 연산자를 찾아 보도록 컴파일러에게 허용하지 않기 때문에 string
변환 연산자를 정의한 우리의 클래스는 ostream
에 삽입할 수 없다.
그런 클래스의 객체를 ostream
객체에 삽입한다고 하더라도 클래스는 (string
인자에서 클래스의 객체를 rvalue로 사용하는 데 요구되는 string
변환 연산자 말고도) 반드시 자신만의 중복정의 삽입 연산자를 정의해야 한다.
클래스가 std::string
변환 연산자를 정의할 때, 해당 객체를 ostream
에 삽입하려고 하면
컴파일러는 부적절한 유형을 ostream
에 삽입하려 한다고 함
이는 operator<<
가 기본 유형을 삽입할 때는 평범한 함수로 정의되어 있지만,
string
을 삽입할 때는 함수 템플릿으로 정의되어 있기 때문
그러므로 string 변환 연산자를 정의한 클래스의 객체를 삽입하려고 할 때
컴파일러는 ostream 객체로 삽입하는 삽입 연산자의 모든 중복정의 버전을 방문
이 클래스의 객체를 ostream 객체에 삽입하더라도 클래스는 반드시 자체적인 중복정의 삽입 연산자를 정의해야 함