프로그래밍 언어/Effective C++

[Effective C++]항목 45~48: 템플릿 2

우향우@ 2023. 6. 10. 03:36

항목 45 : 멤버 함수 템플릿

클래스 템플릿 안에는 별개의 템플릿을 가지는 멤버 함수를 만들 수  있다. 

template<typename T> class c
{
    public:
        tempalte<typename U>
        void fff(U a);

}

이렇게 하면 특정 클래스 템플릿 인스턴스에서 특정 타입의 fff를 호출할때마다 해당 멤버함수가 생성되며 호출된다.

 

이러한 멤버 함수 템플릿의 대표적인 사용예시는 일반화 복사 생성자이다.

스마트 포인터 템플릿을 만든다고 해보자. 만약 평범하게 자신에 대한 복사 생성자 만든다면 자식 타입에 대한 스마트 포인터를 부모 타입에 대한 스마트 포인터에 복사 생성할 수 가 없다. 이를 구현하기 위해선 멤버 함수 템플릿으로 smartPoint<U>를 매개변수로 받는 복사 생성자를 만들면 된다. 그후 매개변수 스마트 포인터의 원시 포인터를 자신의 원시 포인터에 복사하여 구현하면 된다. 이렇게 하면 자식 타입의 스마트 포인터로 부모 타입의 스마트 포인터에 복사 생성해도 정상 작동한다. 만약 원시 포인터끼리 호환되지 않는 타입으로 생성하려 하면 컴파일 타임에 에러를 발생시켜 잡아준다. 이러한 생성자를 일반화 복사 생성자라고 한다. 여기에 explicit도 달지 않는다면 매개변수들에 넘겨줄때의 호환성까지도 구현할 수 있게 된다.

마지막으로 일반화 복사 생성자를 만들때 주의해야하는 점은 일반화 복상 생성자를 템플릿으로 만들어도 기본 복사 생성자는 그대로 생성되므로 구현을 따로 주고 싶다면 기본 복사 생성자는 따로 선언하고 만들어 주어야한다.

 

항목 46 : 비멤버 함수 템플릿

클래스 템플릿 안에는 friend 키워드를 이용하여 비멤버 함수 템플릿을 구현할 수 있다. 비멤버 함수 템플릿은 해당 클래스 템플릿 인스턴스가 생성될 때 넘겨준 템플릿 매개변수로 같이 생성되는 별도의 비멤버 함수라는 점에서 의미가 있다. 비멤버 함수 템플릿은 항목 45처럼 생성하되 friend 키워드를 붙여주면 된다.

이러한 비멤버 함수 템플릿의 대표적인 사용 예시는 양 항목의 암시적 변환을 위한 연산자 정의이다.

만약 분수를 나타내는 클래스 템플릿을 하나 만들었다고 하자. 이 클래스 템플릿은 타입 매개변수를 하나 받고 이 타입으로 구성된 분자와 분모를 가지는 클래스이다. 그리고 이에 대한 외부 함수 템플릿으로 곱하기 operator 오버라이드 함수를 만든다고 하자. 만약 이 함수 템플릿이 T타입 하나를 받고 Rational<T> lhs, rhs를 받아 처리한다고 하자. 이때 rational<int> * int등은 처리되지않는다. 만약 rational 클래스 템플릿에서 생성자를 통해 int에서 rational<int>로의 암시적 변환을 제공해준다 해도 이는 처리되지 않는다. 컴파일러에서는 템플릿 인스턴스 생성전 구문 유효검사부터 하는데 여기서 암시적 변환가지 고려하여 적절한 템플릿이 있는지 찾지 않기 때문이다. 즉 rational<T>,int 를 받는 템플릿 함수가 없는 한 에러가 발생한다. 이를 구현하기 위해선 클래스 템플릿 안에 비멤버 함수 템플릿으로 곱하기 오버라이드를 구현하면된다. 여기서 양 매개변수는 마찬가지로 rational<T>가 된다. 이렇게 하면 rational<int> 객체가 생성된 시점에 클래스 템플릿 인스턴스가 생성되고 rational<int> 두개를 받는 비멤버 연산자 오버라이드 함수가 정확히 생성이 되기 때문이다. 이는 rational<int>두개를 받는 일반 함수가 존재하는 것이므로 일반적인 암시적 변환까지 허용되어 rational<int> * int등이 정상적으로 처리된다.

//일반 외부 템플릿 함수로 만들기 (암시적 형변환 미지원)
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{ ... }


