객체 지향 프로그래밍의 핵심 축은 다음과 같다.
- 명시적 인터페이스(explicit interface)
- 런타임 다형성(runtime polymorphism)
예를 들어 아래의 클래스와,

다음의 함수가 있다고 가정하면,

doProcessing() 안에 있는 w에 대해 말할 수 있는 부분은 다음과 같다.
- w는 Widget 타입으로 선언되었기 때문에, w는 Widget 인터페이스를 지원해야 한다.
- Widget의 맴버 함수 중 몇 개는 가상 함수이므로, 이 가상 함수에 대한 호출은 런타임 다형성에 의해 이루어진다.
하지만 템플릿과 일반화 프로그래밍의 세계에서는 뿌리부터 뭔가 다른 부분이 있다. 명시적 인터페이스 및 런타임 다형성은 그대로 존재하지만 중요도가 떨어진다. 이 세계에서는 암시적 인터페이스(implicit interface)와 컴파일 타임 다형성(complie-time polymorphism)이 더 중요하다. 어떻게 이렇게 되는지는 방금 예제를 템플릿으로 바꿔보면 알 수 있다.

이번에는 doProcessing() 템플릿 안의 w에 대해 다음과 같이 말할 수 있다.
- w가 지원해야 하는 인터페이스는 이 템플릿 안에서 w에 대해 실행되는 연산이 결정한다.
- w가 수반되는 함수 호출이 일어날 때, 이를테면 operator> 및 operator!= 가 호출될 때, 해당 호출을 성공시키기 위해서 템플릿의 인스턴스화가 일어난다. 이러한 인스턴스화가 일어나는 시점은 컴파일 도중이다. 인스턴스화를 진행하는 함수 템플릿에 어떤 템플릿 매개변수가 들어가느냐에 따라 호출되는 함수가 달라지기 때문에, 이것을 가리켜 컴파일 타임 다형성이라고 한다.
컴파일 다형성과 런타임 다형성의 차이를 잘 알고 넘어가자. 오버로드된 함수 중 지금 호출할 것을 골라내는 과정(컴파일 중에 일어남)과 가상 함수 호출의 동적 바인딩(런타임 중에 일어남)의 차이점과 흡사하다.
반면, 명시적 인터페이스와 암시적 인터페이스의 차이는 템플릿에서 처음 접하는 부분일 수 있다. 좀 더 자세히 알아보자.
명시적 인터페이스는 대게 함수 시그니처로 이루어진다. 시그니처는 함수의 이름, 매개변수의 타입, 반환 타입 등을 통들어 부르는 용어이다. 예를 들어, 앞에서 살펴본 예제 클래스인 Widget의 public 인터페이스를 보면,

생성자, 소멸자를 포함해서 size(), normalize(), swap() 그리고 이들의 매개변수 타입, 반환 타입 및 각 함수의 상수성(const 키워드) 여부로 이루어져 있는 것이다. 거기다 컴파일러가 자동으로 만들어 놓은 복사 생성자, 대입 연산자(항목 5 참고) 그리고 typedef 타입이 있을 경우 이것도 포함될 수 있다. (데이터 멤버의 경우 시그니처에 들어가지 않는다.)
반면, 암시적 인터페이스는 함수 시그니처에 기반하고 있지 않다. 암시적 인터페이스를 이루는 요소는 유효 표현식(expression) 이다. doProcessing 템플릿의 시작 부분에 있는 조건문을 다시 한번 보자.

