이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 295p ~ 306p
이펙티브 C++(295p ~ 306p)
요약
7. 템플릿과 일반화 프로그래밍
- C++ 템플릿은 사용자가 타입에 관계없는 컨테이너를 만들어 사용할 때 타입 안전성을 부여할 수 있도록 만들기 위해 도입됐다.
- 일반화 프로그래밍은 템플릿의 한 응용 분야로 파생되었다.
- 이를 기반으로 템플릿 메타프로그래밍이라는 새로운 영역이 탄생하였다.
- 이는 컴파일러 내부에서 실행되고 컴파일 과정이 끝날 때 실행을 멈추는 또 하나의 프로그램을 만드는 것이다.
- 템플릿의 활용 영역은 팔색조처럼 다양하지만, 어떤 영역을 막론하고 템플릿 기반 프로그래밍이란 것의 밑바닥에는 핵심 아이디어가 존재한다.
항목 41: 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일타임 다형성부터
- 객체 지향 프로그래밍의 세계를 회전시키는 축은 명시적 인터페이스와 런타임 다형성이다.
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
vodi 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이 선언된 헤더 파일 등)에서 찾으면 어떤 형태인지 확인할 수 있으므로, 이런 인터페이스를 가리켜 명시적 인터페이스라고 한다. 다시 말해, 소스 코드에 명시적으로 드러나는 인터페이스를 일컫는다.
- Widget의 멤버 함수 중 몇 개는 가상 함수이므로, 이 가상 함수에 대한 호출은 런타임 다형성에 의해 이루어진다. 다시 말해, 특정한 함수에 대한 실제 호출은 w의 동적 타입을 기반으로 프로그램 실행 중, 즉 런타임에 결정된다.
- 템플릿과 일반화 프로그래밍의 세계에도 명시적 인터페이스 및 런타임 다형성은 그대로 존재하지만, 더 중요한 것은 암시적 인터페이스와 컴파일 타임 다형성이다.
doProcessing
함수를 템플릿으로 바꾸면 다음과 같다.
template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w != someNastyWidget)
{
T temp(w);
temp.normalize();
temp.swap(w)
}
}
- 템플릿으로 바뀐
doProcessing
함수에 있는 w에 대해 다음과 같이 말할 수 있다.- w가 지원해야 하는 인터페이스는 이 템플릿 안에서 w에 대해 실행되는 연산이 결정한다.
- size, normalize, swap 멤버 함수를 지원해야 하는쪽은 w의 타입 T이다.
- 그 외에도 temp를 만들기 위한 복사 생성자, 부등 비교 연산자가 필요하다.
- 중요한 부분은 이 템플릿이 제대로 컴파일되려면 몇 개의 표현식이 유효(valid)해야 하는데, 이 표현식들은 바로 T가 지원해야 하는 암시적 인터페이스이다.
- w가 수반되는 함수 호출이 일어날 때, 이를테면
operator<
및operator!=
함수가 호출될 때는 해당 함수 호출을 성공시키기 위해 템플릿의 인스턴스화가 일어난다. 이러한 인스턴스는 컴파일 도중에 일어난다. 함수 템플릿에 어떤 템플릿 매개변수가 들어가느냐에 따라 호출되는 함수가 달라지기 때문에, 이것을 가리켜 컴파일 타임 다형성이라 한다.
- w가 지원해야 하는 인터페이스는 이 템플릿 안에서 w에 대해 실행되는 연산이 결정한다.
- 런타임 다형성과 컴파일 타임 다형성의 차이는 다음 동작과 유사하다.
- 가상 함수 호출의 동적 바인딩과 런타임 다형성의 동작 방식이 유사
- 오버로드된 함수 중 지금 호출할 것을 골라내는 과정과 컴파일 타임 다형성의 동작 방식이 유사
명시적 인터페이스
- 대개 함수 시그니처로 이루어진다. 시그니처는 함수의 이름, 매개변수 타입, 반환 타입 등을 통틀어 부르는 용어이다.
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
vodi swap(Widget& other);
}
- Widget의 public 인터페이스는 다음과 같다.
- 생성자
- 소멸자
- size()
- normalize()
- swap()
- 매개변수 타입, 반환 타입, 각 함수의 상수성
- 컴파일러가 자동으로 만들어 놓은 복사 생성자 및 복사 대입 연산자
- typedef가 존재하면 이것도 포함됨
암시적 인터페이스
- 암시적 인터페이스는 함수 시그니처에 기반하고 있지 않다.
- 암시적 인터페이스를 이루는 요소는 유효 표현식이다.
template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w != someNastyWidget)
{
T temp(w);
temp.normalize();
temp.swap(w)
}
}
- 템플릿 함수의 시작 부분에 있는 조건문을 보면 w의 타입인 T에서 제공되어야 할 암시적 인터페이스에는 다음과 같은 제약이 걸린다.
- 정수 계열의 값을 반환하고 이름이 size인 함수를 지원해야 한다.
- T 타입의 객체 둘을 비교하는
operator!=
함수를 지원해야 한다.
- 그런데 실제로는 연산자 오버로딩의 가능성이 있기 때문에 T는 위의 두 가지 제약 중 어떤 것도 만족시킬 필요가 없다.
- 첫 번째 제약에서 T가 size 멤버 함수를 지원해야 하는 것은 맞다. 하지만 이 멤버 함수는 수치 타입을 반환할 필요까지는 없다.
- 심지어
operator>
정의에 필요한 타입 반환도 필요없다. - size 멤버 함수의 역할은 어떤 X 타입 객체와 int(비교 대상인 10이 int)가 함께 호출될 수 있는
operator>
가 성립될 수 있도록 X 타입 객체만 반환하면 된다. operator>
함수는 반드시 X 타입의 매개변수를 받아들일 이유가 없다. 이 함수가 Y 타입의 매개변수를 받도록 정의되어 있으면, X 타입에서 Y 타입으로 암시적으로 변환이 가능하기만 하면 된다.
- 심지어
- 첫 번째 제약과 비슷한 이유로
operator!=
함수를 지원해야 하는 두 번째 제약도 필수 요구사항이라 할 수 없다.operator!=
함수가 X 타입의 객체 하나와 Y 타입의 객체 하나를 받아들인다고 하면 T가 X로, someNastyWidget이 Y 타입으로 변환되는 것이 가능하면 유효 호출로 간주될 것이다.
- 암시적 인터페이스는 그저 유효 표현식의 집합으로만 구성되어 있다.
- 이 표현식에 걸리는 제약은 일반적으로 지극히 평이하다.
- if 문의 조건식 부분은 boolean 표현식이어야 하기 때문에, 내부에 있는 코드가 정확히 어떤 값을 내놓든 간에, 이 조건식 부분의 결과 값은 bool과 호환되어야 한다.
- 복사 생성자, normalize, swap 함수에 대한 호출이 T 타입의 객체에 대해 유효해야 한다.
- 이것이
doProcessing
템플릿이 타입 매개변수인 T에 대해 요구하는 암시적 인터페이스의 일부이다.
- 클래스에서 제공하는 명시적 인터페이스와 호환되지 않는 방법으로 그 클래스의 객체를 쓸 수 없듯이, 어떤 템플릿 안에서 어떤 객체를 쓰려고 할 때 그 템플릿에서 요구하는 암시적 인터페이스를 그 객체가 지원하지 않으면 사용이 불가능하다.(컴파일되지 않음)
항목 42: typename의 두 가지 의미를 제대로 파악하자
- 아래의 두 템플릿 선언문에 쓰인 class와 typename의 차이는 무엇일까?
template<class T> class Widget;
template<typename T> class Widget;
- 정답은 ‘차이가 없다’이다.
- 템플릿의 타입 매개변수를 선언할 때는 class와 typename이 완전히 똑같다.
- 그러나 항상 class와 typename이 동등한 것은 아니다. typename을 쓰지 않으면 안 되는 경우가 분명히 있다.
- 이때가 언제인지를 제대로 알아보려면, 일단 템플릿 안에서 참조할 수 있는 이름의 종류가 두 가지라는 것부터 알아야 한다.
// 컨테이너에 들어 있는 두 번째 원소를 출력하는 함수 템플릿
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2) {
// 첫 번째 원소에 대한 반복자를 얻음
C::const_iterator iter(container.begin()); // note
++iter;
int value = *iter; // note
std::cout << value;
}
}
- 위의 함수 템플릿은 다음과 같이 설명할 수 있다.
- 이 템플릿은 STL과 호환되는 컨테이너를 매개변수로 받아들이도록 만들어졌다.
- 이 컨테이너에 담기는 객체는 int에 대입할 수 있다.
- 이 템플릿이 하는 일은 컨테이너에 담긴 원소들 중 두 번째 것의 값을 출력하는 것이다.
- 컴파일도 안 된다.
iter
의 타입은C::const_iterator
인데, 템플릿 매개변수인 C에 따라 달라지는 타입이다.- 템플릿 내의 이름 중에 이렇게 템플릿 매개변수에 종속된 것을 의존 이름(dependent name)이라고 한다.
- 의존 이름이 어떤 클래스 안에 중첩되어 있는 경우가 있는데, 책의 필자는 이를 중첩 의존 이름이라고 부른다.
iter
는 중첩 의존 이름이다.
value
의 타입은int
이다.int
는 템플릿 매개변수가 어떻든 상관없는 타입 이름이다.- 이러한 이름을 비의존 이름(non-dependent name)이라고 한다.
중첩 의존 이름의 문제점
- 코드 안에 중첩 의존 이름이 있으면 컴파일러가 구문분석을 할 때 애로사항이 꽃핀다.
template<typename C>
void print2nd(const C& container)
{
// 첫 번째 원소에 대한 반복자를 얻음
C::const_iterator *x;
}
- 언뜻 보면,
C::const_iterator
에 대한 포인터인 지역 변수 x를 선언하고 있는 것 같다. - 하지만 이는
C::const_iterator
가 타입이라는 사실을 인간인 우리만 알고 있을 때만 그렇다. - 즉,
C::const_iterator
가 타입이 아니라 C의 정적 데이터 멤버일 수도 있다.- 이 상황에서 x가 다른 전역 변수의 이름이라면
C::const_iterator
라는 정적 데이터 멤버와 x를 곱셈한 것과 같아진다.
- 이 상황에서 x가 다른 전역 변수의 이름이라면
- C의 정체가 무엇인지 다른 곳에서 알려주지 않으면,
C::const_iterator
가 진짜 타입인지 아닌지를 알아낼 방법은 없다.print2nd
함수 템플릿이 구문분석이게 의해 처리되는 순간에도 C의 정체는 저절로 밝혀지지 않는다. - 따라서 C++는 이러한 모호성을 해결하기 위해, 구문 분석기는 템플릿 안에서 중첩 의존 이름을 만나면 프로그래머가 정의한 타입이라고 알려 주지 않는 한 그 이름이 타입이 아니라고 가정하게 되어 있다.
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2)
{
// 이 이름은 타입이 아닌 것으로 가정한다.
C::const_iterator iter(container.begin());
}
}
iter
의 선언이 선언으로서 의미가 있으려면C::const_iterator
가 반드시 타입이어야 한다. 이를 위해 C++에게C::const_iterator
가 타입이라고 명시적으로 말해주어야 한다.
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
}
}
- 어떤 경우에도 템플릿 안에서 중첩 의존 이름을 참조할 경우에는, 그 이름 앞에
typename
키워드를 붙여주면 된다. typename
키워드는 중첩 의존 이름만 식별하는 데 사용해야 한다. 그 외 이름은typename
을 가져선 안 된다.
template<typename C>
void f(const C& container, // 여기에는 typename 사용하면 안 됨
typename C::iterator iter); // 중첩 의존 이름에는 반드시 typename 키워드 사용
- 중첩 의존 이름 앞에
typename
을 사용해서는 안 되는 예외가 하나 있다. - 이는 중첩 의존 타입 이름이 기본 클래스의 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있을 경우이다.
template<typename T>
class Derived : public Base<T>::Nested // 상속되는 기본 클래스 리스트, typename 쓰면 안 됨
{
public:
explicit Derived(int x) : Base<T>::Nested(x) // 멤버 초기화 리스트에 있는 기본 클래스 식별자, typename 쓰면 안 됨
{
typename Base<T>::Nested temp; // 중첩 의존 이름이며 기본 클래스 리스트에도 없고, 멤버 초기화 리스트도 아님, typename 필요
}
};
실전 예제
- 반복자를 매개변수로 받는 어떤 함수 템플릿을 만드는데, 매개변수로 넘어온 반복자가 가리키는 객체의 사본을 temp라는 지역 변수에 담고 싶다.
template<typename IterT>
void workWithIterator(IterT& iter)
{
typename std::iterator_traits<IterT>::value_type temp(*iter);
}
IterT
타입의 객체로 가리키는 대상의 타입과 같은 타입의 지역 변수 temp를 선언하고, iter가 가리키는 객체로 temp를 초기화하는 문장이다.IterT
가 vector::iterator라면 temp의 타입은 int가 된다.IterT
가 vector::iterator라면 temp의 타입은 string이 된다.
std::iterator_traits<IterT>::value_type
은 중첩 의존 이름이기 때문에 앞에 typename 키워드를 써야 한다.- 이것이 너무 길어서 typedef 이름을 만들려고 한다.
- 특성정보 클래스에 속한
value_type
등의 멤버 이름에 대해 typedef 이름을 만들 때는 그 멤버 이름과 똑같이 짓는 것이 관례이다.
template<typename IterT>
void workWithIterator(IterT& iter)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter):
}
typedef typename
은 중첩 의존 이름을 참조하기 위해 지켜야 할 규칙이며, 논리적으로도 문제가 없다.
메모
항목 41: 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일타임 다형성부터
- 클래스 및 템플릿은 모두 인터페이스와 다형성을 지원한다.
- 클래스의 경우, 인터페이스는 명시적이며 함수의 시그니처를 중심으로 구성되어 있다. 다형성은 프로그램 실행 중에 가상 함수를 통해 나타난다.
- 템플릿 매개변수의 경우, 인터페이스는 암시적이며 유효 표현식에 기반을 두어 구성된다. 다형성은 컴파일 중에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타난다.
항목 42: typename의 두 가지 의미를 제대로 파악하자
- 템플릿 매개변수를 선언할 때, class 및 typename은 서로 바꾸어 써도 무방하다.
- 중첩 의존 타입 이름을 식별하는 용도에는 반드시 typename을 사용해야 한다. 단, 중첩 의존 이름이 기본 클래스 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있는 경우에는 예외이다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(320p ~ 331p) (1) | 2024.02.08 |
---|---|
이펙티브 C++(306p ~ 312p) (1) | 2024.02.06 |
이펙티브 C++(279p ~ 290p) (1) | 2024.02.03 |
이펙티브 C++(270p ~ 279p) (0) | 2024.02.02 |
이펙티브 C++(261p ~ 270p) (0) | 2024.02.01 |