C++ 함수 템플릿의 특수화와 구체화

프로그래밍/C++ 2019.08.22 댓글 Plorence

특수화(specialization)

암시적 구체화, 명시적 구체화, 명시적 특수화를 모두 특수화라고 합니다.
이들의 공통점은 이들이 일반화 서술을 나타내는 함수 정의가 아니라, 모두 구체적인 데이터형을 사용하느 함수 정의를 나타낸다는 것이기 때문입니다.

함수 템플릿에서의 명시적 특수화(Explicit Specialization)

명시적 특수화(explicit specialization)라는 특수화된 함수 정의를, 필요한 코드와 함께 제공될 수 있습니다.
컴파일러가 함수 호출에 정확히 대응하는 특수화된 정의를 발견하게 되면 템플릿을 찾지 않고 그 정의를 사용합니다.
명시적 특수화를 사용하는 이유는 특정 형식(데이터형)에 대한 범위를 줄여 특별한 동작을 하기 위해서입니다.

만약 매개변수가 int형일경우 다른 코드(특별한 동작)를 실행하고 싶다면 명시적 특수화를 해야합니다.

 

함수의 종류는 아래와 같이 세 가지입니다.

  • 명시적 특수화된 템플릿
  • 템플릿
  • 템플릿이 아닌 것(일반 함수)

만약 세 가지 종류 모두 있다면 컴파일러는 어떤 함수를 선택할까요?

컴파일러의 함수 선택 우선 순위는 아래와 같습니다.

  1. 템플릿이 아닌 것(일반 함수)
  2. 명시적 특수화된 템플릿
  3. 오버로딩된 템플릿
  4. 템플릿

만약 템플릿 초안 이전 버전의 컴파일러를 사용하면, 템플릿의 우선순위가 일반 함수보다 높습니다.

4개 다 해당하는게 없다면 컴파일 에러가 발생하게 됩니다.

 

명시적 특수화 방법

명시적 특수화는 다음과 같은 조건이 필요합니다.

  • 함수 매개변수 개수가 같아야 한다.
  • 데이터형의 특수화는 일반형에만 해당된다. (구체화형 매개변수는 특수화 불가능)
  • 함수 템플릿 반환형과 동일해야 한다.
  • 반환형 앞에 template<>를 붙인다.

명시적 특수화 문법은 아래와 같습니다.

template<> ReturnType Function<DataType>(DataType t1);

만약 아래와 같은 함수 템플릿이 있다고 가정합시다.

template<typename T>
T Plus(T a, T b) {
       return a + b;
}

int형으로 명시적 특수화를 하면 아래와 같아집니다.

template<> int Plus<int>(int a, int b) {
       return a + b + 10;
}

이렇게 하면 되는데 간단히 말해서 <>안에는 특수화할 데이터 타입이 들어가고, 그 데이터 타입을 기준으로 작성하시면 됩니다.

<>를 사용한 명시적으로 타입을 지정하는 것은 빼도 됩니다.

매개변수의 데이터형이 타입을 알려주기 때문입니다.

명시적 특수화를 그림으로 표현한 예시

맞춰 끼우기만 하면 명시적 특수화가 됩니다.

 

또 다른 함수 템플릿이 있습니다.

template<typename T1,typename T2>
void Show(T1 a, T1 b,T2 c) {
       return std::cout << a << b << c;
}

위의 함수 템플릿은 템플릿 매개변수가 2개입니다.

1개랑 별 차이 없습니다.

template<> void Show<int, double>(int a, int b, double c) {
       std::cout << a << b << c;
}

마찬가지로 맞춰 끼우기만 하면 됩니다.

명시적 특수화를 그림으로 표현한 예시

템플릿 매개변수를 특정 타입으로 치환한다라고 이해하시면 쉽습니다.

 

오래된 특수화 형식

이때까지 사용하던 템플릿 방식이 컴파일에러가 난다면 오래된 특수화 형식을 써줘야 합니다.

#include <iostream>
template<typename T>
T Plus(T a,T b) {
    return a + b;
}
int Plus<int>(int a, int b) { //오래된 특수화 형식,앞에 template<>가 없다.
    return a + b+10;
}
int main(void) {
    std::cout << Plus(1, 1) << std::endl;
}

부분 특수화

함수 템플릿은 부분 특수화 불가능합니다.
그래서 오버로딩으로 해결해야 하는데 신경써줘야 할 점은 명시적 특수화된 함수들보다 위에 정의되어 있어야 합니다. (또는 선언)

#include <iostream>
template<typename T>
T Plus(T a, T b) {
	return a + b;
}
template<typename T>
T Plus(T a, char b) {
	return b + a;
}
template<> int Plus(int a, char b) {
	return a + b + 10;
}

int main(void) {
	std::cout << Plus(5.5, 123.4) << std::endl;
	std::cout << Plus(1, 'A') << std::endl;
}

이 경우에 명시적 특수화된 템플릿이 우선 순위가 더 높으므로 명시적 특수화된 템플릿 함수가 호출되지만 명시적 특수화된 함수 템플릿을 주석처리하면 오버로딩된 함수가 호출됩니다.

 

명시적 특수화할 때 참조를 조심하자

아래와 같은 코드가 있다고 가정합시다.

#include <iostream>
template<typename T>
T Plus(T a, T b) {
       return a + b;
}
template<> int & Plus(int & a, int & b) {
       int * ptr = new int(a + b+100);
       return *ptr;
}
int main(void) {
       std::cout << Plus(10,10) << std::endl;
       int N1 = 0, N2 = 10;
       int & A = Plus<int&>(N1, N2);
       std::cout << A << std::endl;
       int * ptr = &A;
       delete ptr;
}

