41: 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터
객체 지향 프로그래밍의 세계를 회전시키는 축은 명시적 인터페이스(explicit interface)와 런타임 다형성(runtime polymorphism)이다.
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
};
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
위의 doProcessing 함수 안에 있는 w에 대해 말할 수 있는 부분은 다음과 같다.
- w는 Widget 타입으로 선언되었기 때문에, w는 Widget 인터페이스를 지원해야 한다. 이 인터페이스의 소스 코드에서 찾으면 이것이 어떤 형태인지를 확인할 수 있으므로, 이런 인터페이스를 가리켜 명시적 인터페이스라고 한다. 다시 말해, 소스코드에 명시적으로 드러나는 인터페이스이다.
- Widget의 멤버 함수 중 몇 개는 가상 함수이므로, 이 가상 함수에 대한 호출은 런타임 다형성에 의해 이루어진다. 특정한 함수에 대한 실제 호출은 w의 동적 타입을 기반으로 프로그램 실행 중, 즉 런타임에 결정된다.
템플릿에서는 암시적 인터페이스(implicit interface)와 컴파일 타임 다형성(compile-time-polymorphism)이다.
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget)
{
T temp(w);
temp.normalize();
temp.swap(w);
}
}
이번에는 템플릿 안의 w에 대해 어떻게 말할 수 있을까?
- w가 지원해야 하는 인터페이스는 이 템플릿 안에서 w에 대해 실행되는 연산이 결정한다. size, normalize, swap 멤버 함수를 지원해야 하는 쪽은 w의 타입, 즉 T이다. 정말 중요한 점은, 이 템플릿이 제대로 컴파일되려면 몇 개의 표현식이 유효해야 하는데 이 표현식들은 바로 T가 지원해야 하는 암시적 인터페이스라는 점이다.
- w가 수반되는 함수 호출이 일어날 때, (예를 들어 operator !=) 해당 호출을 성공시키기 위해 템플릿의 인스턴스화가 일어난다. 이러한 인스턴스화가 일어나는 시점은 컴파일 도중이다. 인스턴스화를 진행하는 함수 템플릿에 어떤 템플릿 매개변수가 들어가느냐에 따라 호출되는 함수가 달라지기 때문에, 이것을 가리켜 컴파일 타임 다형성이라고 한다.
명시적 인터페이스는 대개 함수 시그너처로 이루어진다. 함수의 이름, 매개변수 타입, 반환 타입 등을 통틀어 부르는 용어다. 데이터 멤버의 경우에는 시그너처에 들어가지 않는다.
반면, 암시적 인터페이스는 사뭇 다르다. 함수 시그너처에 기반하고 있지 않다는 것이 가장 큰 차이점이다. 암시적 인터페이스를 이루는 요소는 유효 표현식(expression)이다.
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
위의 조건문에서 다음과 같은 제약이 걸린다.
- 정수 계열의 값을 반환하고 이름이 size인 함수를 지원해야 한다.
- T 타입의 객체 둘을 비교하는 operator!= 함수를 지원해야 한다.
실제로는 연산자 오버로딩의 가능성이 있기 때문에 T는 위의 두 가지 제약 중 어떤 것도 만족시킬 필요가 없다. 우선 첫 번째 제약을 보면 T가 size 멤버 함수를 지원해야 하는 것은 맞다. 그렇다고 해도 이 멤버 함수는 수치 타입을 반환할 필요까지는 없다. 심지어 operator>의 정의에 필요한 타입도 반환할 필요가 없다. size 멤버 함수는 그저 어떤 X 타입의 객체와 int가 함께 호출될 수 있는 oeprator>가 성립될 수 있도록, X 타입의 객체만 반환하면 임무 종료인 것이다. 한편, operator> 함수는 반드시 X 타입의 매개변수를 받아들일 이유가 없다. 이 함수는 Y타입의 매개변수를 받도록 정의되어 있고 X 타입에서 Y 타입으로 암시적인 변환이 가능하다면 OK!이기 때문이다.
T가 operator!= 함수를 지원해야 한다는 두 번째 제약도 필수 요구사항이 되지 않는다. operator!= 함수가 X 타입의 객체 하나와 Y 타입의 객체 하나를 받아들인다고 하면 이 부분은 별 걸림돌 없이 넘어갈 수 있기 때문이다.
암시적 인터페이스는 그저 유효 표현식의 집합으로 구성되어 있을 뿐이다.
if (w.size() > 10 && w != someNastyWidget)
이 표현식에 대한 제약을 살펴보자. if 문의 조건식 부분은 boolean 표현식이어야 하기 때문에, 이 조건식 부분의 결과 값은 bool과 호환되어야 한다. 이 제약이 바로 doProcessing 템플릿이 타입 매개변수인 T에 대해 요구하는 암시적 인터페이스의 일부이다. 나머지는 복사 생성자, normalize 그리고 swap 함수에 대한 호출이 T 타입의 객체에 대해 유효해야 한다는 것이다.
어떤 템플릿 안에서 어떤 객체를 쓰려고 할 때 그 템플릿에서 요구하는 암시적 인터페이스를 그 객체가 지원하지 않으면 사용이 불가능하다.
이것만은 잊지 말자!
- 클래스 및 템플릿은 모두 인터페이스와 다형성을 지원한다.
- 클래스의 경우, 인터페이스는 명시적이며 함수의 시그너처를 중심으로 구성되어 있다. 다형성은 프로그램 실행 중에 가상 함수를 통해 나타난다.
- 템플릿 매개변수의 경우, 인터페이스는 암시적이며 유효 표현식에 기반을 두어 구성된다. 다형성은 컴파일 중에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타난다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 (0) | 2023.11.13 |
---|---|
[Effective C++] 42: typename의 두 가지 의미를 제대로 파악하자 (1) | 2023.11.11 |
[Effective C++] 40: 다중 상속은 심사숙고해서 사용하자 (2) | 2023.11.09 |
[Effective C++] 39: private 상속은 심사숙고해서 구사하자 (0) | 2023.11.07 |
[Effective C++] 38: "has-a(...는...를 가짐)" 혹은 "is-implemented-in-terms-of(...는...를 써서 구현됨)"를 모형화할 때는 객체 합성을 사용하자 (1) | 2023.11.06 |