본문 바로가기
프로그래밍/Effective C++

Chapter 7 템플릿과 일반화 프로그래밍

by Ohdumak 2018. 7. 11.

2018/06/07 - [프로그래밍/Effective C++] - Chapter 6 상속, 그리고 객체 지향 설계


C++ 템플릿을 만들려고 했던 동기는 단순하다. 사용자가 타입에 관계없는 컨테이너(container)를 만들어 사용할 때 타입 안전성을 부여할 수 있도록 하는 것이다. 컨테이너는 그 자체만으로도 훌륭했지만, 템플릿의 한 응용 분야로 파생된 일반화 프로그래밍(generic programming, 조작할 객체의 타입과 상관없이 코드를 작성하도록 하는 개념)도 훌륭하다. C++ 템플릿을 사용하면 계산 가능한(computable) 어떤 값도 계산할 수 있다.


템플릿 프로그래밍에 있어 탄탄한 기초를 기르는 데 도움이 될것이다.



항목 41: 템플릿 프로그램밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성 부터


객체 지향 프로그래밍의 세계를 회전시키는 축은 명시적 인터페이스(explicit interface)와 런타임 다형성(runtime polymorphism)이다.


탬플릿과 일반화 프로그래밍의 세계에는 뿌리부터 뭔가 다른 부분이 있습니다. 명시적 인터페이스 및 런타임 다형성은 그대로 존재하긴 하지만 중요도는 사뭇 떨어진다. 


런타임 다형성과 컴파일 타임 다형성의 차이를 헷갈리면 안된다. 오버로드된 함수 중 지금 호출할 것을 골라내는 과정(컴파일 중에 일어난다)과 가상 함수 호출의 동적 바인딩(프로그램 실행 중에 일어난다)의 차이점과도 흡사하다. 


템플릿 매개변수에 요구되는 암시적 인터페이스는 클래스의 객체에 요구되는 명시적 인터페이스만큼이나 우리 피부에 가깝게 닿아 있다. 클래스에서 제공하는 명시적 인터페이스와 호환되지 않는 방법으로 그 클래스의 객체를 쓸 수 없듯이, 어떤 템플릿 안에서 어떤 객체를 쓰려고 할 때 그 템플릿에게 요구하는 암시적 인터페이스를 그 객체가 지원하지 않으면 사용이 불가능하다.


이것만은 잊지 말자!

- 클래스 및 템플릿은 모두 인터페이스와 다형성을 지원한다.

- 클래스의 경우, 인터페이스는 명시적으로 함수의 시그너처를 중심으로 구성되어 있다. 다형성은 프로그램 실행 중에 가상 함수를 통해 나타난다.

- 템플릿 매개변수의 경우, 인터페이스는 암시적이며 유효 표현식에 기반을 두어 구성된다. 다형성은 컴파일 중에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타난다.



항목 42: typename의 두 가지 의미를 제대로 파악하자


질문: 아래의 두 템플릿 선언문에 쓰인 class와 typename의 차이점이 무엇일까?

template<class T> class Widget;            // "class"를 사용한다.

template<typename T> class Widget;   // "typename"을 사용한다.

답변: 차이가 없다. 템플릿의 타입 매개변수를 선언할 때는 class와 typename의 뜻이 완전히 똑같다.


그렇다고 언제까지나 class와 typename이 C++ 앞에서 동등한 것만은 아니다. typename을 쓰지 않으면 안 되는 때가 분명히 있다.


이것만은 잊지 말자!

- 템플릿 매개변수를 선언할 때, class 및 typename은 서로 바꾸어 써도 무방하다.

- 중첩 의존 타입 이름을 식별하는 용도에는 반드시 typename을 사용한다. 단, 중첩 의존 이름이 기본 클래스 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있는 경우에는 예외이다.



항목 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자


"template<>" 괄호 안에 아무것도 없는 template의 뜻은 '이건 템플릿도 아니고 클래스도 아니다'이다. 완전 템플릿 특수화(total template specialization)라고 한다. 이 테플릿의 매개변수들이 하나도 빠짐없이(완전히) 구첵적인 타입으로 정해진 상태라는 뜻이다.


C++ 컴파일러는 템플릿으로 만들어진 기본 클래스를 뒤져서 상속된 이름을 찾는 것을 거부한다.


기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 진단하는 과정이 미리 들어가느냐, 아니면 나중에 들어가느냐가 이번 항목의 핵심이다. 여기서 C++는 이른바 '이른 진단(early diagnose)'을 선호하는 정책으로 결정한 것이다. 


이것만은 잊지 말자!

- 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는, "this->"를 접두사로 붙이거나 기본 클래스 한정문을 명시적으로 써 주는 것으로 해결하자.



항목 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자


템플릿은 코딩 시간 절약, 코드 중복 회피의 두 마리 토끼를 한꺼번에 잡아 주는 참으로 기막힌 물건이다.


아무 생각 없이 템플릿을 사용하면 템플릿의 적, 코드 비대화(code bloat)가 초래될 수 있다. 똑같은 내용의 코드와 데이터가 여러 벌로 중복되어 이진 파일로 구워진다는 뜻이다.