//Freind로 만들기 (암시적 형변환 지원)
template<typename T>
class Rational
{
public:
    friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
    {
		...
    }
    ...
};

 

항목 47 : 특성 정보 클래스

특성 정보 클래스는 특정 컨셉의 클래스 집합의 각 클래스들에 대해 집합과 관련된 정보를 알려주는 클래스 템플릿을 의미한다. 예를 들어 stl의 반복자들에 대한 특성 정보 클래스로 interator_trait이 존재한다. 이 클래스 템플릿으로 부터 특정 반복자의 접근 수준등을 알아낼 수 있다. typename std::iterator_trait<반복자 타입>::iterator_category를 이용하면 해당 반복자의 접근 수준를 나타내는 구조체의 타입이 반환된다. 접근 수준 구조체 타입에 대해 오버로드를 한 이름의 함수를 해당 구조체 타입으로 생성한 매개변수를 넘겨주며 호출하면 반복자의 접근 수준에 따른 로직을 처리할 수 있다. 이는 if문으로 타입을 구분하여 처리하는 다른 방법(물론 컴파일 에러때문에 정상적으로 구현되지는 않는다)에 비해 코드 비대화나 런타임이 아닌 컴파일 시점에 호출될 코드가 정해진다는 점에서 여러 성능 이점이 있다.

iterator_trait은 typedef와 특수화를 통해 구현되었다. iterator_trait에 타입 매개변수로 들어오는 반복자는 둘중 하나이다. 클래스로 만들어진 반복자, 포인터이다. 이때 클래스로 만들어진 반복자는 구현될때 typedef 자신의 접근 수준 구조체 iterator_category를 적을 것을 요구받는다. iterator_trait은 타입 매개변수로 받은 반복자 타입의 해당 iterator_category를 다시 자신의 typedef에서 iterator_catogory로 명시한다. 이렇게 하면 클래스 반복자에 대한 iterator_category는 쉽게 구현된다. 포인터에 대해서는 포인터 타입 특수화를 통해 typedef random_access_iterator_tag iterator_category만 제공해주면 된다. 포인터는 랜던 접근 수준을 가지기 때문이다.

//반복자 클래스내에 다음과 같이 정의
typedef iterator_catrgory random_access_iterator_tag

 

 

항목 48 : TMP (템플릿 메타 프로그래밍)

TMP는 템플릿을 활용하여 최대한 많은 로직이 컴파일 타임에 처리될 수 있도록 구현하는 프로그래밍 기법을 말한다.

TMP에는 기존 기법과는 다른 독특한 형태가 많이 나타난다. 대표적인 것이 항목 47에서 봤던 오버로드를 활용하여 반복자 별로 로직 분기를 이르키는 부분이다. 기존의 기법에서는 if문으로 사용되던 것이 오버로드로 구현된 것이다. 이렇듯 TMP는 런타임의 처리(if문 비교)를 컴파일타임의 처리(오버로드된 함수 중 하나를 매핑)로 옮기는 것을 목표로 한다.

대표적인 TMP 기법중 하나는 loop를 대체한 재귀식 템플릿 인스턴스화가 있다. 팩토리얼을 이로 구현해보자. TMP에서 펙토리얼은 다음과 같이 구현한다.

template<unsigned n>
struct Factorial
{
    enum { value = n * Factorial<n-1>::value}
}

template<>
struct Factorial<0>
{
    enum { value = 1}
}

여기서 enum 속 value는 나열자 둔갑술을 이용해 만든 템플릿 상수이다. 그냥 각 템플릿 인스턴스에서 상수 처럼 작동한다.

이렇게 펙토리얼을 구현하면 Factorial<n>::value가 사용될시 Factorial<n>이 만들어 질것이고 이를 위해 n-1,n-2들이 계속 만들어지다가 0에서 부터 value값이 계산되며 모두 생성이 완료되게 되며 value값도 가지게 된다. 한번 만들어진 n에 대해서는 더 생성할 필요없이 바로 사용하면 되고 추가적인  n에 대해서도 만들어진 부분 위쪽에 대해서만 추가적으로 생성된다. 그리고 이는 컴파일 타임에 템플릿 인스턴스를 만드며 모두 계산되고 완성된다. 런타임에는 이렇게 만들어진 value값을 가지고 올 뿐이다. 모든 펙토리얼 계산이 컴파일 타임으로 옮겨간 것이다.

 

 

 

 

 

 

참고서적

Effective C++