Program Language/C & C++

[C++] Function Template

야곰야곰+책벌레 2022. 2. 17. 20:10
728x90
반응형

많은 사람들에게 템플릿의 첫 번째이자 가장 명백한 용도는 vector, list 및 map과 같은 컨테이너 클래스를 정의하고 사용하는 것이다. 곧 이러한 컨테이너를 조작하기 위한 함수 템플릿의 필요성이 발생한다. 벡터 정렬은 간단한 예다.

template<typename T> void sort(vector<T>&); // declaration
void f(vector<int>& vi, vector<string>& vs)
{
	sort(vi); // sor t(vector<int>&);
	sort(vs); // sor t(vector<str ing>&);
}

함수 템플릿이 호출되면 함수 인수의 유형에 따라 사용되는 템플릿 버전이 결정된다. 즉, 템플릿 인수는 함수 인수에서 추론된다. 당연히 함수 템플릿은 어딘가에 정의되어야 한다.

template<typename T>
void sort(vector<T>& v) // definition
	// Shell sort (Knuth, Vol. 3, pg. 84)
{
	const size_t n = v.siz e();
	for (int gap=n/2; 0<gap; gap/=2)
		for (int i=gap; i<n; i++)
			for (int j=i−gap; 0<=j; j−=gap)
				if (v[j+gap]<v[j]) { // swap v[j] and v[j+gap]
					T temp = v[j];
					v[j] = v[j+gap];
					v[j+gap] = temp;
				}
}

이 템플릿화 된 버전은 정렬하는 요소 유형에 대한 더 많은 정보에 의존할 수 있기 때문에 더 깔끔하고 짧다. 일반적으로 비교를 위해 포인터에 의존하지 않기 때문에 더 빠르다. 이것은 간접 함수 호출이 필요하지 않으며 간단한 '<' 인라인이 쉽다는 것을 의미한다.

추가 단순화는 표준 라이브러리 템플릿 swap()을 사용하여 자연적인 형태로 줄이는 것이다.

if (v[j+gap]<v[j])
	swap(v[j],v[j+gap]);

이로 인해 새로운 오버헤드가 발생하지 않는다. 더 좋은 점은 표준 라이브러리 swap()이 move semantics 체계를 사용하므로 속도 향상을 볼 수 있다는 것이다.

이 예제에서 '<' 연산자는 비교에 사용된다. 그러나 모든 유형에 '<' 연산자가 있는 것은 아니다. 이것은 이 버전의 sort() 사용을 제한하지만 인수를 추가하여 제한을 쉽게 피할 수 있다.

template<typename T, typename Compare = std::less<T>>
void sort(vector<T>& v) // definition
	// Shell sort (Knuth, Vol. 3, pg. 84)
{
	Compare cmp; // make a default Compare object
	const size_t n = v.siz e();
    
	for (int gap=n/2; 0<gap; gap/=2)
		for (int i=gap; i<n; i++)
			for (int j=i−gap; 0<=j; j−=gap)
				if (cmp(v[j+gap],v[j]))
					swap(v[j],v[j+gap]);
}

이제 기본 비교 연산(<)을 사용하여 정렬하거나 자체적으로 제공할 수 있다.

struct No_case {
	bool operator()(const string& a, const string& b) const; // compare case insensitive
};

void f(vector<int>& vi, vector<string>& vs)
{
	sort(vi); // sor t(vector<int>&)
	sort<int,std::greater<int>>(vi); // sor t(vector<int>&) using greater
    
	sort(vs); // sor t(vector<str ing>&)
	sort<string,No_case>(vs); // sor t(vector<str ing>&) using No_case
}

불행히도 후행 템플릿 인수만 지정할 수 있다는 규칙으로 인해 비교 작업을 지정할 때 요소 유형을 (추론하기보다) 지정해야 한다.

Function Template Argument

함수 템플릿은 다양한 컨테이너 유형에 적용할 일반 알고리즘을 작성하는데 필수적이다. 함수 인수에서 호출에 대한 템플릿 인수를 추론하는 기능은 중요하다.

컴파일러는 함수 인수 목록이 템플릿 인수 집합을 고유하게 식별하는 경우 호출에서 유형 및 비유형 인수를 추론할 수 있다.

template<typename T, int max>
struct Buffer {
	T buf[max];
public:
	// ...
};