템플릿 함수를 호출할 때 명시적으로 타입을 써줘서 문제가 없습니다.

Plus(N1,N2)

하지만 위와 같이 템플릿 함수를 호출했다면 문제가 발생합니다.

위와 같은 호출은 (int,int)도 되고 (int&,int&)도 됩니다.
즉 모호해지기 때문에 컴파일 에러가 발생합니다.
이와같은 문제는 N1, N2가 참조변수 일때도 발생합니다.
그래서 반드시 이와 같은 호출은 명시적 타입 지정을 해줘야 합니다.

 

"이때 반환된 값이 대입되는 변수의 타입이 참조인데 컴파일러가 알아서 처리하지 않을까요?"

라고 생각할 수도 있는데, 컴파일러가 함수를 선택할 때는 반환하는 값(또는 변수)이 어떤 타입에 대입되는지까지 따져서 선택되지 않습니다.

 

구체화(instantiation)

소스코드에 함수 템플릿을 넣는다고 해서 함수 정의가 저절로 생성되는 것은 아닙니다.
단지 함수 정의를 생성할 계획을 세우는 것에 불과합니다.(틀을 만든거였다.)
구체화는 컴파일러가 특적 데이터형에 맞는 함수 정의를 생성하기 위해 템플릿을 사용할때를 말합니다.
물론 이 작업을 하는 시기는 컴파일 타임에서 미리 만들어진 상태입니다.(함수가 호출할때 만들어 지는게 아니다.)

구체화에는 암시적 구체화(implicit instantiation)명시적 구체화(explicit instantiation)가 있습니다.

 

#include <iostream>
template<typename T>
T Plus(T a,T b) {
    return a + b;
}
int main(void) {
    std::cout << Plus(1, 1) << std::endl;
    /*
    Plus 함수 호출에서 인자의 데이터형이 int형이니
    컴파일러가 int형을 사용하는 Plus()함수를 구체화 하게 만든다.
    */
}

암시적 구체화(implicit instantiation)

int형 매개변수를 요구하는 Swap() 함수를 사용한다는것을 컴파일러에게 알림으로써 함수 정의를 할 필요가 있다 것을 암시적으로 인식하고 있습니다.
즉 호출할때 명시적으로 타입을 지정해주는것이 아니고 컴파일러가 매개변수를 분석해 알아서 구체화 하는것이 암시적 구체화 입니다.

 

명시적 구체화(explicit instantiation)

암시적 구체화는 컴파일러가 매개변수의 타입을 분석해 알아서 구체화를 했지만
명시적 구체화는 그와 다르게 매개변수의 타입을 명시적으로 지정해주고,컴파일러가 그에 맞춰서 구체화를 합니다.

template int Plus<int>(int a,int b);

이 선언은 함수 템플릿을 사용하여 int형에 맞는 함수 정의를 생성하라는 의미입니다.
이렇게 되면 컴파일러는 이 함수를 호출하든 안하든 구체화를 합니다.
구체화를 하는것이지 호출하는게 아닙니다.

 

명시적 타입 지정(명시적 구체화)

호출 과정에서 매개변수에 대한 타입을 명시적으로 전달이 가능한데 이때 아래와 같이 호출하면 됩니다.
꺽쇠(<>)를 사용하고 그 안에 타입을 써주면 됩니다.

#include <iostream>
template<typename T>
T Plus(T a,T b) {
    return a + b;
}
int main(void) {
    std::cout << Plus<double>(123.9,1) << std::endl; //명시적 구체화
}

템플릿은 두 개의 매개변수의 데이터형이 동일하다고 판단하기에 전달해줄 인자의 데이터형이 서로 다르면 명시적 구체화를 해야합니다.
대신 명시적 구체화는 템플릿의 파라미터가 참조변수일때 타입이 다르면 참조를 못하므로 에러가 발생하게 됩니다.

이것도 왜 명시적 구체화냐면, 결국엔 특정한 데이터형으로 구체화하라고 써줬기 때문입니다.

 

명시적 구체화에 대한 확인

함수 템플릿에서의 검사는 한번이라도 호출을 하여야만이 문제가 있는지 없는지 검사를 합니다.
즉 전역 상수의 값을 함수 템플릿에서 바꾼다고 해도 호출하지 않으면 컴파일 에러는 발생하지 않습니다.
(쓰지도 않는 함수를 검사해야 할 이유가 있을까요?)

#include <iostream>
const int a1 = 0;
template<typename T>
T display(T a, T b) {
        a1 = 1;
        return a + b+10;
}
//template int display<int>(int a, int b);
int display(int a, int b) {
        return a + b;
}
int main(void) {
        
}

이 점을 이용해 a1 =1은 심각한 문제지만 컴파일 에러가 발생하지 않습니다.

하지만 명시적 구체화를 하면 이 함수 템플릿이 구체화되어 사용될것이기 때문에 컴파일 에러가 발생하는지 안하는지 검사합니다.

#include <iostream>
const int a1 = 0;
template<typename T>
T display(T a, T b) {
        a1 = 1;
        return a + b+10;
}
template int display<int>(int a, int b);
int display(int a, int b) {
        return a + b;
}
int main(void) {
        
}

주석을 제거해 컴파일을 해보면 컴파일 에러가 발생한다는것을 알 수 있습니다.
즉 구체화가 되었다는것을 의미합니다.

댓글