이때 T(w의 타입)에서 제공될 암시적 인터페이스에는 다음과 같은 제약이 걸린다.
- 정수 계열의 값을 반환하고 이름이 size인 함수를 지원해야 한다.
- T 타입의 객체 둘을 비교하는 operator!= 함수를 지원해야 한다.(여기서 somNatsyWidget 객체의 타입은 T라고 가정)
실제로는 연산자 오버로딩의 가능성이 있기 때문에 T는 위 두 제약을 꼭 만족시킬 필요는 없다.
첫번째 제약부터 보면, T가 size()를 지원해야하는 것은 맞다. 물론 기본 클래스로부터 물려받았을 수 있지만, 이 멤버 함수는 수치 타입을 반환할 필요까지는 없다. 심지어 operator>의 정의에 필요한 타입도 반환할 필요가 없다. size()가 해야할 일은 그저 어떤 X 타입의 객체와 int(10이 int 타입이다.)가 함께 호출될 수 있는 operator>가 성립될 수 있도록, X 타입의 객체만 반환하면 임무 종료이다. 즉 size()는 어떤 타입의 객체를 리턴하고, 그 객체가 operator>의 매개변수로 들어가기만 하면 상관없다는 것이다.
한편, operator> 함수가 반드시 X 타입의 매개변수를 받아들일 이유도 없다. 왜냐하면 이 함수가 Y 타입의 매개변수를 받도록 정의되어 있고, X 타입에서 Y 타입으로 암시적인 변환이 가능하다면 상관없기 때문이다.
예를 들면, w.size()가 int(정수 타입) 대신 double(실수 타입)을 리턴해도 int -> double의 암시적 변환이 가능하기 때문에 상관없다는 뜻이다.
두번째 제약을 보면, operator!= 함수가 X 타입의 객체 하나와 Y 타입의 객체 하나를 받아드린다고 하면 이 부분은 별 걸림돌 없이 넘어갈 수 있다. T가 X로 반환될 수 있으며, someNastyWidget의 타입이 Y로 변환되는 것이 가능하기만 하면 operator!= 함수의 호출은 유효 호출이 된다.
(여담으로, 위의 이야기는 operator&&의 오버로드 가능성을 염두에 두지 않고 한 것이다.)
간단하게 말하면, 암시적 인터페이스는 유효 표현식의 집합으로 구성되어있을 뿐이라는 것이다. 표현식 자체만 뚫어지게 보면 복잡해 보이지만, 표현식에 걸리는 제약들은 일반적으로 매우 평이하다.
예를 들어, 다음 조건식에서

size, operator>, operator&& 혹은 operator!= 함수에 대한 제약을 일일이 집어내보라고 하면 좀 난감해지지만, 이 표현식에 대한 제약은 되레 집어내기가 쉽다. if 문의 조건식 부분은 boolean 표현식이여야 하기 때문에, 표현식에서 쓰이는 것들이 정확이 어떤 타입인지 상관없이, 그리고 "w.size() > 10 && w != someNastyWidget" 이 식이 뭔 값이 내놓던지 간에, 그냥 이 조건식의 결과값이 bool 타입과 호환되어야 한다.
이 제약이 doProcessing 템플릿이 타입 매개변수인 T에 대해 요구하는 암시적 인터페이스의 일부이다. 나머지도 마찬가지이다. 복사 생성자, normalize() 그리고 swap()에 대한 호출이 T 타입의 객체에 대해 말그대로 "유효"하기만 하면 되는
것이다.
템플릿 매개변수에 요구되는 암시적 인터페이스는 클래스의 객체에 요구되는 명시적 인터페이스 만큼이나 우리와 가까이 있다. 게다가 컴파일 도중에 점검된다는 점도 둘이 똑같다. 클래스에서 제공하는 명시적 인터페이스와 호환되지 않는 방법으로 그 클래스의 객체를 쓰면 에러가 나듯, 어떤 템플릿 안에서 어떤 객체를 쓰려고할 때 그 템플릿에서 요구하는 암시적 인터페이스를 그 객체가 지원하지 않으면 에러가 난다.(즉, 코드가 컴파일되지 않는다.)
- 클래스 및 템플릿은 모두 인터페이스와 다형성을 지원한다.
- 클래스의 경우, 인터페이스는 명시적이며 함수의 시그니처를 중심으로 구성되어 있다.
- 템플릿 매개변수의 경우, 인터페이스는 암시적이며 유효 표현식에 기반을 두어 구성된다. 다형셩은 컴파일 중에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타난다.
'Effective C++ > 7. 템플릿과 일반화 프로그래밍' 카테고리의 다른 글
| 항목 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (0) | 2021.05.23 |
|---|---|
| 항목 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아두자. (0) | 2021.05.19 |
| 항목 42: typename의 두 가지 의미를 제대로 파악하자 (0) | 2021.05.16 |