template<typename T, int max>
T& lookup(Buffer<T,max>& b, const char∗ p);

Record& f(Buffer<string,128>& buf, const char∗ p)
{
	return lookup(buf,p); // use the lookup() where T is string and i is 128
}

여기서 lookup()의 T는 string으로 추론되고 max는 128로 추론된다.

클래스 템플릿 매개변수는 추론되지 않는다. 그 이유는 클래스에 대해 여러 생성자가 제공하는 유연성으로 인해 많은 경우 이러한 추론이 불가능하고 더 많은 경우 모호하기 때문이다. 대신, 전문화는 템플릿 대체 정의 사이에서 암시적으로 선택하기 위한 메커니즘을 제공한다. 추론된 유형의 객체를 생성해야 하는 경우 추론(및 생성)을 수행하는 함수를 호출하여 종종 그렇게 할 수 있다. 예를 들어, 표준 라이브러리의 make_pair()의 간단한 변형을 해보자.

template<typename T1, typename T2>
pair<T1,T2> make_pair(T1 a, T2 b)
{
	return {a,b};
}

auto x = make_pair(1,2); // x is a pair<int,int>
auto y = make_pair(string("New York"),7.7); // y is a pair<str ing,double>

템플릿 인수를 함수 인수에서 추론할 수 없는 경우 명시적으로 지정해야 한다. 이는 템플릿 클래스에 대해 템플릿 인수가 명시적으로 지정되는 것과 동일한 방식으로 수행된다.

template<typename T>
T∗ create(); // make a T and return a pointer to it

void f()
{
	vector<int> v; // class, template argument int
	int∗ p = create<int>(); // function, template argument int
	int∗ q = create(); // error : can’t deduce template argument
}

함수 템플릿에 대한 반환 유형을 제공하기 위해 명시적 사양을 사용하는 것은 매우 일반적이다. 이를 통해 객체 생성 함수 및 변환 함수 제품군을 정의할 수 있다. static_cast, dynamic_cast 등의 구문은 명시적으로 한정된 함수 템플릿 구문과 일치한다. 경우에 따라 기본 템플릿 인수를 사용하여 명시적 자격을 단순화할 수 있다.

Function Template Argument Deduction

컴파일러는 형식 템플릿 인수 T 또는 TT와 형식이 아닌 템플릿 인수 I를 다음 구문으로 구성된 형식을 가진 템플릿 함수 인수에서 추론할 수 있다.

여기서 args_TI는 이러한 규칙을 재귀적으로 적용하여 T 또는 I를 결정할 수 있는 매개변수 목록이고, args는 연역을 허용하지 않는 매개변수 목록이다. 이러한 방식으로 모든 매개변수를 추론할 수 없는 경우 호출이 모호하다.

template<typename T, typename U>
void f(const T∗, U(∗)(U));

int g(int);

void h(const char∗ p)
{
	f(p,g); // T is char, U is int
	f(p,h); // error : can’t deduce U
}

f()의 첫 번째 호출의 인수를 보면 템플릿 인수를 쉽게 추론할 수 있다. f()의 두 번째 호출을 보면 h()의 인수와 반환 유형이 다르기 때문에 h()가 패턴 U(*)(U)와 일치하지 않는다는 것을 알 수 있다.

템플릿 매개변수가 둘 이상의 함수 인수에서 추론될 수 있는 경우 각 추론의 결과는 동일한 유형이어야 한다. 그렇지 않으면 호출은 오류다.

template<typename T>
void f(T i, T∗ p);

void g(int i)
{
	f(i,&i); // OK
	f(i,"Remember!"); // error, ambiguous: T is int or T is const char?
}

Reference Deduction

lvalue와 rvalue에 대해 서로 다른 조치를 취하는 것이 유용할 수 있다. {int,pointer} 쌍을 보유하는 클래스를 보자.

template<typename T>
class Xref {
public:
	Xref(int i, T∗ p) // store a pointer: Xref is the owner
		:index{i}, elem{p}, owner{true}
	{ }
    
	Xref(int i, T& r) // store a pointer to r, owned by someone else
		:index{i}, elem{&r}, owner{false}
	{ }
    
	Xref(int i, T&& r) // move r into Xref, Xref is the owner
		:index{i}, elem{new T{move(r)}}, owner{true}
	{ }
    
	˜Xref()
	{
		if(owned) delete elem;
	}
	// ...
private:
	int index;
	T∗ elem;
	bool owned;
};