우선적으로 공통성 및 가변성 분석(commonality and variability analysis)이 있다.


실행 코드가 작아지면 작은 크기로 끝나는 것이 아니라, 프로그램의 작업 세트(프로세스가 현재 사용하는 메모리 양) 크기가 줄어들면서 명령어 캐시 내의 참조 지역성(프로세스의 메모리 참조가 실행 중에 균일하게 흩어져 있지 않으며 특정 시점 및 특정 부분에 집중된다는 경험적/실험적 특성)도 향상된다는 것이 중요한 포인트다.


타입 제약이 엄격한 포인터(즉, T* 포인터)f르 써서 동작하는 멤버 함수를 구현할 때는 하단에서 타입미정(untyped) 포인터(즉, void* 포인터)로 동작하는 버전을 호출하는 식으로 만든다. C++ 표준 라이브러리의 몇 개 구현 제품이 vector, deque, list 등의 템플릿에 대해 이런 식으로 하고 있다.


이것만은 잊지 말자!

- 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어진다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이 된다.

- 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있다.

- 타임 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다.



함수 45: "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방!


스마트 포인터(smart pointer)는 그냥 포인터처럼 동작하면서도 포인터가 주지 못하는 기능을 덤으로 갖고 있는 객체이다.


포인터에도 스마트 포인터로 대신할 수 없는 특징이 있다. 그 중 하나가 암시적 변환(implicit conversion)을 지원한다는 점이다. 


템플릿을 인스턴스화하면 '무제한' 개수의 함수를 만들어낼 수 있다. SmartPtr에 생성자 함수(function)를 둘 필요가 없다. 바로 생성자를 만들어내는 템플릿(template)을 쓴다. 이 생성자 템플릿은 이번 항목에서 이야기할 멤버 함수 템플릿(member function template, 멤버 템플릿이라고도 함)의 한 예이다. 멤버 함수 템플릿은 간단히 말해서 어떤 클래스의 멤버 함수를 찍어내는 템플릿을 일컫는다.


template<typename T>
class SmartPtr {
public:
	template<typename U>				// "일반화된 복사 생성자"
	SmartPtr(const SmartPtr<U>& other);	// 만들기 위해 마련한
									// 멤버 템플릿
};

위 코드는 모든 T 타입 및 모든 U 타입에 대해서, SmartPtr<T> 객체가 SmartPtr<U>로 부터 생성될 수 있다는 이야기다. 그 이유는 SmartPtr<U>의 참조자를 매개변수로 받아들이는 생성자가 SmartPtr<T> 안에 들어있기 때문이다. 이런 꼴의 생성자(같은 템플릿을 써서 인스턴스화되지만 타입이 다른 타입의 객체로부터 원하는 객체를 만들어 주는(즉, SmartPtr<U>로부터 SmartPtr<T>를 만들어내는) 생성자를 가리켜 일반화 복사 생성자(generalized copy constructor)라고들 부른다.

template<typename T>
class SmartPtr {
public:
	template<typename U>
	SmartPtr(const SmartPtr<U>& other)	// 이 SmartPtr에 담긴 포인터를
	:heldPtr(other.get()) { }				// 다른 SmartPtr에 담긴 포잍러로 초기화한다.
	
	T* get() const { return heldPtr; }
	
private:								// SmartPtr에 담
	T *heldPtr;						// 기본 제공 포인터
};

멤버 초기화 리스트를 사용해서, SmartPtr<T>의 데이터 멤버인 T* 타입의 포인터를 SmartPtr<U>에 들어 있는 U* 타입의 포인터를 초기화 했다. 이렇게 해 두면 U*에서 T*로 진행되는 암시적 변환이 가능할 때만 컴파일 에러가 나지 않는다.


멤버 함수 템플릿의 활용은 비단 생성자에만 그치지 않는다. 가장 흔히 쓰이는 예는 대입 연산이다.


멤버 함수 템플릿은 코드 재사용만큼이나 키특하고 훌륭한 기능이지만, C++ 언어의 기본 규칙까지 바꾸지는 않는다. 


어떤 클래스의 복사 생성을 전부 하고 싶으면, 일반화 복사 생성자는 물론이고 "보통의" 복사 생성자까지 직접 선언해야 한다. 대입 연산자도 마찬가지다.


이것만은 잊지 말자!

- 호환되는 모든 타입을 받아들이는 멤버 함수를 만들려면 멤버 함수 템플릿을 사용하자

- 일반화된 복사 생성 연선과 일반화된 대입 연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사 생성자와 복사 대입 연산자는 여전히 직접 선언해야 한다.



항목 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿안에 정의해 두자


모든 매개변수에 대해 암시적 타입 변환이 되도록 만들기 위해서는 

728x90

'프로그래밍 > Effective C++' 카테고리의 다른 글

Chapter 6 상속, 그리고 객체 지향 설계  (0) 2018.06.07
Chapter 5 구현  (0) 2018.05.23
Chapter 4 설계 및 선언  (0) 2018.04.17
Chapter 3 자원 관리  (0) 2018.04.11
Chapter 2 생성자, 소멸자 및 대입 연산자  (0) 2018.03.28

댓글