그래서,

string x {"There and back again"};

Xref<string> r1 {7,"Here"}; // r1 owns a copy of str ing{"Here"}
Xref<string> r2 {9,x}; // r2 just refers to x
Xref<string> r3 {3,new string{"There"}}; // r3 owns the string{"There"}

여기서 r1은 x가 rvalue이기 때문에 Xref(int, string&&)를 선택한다. 마찬가지로 r2는 x가 lvalue이기 때문에 Xref(int, string&)를 선택한다.

lvalue와 rvalue는 템플릿 인수 추론으로 구분된다. X 유형의 lvalue는 X&로 추론되고 rvalue는 X로 추론된다. 이것은 템플릿이 아닌 인수 rvalue 참조에 대한 값의 바인딩과 다르지만 특히 인수 전달에 유용하다. free store에서 외부 참조를 만들고 unique_ptr를 반환하는 팩토리 함수를 작성해 보자.

template<typename T>
	T&& std::forward(typename remove_reference<T>::type& t) noexcept; // §35.5.1
template<typename T>
	T&& std::forward(typename remove_reference<T>::type&& t) noexcept;
template<typename TT, typename A>
unique_ptr<TT> make_unique(int i, A&& a) // simple var iant of make_shared (§34.3.2)
{
	return unique_ptr<TT>{new TT{i,forward<A>(a)}};
}

make_unique <T>(arg)가 가자 복사본을 만들지 않고 arg에서 T를 생성하기를 원한다. 이를 위해서 lvalue/rvalue 구분이 유지되어야 한다.

auto p1 = make_unique<Xref<string>>(7,"Here");

"Here"는 rvalue이므로 forward(string&&)가 호출되고 rvalue를 따라 전달되어 Xref(int, string&&)가 "Here"를 포함하는 문자열에서 이동하도록 호출된다.

더 흥미로운(미묘한) 경우는 다음과 같다.

auto p2 = make_unique<Xref<string>>(9,x);

여기서 x는 lvalue이므로 forward(string&)가 호출되어 lvalue를 전달한다. forward()의 T는 string&로 추론되어 반환 값은 string&를 의미하는 string&&가 된다. 따라서 lvalue x에 대해 Xref(int, string&)가 호출되어 x가 복사된다.

불행히도 make_unique()는 표준 라이브러리의 일부가 아니지만 그럼에도 불구하고 널리 지원된다. 임의의 인수를 사용할 수 있는 make_unique()를 정의하는 것은 전달을 위한 가변 템플릿을 사용하여 비교적 쉽다.

Function Template Overloading

같은 이름을 가진 여러 함수 템플릿을 선언할 수 있으며 심지어 같은 이름을 가진 함수 템플릿과 일반 함수의 조합을 선언할 수 있다. 오버 로드된 함수가 호출되면 호출할 올바른 함수 도는 함수 템플릿을 찾기 위해 오버로드 해결이 필요하다.

template<typename T>
	T sqr t(T);
template<typename T>
	complex<T> sqrt(complex<T>);
double sqrt(double);

void f(complex<double> z)
{
	sqrt(2); // sqr t<int>(int)
	sqrt(2.0); // sqr t(double)
	sqrt(z); // sqr t<double>(complex<double>)
}

함수 템플릿이 함수 개념의 일반화인 것과 같은 방식으로, 함수 템플릿이 있는 경우 해결 규칙은 함수 오버로드 해결 규칙의 일반화다. 기본적으로 각 템플릿에 대해 함수 인수 집합에 가장 적합한 specialization를 찾는다. 그런 다음 일반적인 함수 오버로드 해결 규칙을 이러한 specialization 및 모든 일반 함수에 적용한다.

template<typename T>
T max(T,T);
const int s = 7;

void k()
{
	max(1,2); // max<int>(1,2)
	max('a','b'); // max<char>(’a’,’b’)
	max(2.7,4.9); // max<double>(2.7,4.9)
	max(s,7); // max<int>(int{s},7) (trivial conversion used)
	max('a',1); // error : ambiguous: max<char,char>() or max<int,int>()?
	max(2.7,4); // error : ambiguous: max<double,double>() or max<int,int>()?
}

마지막 두 호출의 문제는 템플릿 매개변수가 고유하게 결정될 때까지 프로모션과 표준 변환을 적용하지 않는다는 것이다. 컴파일러가 다른 것보다 하나의 해상도를 선호하도록 지시하는 규칙은 없다. 대부분의 경우 언어 규칙이 미묘한 결정을 프로그래머의 손에 맡기는 것이 좋을 것이다. 놀라운 모호성 오류에 대한 대안은 예상치 못한 해결의 놀라운 결과다. 과부하 해결에 대한 사람들의 '직관'은 극적으로 다르기 때문에 완벽하게 직관적인 과부하 해결 규칙 세트를 설계하는 것은 불가능하다.

Ambiguity Resolution

명시적 자격을 통해 두 가지 모호성을 해결할 수 있다.

void f()
{
	max<int>('a',1); // max<int>(int(’a’),1)
	max<double>(2.7,4); // max<double>(2.7,double(4))
}

또는 적절한 선언을 투가할 수 있다.

inline int max(int i, int j) { return max<int>(i,j); }
inline double max(int i, double d) { return max<double>(i,d); }
inline double max(double d, int i) { return max<double>(d,i); }
inline double max(double d1, double d2) { return max<double>(d1,d2); }

void g()
{
	max('a',1); // max(int(’a’),1)
	max(2.7,4); // max(2.7,4)
}

일반 함수의 경우 일반 오버로드 규칙이 적용되며 인라인을 사용하면 추가 오버헤드가 부과되지 않는다.

max()의 정의는 간단하므로 max()의 specialization을 호출하는 대신 직접 비교를 구현할 수 있다. 그러나 템플릿의 명시적 특수화를 사용하는 것은 이러한 해결 기능을 정의하는 쉬운 방법이며 여러 기능에서 거의 동일한 코드를 피함으로써 유지 관리에 도움이 될 수 있다.

Argument Substitution Failure

함수 템플릿에 대한 인수 집합에 대한 최상의 일치를 찾을 때 컴파일러는 전체 함수 템플릿 선언(반환 유형 포함)에 필요한 방식으로 인수를 사용할 수 있는지 여부를 고려한다.

template<typename Iter>
typename Iter::value_type mean(Iter first, Iter last);

void f(vector<int>& v, int∗ p, int n)
{
	auto x = mean(v.begin(),v.end()); // OK
	auto y = mean(p,p+n); // error
}

여기서 인수가 일치하고 vector<int>::iterator에 value_type이라는 멤버가 있기 때문에 x의 초기화가 성공한다. 인수가 일치하더라도 int*에는 value_type이라는 멤버가 없으므로 y 초기화가 실패하므로 다음과 같이 말할 수 없다.

int∗::value_type mean(int∗,int∗); // int* does not have a member called value_type

그러나 mean()에 대한 다른 정의가 있다면 어덯게 될까?

template<typename Iter>
typename Iter::value_type mean(Iter first, Iter last); // #1

template<typename T>
T mean(T∗,T∗); // #2

void f(vector<int>& v, int∗ p, int n)
{
	auto x = mean(v.begin(),v.end()); // OK: call #1
	auto y = mean(p,p+n); // OK: call #2
}

이것은 작동을 한다. 초기화는 모두 성공한다. 그런데 왜 mean(p, p+n)을 첫 번째 템플릿 정의와 일치시키려고 할 때 오류가 발생하지 않을까? 인수는 완벽하게 일치하지만 실제 템플릿 인수 (int*)를 대체하여 함수 선언을 얻는다.

int∗::value_type mean(int∗,int∗); // int* does not have a member called value_type

물론 쓰레기 값이다. 포인터에는 value_type멤버가 없다. 다행히도 이런 선언을 고려하는 것 자체가 오류는 아니다. 이러한 대체 실패는 오류가 아니라는 규칙이 있다. 단순히 템플릿이 무시되도록 한다. 즉, 템플릿은 오버로드 집합에 대한 specialization을 제공하지 않는다. 그러면 mean(p, p+n)이 호출된 선언 #2와 일치한다.

 

728x90
반응형

'Program Language > C & C++' 카테고리의 다른 글

[C++] Algorithms and Lifting  (0) 2022.02.18
[C++] Template Aliases  (0) 2022.02.18
[C++] Console 한 줄 Refresh  (0) 2022.02.16
[C++] Class Template  (0) 2022.02.10
[C++] Simple String 템플릿  (0) 2022.02.10