[Effective C++ 3판] Chapter 7. 템플릿과 일반화 프로그래밍 (항목 41~ 48)
Chapter 7. 템플릿과 일반화 프로그래밍 (항목 41~ 48)
항목 41. 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터.
[이것만은 잊지 말자]- 클래스 및 템플릿은 모두 인터페이스와 다형성을 지원합니다.
- 클래스의 경우, 인터페이스는 명시적이며 함수의 시그너처를 중심으로 구성되어 있습니다. 다형성은 프로그램 실행 중에 가상 함수를 통해 나타납니다.
- 템플릿 매개변수의 경우, 인터페이스는 암시적이며 유효 표현식에 기반을 두어 구성됩니다. 다형성은 컴파일 중에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타납니다.
항목 42. typename의 두 가지 의미를 제대로 파악하자.
// 중첩 의존 이름은 기본적으로 타입이 아니라고 가정된다.
// 중첩 의존 이름이 타입임을 알려주기 위해서는 typename 키워드를 사용해야한다.
template <class T>
void example(void) {
int a; // 비의존 이름(non-dependent name)
T b; // 의존 이름(dependent name)
auto c = T::variable; // 중첩 의존 이름(nested dependent name)
typename T::type d; // 중첩 의존 타입 이름(nested dependent type name)
}
// typename은 중첩 의존 이름을 식별하는 데에만 사용할 수 있다.
template <class T>
void func(const T& obj); // typename 쓰면 안 됨.
// 중첩 의존 이름이 '문맥상' 타입일 수 밖에 없는 경우에는 typename을 쓰면 안된다.
template <class T>
class Derived :public Base<T>::musttype {
Derived(int val)
: Base<T>::musttype(val)
{}
};
[이것만은 잊지 말자]
- 템플릿 매개변수를 선언할 대, class 및 typename은 서로 바꾸어 써도 무방합니다.
- 중첩 의존 타입 이름을 식별하는 용도에는 반드시 typename을 사용합니다. 단, 중첩 의존 이름이 기본 클래스 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있는 경우에는 예외입니다.
항목 43. 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자.
template <class T>
class Base {
public:
void func_of_base() {}
};
// C++은 '이른 진단(early diagnose)'을 선호한다.
// 무효성 체크를 '클래스 템플릿 정의의 구문 분석' 시에 진행한다. (인스턴화될 때가 아니라)
// 그래서 클래스 템플릿이 어디로 부터 상속되는 지 구체적으로 모른다.
// 따라서 func_of_base가 들어있는지도 알 수 없다. 그것이 문제다.
// 컴파일러에 따라 표준을 어기고, 이를 허용해주는 경우도 있다. (visual studio 2013이 그러네요ㅜ)
template <class T>
class Problem :public Base<T> {
public:
void foo() {
func_of_base(); // compile error!
}
};
// this pointer를 이용한 해결책.
template <class T>
class SolutionOne :public Base<T> {
public:
void foo() {
this->func_of_base(); // 함수가 상속되는 것으로 가정된다.
}
};
// using 선언을 이용한 해결책.
template <class T>
class SolutionTwo :public Base<T> {
public:
using Base<T>::func_of_base; // Base<T>에 함수가 있는 것으로 가정된다.
void foo() {
func_of_base();
}
};
// 명시적호출을 통한 해결책.
// 단, 가상 함수 바인딩이 무시되므로 안쓰는게 좋다.
template <class T>
class SolutionThree :public Base<T> {
public:
void foo() {
Base<T>::func_of_base(); // 명시적으로 함수를 한정한다.
}
};
// 개인적인 생각으로는 해결책1(this pointer를 이용한 해결책) 이 가장 좋아보인다.
[이것만은 잊지 말자]
- 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는, "this->"를 접두사로 붙이거나 기본 클래스 한정문을 명시적으로 써 주는 것으로 해결합시다.
항목 44. 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자.
[이것만은 잊지 말자]- 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어집니다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이 됩니다.
- 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있습니다.
- 타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있습니다.
항목 45. "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방!
template <class T>
class SmartPointer {
public:
template<class U>
explicit SmartPointer(U *ptr)
: ptr(ptr)
{}
SmartPointer(const SmartPointer &other);
template <class U>
SmartPointer(const SmartPointer<U>& other)
: ptr(other.get())
{}
T* get() const { return ptr; }
...
private:
T* ptr;
};
class Base {};
class Derived : public Base{};
class Base2 {};
int main() {
SmartPointer<Base> a(new Derived); // compile ok.
SmartPointer<Base2> b(new Base); // compile error.
SmartPointer<Derived> c(new Derived); // compile ok.
SmartPointer<Base> d = c; // compile ok.
}
[이것만은 잊지 말자]
- 호환되는 모든 타입을 받아들이는 멤버 함수를 만들려면 멤버 함수 템플릿을 사용합시다.
- 일반화된 복사 생성 연산과 일반화된 대입 연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사 생성자와 복사 대입 연산자는 여전히 직접 선언해야 합니다.
항목 46. 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자.
// 템플릿 인자 추론 과정에서는 암시적 타입 변환이 고려되지 않습니다.
// 암시적 타입 변환을 고려하려면 타입을 명확히 알아야하는데 추론작업중이니까 당연히 불가능한 것입니다.
namespace issue {
template <class T>
class Example {
public:
Example(int num);
};
template <class T>
const Example<T> operator*(const Example<T> &lhs, const Example<T> &rhs) {
...
}
void situation() {
Example<int> a(2);
a * 2; // compile error.
}
}
// 해결책은 비멤버 함수를 클래스 템플릿안에 담는 것입니다.
// 그러면 클래스 템플릿이 인스턴스화될 때 비멤버 함수도 같이 정의되므로 문제가 없습니다.
// 비멤버 함수를 클래스 템플릿 안에 담기위해 friend 선언을 사용합니다.
namespace solution {
template <class T>
class Example {
public:
Example(int num);
friend const Example<T> operator*(const Example<T> &lhs, const Example<T> &rhs) {
...
}
};
void situation() {
Example<int> a(2);
a * 2; // compile ok!
}
}
[이것만은 잊지 말자]
- 모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과 관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면, 이런 함수는 클래스 템플릿 안에 프렌드 함수로서 정의합시다.
항목 47. 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자.
[STL의 5대 반복자 범주]- input iterator (전진, 읽기, 해당 위치에서 읽기 1번만 허용)
- output iterator (전진, 쓰기, 해당 위치에서 쓰기 1번만 허용)
- forward iterator (전진, 읽기/쓰기, 무한정)
- bidirectional iterator (양방향, 읽기/쓰기, 무한정)
- random access iterator (iterator arithmetic, 양방향, 읽기/쓰기, 무한정)
그리고 위 범주에 해당하는 tag 구조체들이 표준 라이브러리에 정의되어 있다.
[특성정보 클래스]
- 컴파일타임에 주어진 타입의 정보를 얻을 수 있게 하는 클래스.
- 대표적인 예시로 STL의 iterator_traits가 있다.
// https://gist.github.com/taeguk/f80fddabaa48877e80a0600722d566a0
#include <iostream>
// 특성정보에 대한 tag들.
struct walk_ability_tag {};
struct jump_ability_tag {};
struct fly_ability_tag {};
struct run_ability_tag :public walk_ability_tag {};
struct ground_ability_tag :public run_ability_tag, public jump_ability_tag {};
struct all_ability_tag :public ground_ability_tag, public fly_ability_tag {};
// 특성정보 클래스 템플릿.
// 특성정보 클래스는 관례에 따라 항상 구조체로 구현한다.
template <class T>
struct ability_traits {
typedef typename T::ability_category ability_category;
};
// 특성정보에 따라 호출될 함수가 컴파일 타임에 결정된다.
template <class T>
void move(T& obj) {
move(obj, typename ability_traits<T>::ability_category());
}
// 특성정보에 따른 함수들의 오버로딩.
template <class T>
void move(T&, walk_ability_tag) { std::cout << "move by walking." << std::endl; }
template <class T>
void move(T&, jump_ability_tag) { std::cout << "move by jumping." << std::endl; }
template <class T>
void move(T&, fly_ability_tag) { std::cout << "move by flying." << std::endl; }
template <class T>
void move(T&, run_ability_tag) { std::cout << "move by running." << std::endl; }
template <class T>
void move(T&, ground_ability_tag) { std::cout << "move through ground." << std::endl; }
template <class T>
void move(T&, all_ability_tag) { std::cout << "move by all methods." << std::endl; }
// 특성정보를 지원하는 클래스들.
class Walker {
public:
typedef walk_ability_tag ability_category;
};
class Jumper {
public:
typedef jump_ability_tag ability_category;
};
class Flyer {
public:
typedef fly_ability_tag ability_category;
};
class Runner {
public:
typedef run_ability_tag ability_category;
};
class GroundMan {
public:
typedef ground_ability_tag ability_category;
};
class Master {
public:
typedef all_ability_tag ability_category;
};
int main() {
Walker walker;
Jumper jumper;
Flyer flyer;
Runner runner;
GroundMan groundMan;
Master master;
move(walker);
move(jumper);
move(flyer);
move(runner);
move(groundMan);
move(master);
/* 실행결과
move by walking.
move by jumping.
move by flying.
move by running.
move through ground.
move by all methods.
*/
return 0;
}
위는 제가 작성해본 특성정보 클래스 예시입니다. 현재 naver smart editor 3.0의 코드 하이라이팅 지원이 매우 부실한 관계로 보기가 불편한 분들은 https://gist.github.com/taeguk/f80fddabaa48877e80a0600722d566a0 에 가서 코드를 보시는 것을 적극 추천합니다.
일반 함수/클래스의 경우에는 객체의 타입을 바로 알 수가 있습니다. 하지만 함수/클래스 템플릿의 경우에는 템플릿 파라미터에 의존적임으로 객체의 타입을 알 수가 없습니다. 따라서 객체의 타입에 따라 동작을 다르게 하기 위해서는 템플릿 특수화를 사용해야합니다. 하지만 이는 템플릿 작성자가 알고있는 타입에 대해서만 적용가능할 뿐이고, 임의의 타입에 대해서 동작을 다르게 할 수는 없습니다. 따라서 이에 대한 해결책은 미리 임의의 타입들을 분류 할 수 있는 tag들을 정의해놓고, 함수/클래스 템플릿 개발자는 타입 내부에 tag정보가 있다고 가정하고 코드를 짭니다. 사용자들은 이러한 함수/클래스 템플릿을 이용하기 위해 typedef를 통해 자신이 만든 class에 tag를 답니다. 하지만 이 방법은 C++의 기본제공타입(char,int,포인터등)에 대해 템플릿을 적용할 수 없다는 문제가 있습니다. 그래서 따로 특성정보를 나타내는 구조체 템플릿을 하나 만들고, 이 템플릿을 특수화함으로서 기본제공타입을 지원하는 것입니다. 이것이 바로 '특성정보 클래스'기법입니다.
자, 그러면 이제 함수/클래스 템플릿 개발자는 특성정보 클래스를 이용하여 컴파일타임에 특정 객체의 tag를 알아낼 수 있게 되었습니다. (즉, 컴파일타임에 객체들을 '분류'할 수 있게 되었습니다.) 그러면 본래 목표대로 tag에 따라 동작을 다르게 해야합니다. 일단, typeid를 이용한 방법이 있습니다. 하지만 이는 RTTI를 이용함으로 runtime시에 overload가 존재합니다. 또한 특정 tag에만 특정 함수가 존재하는 그런 경우에는 typeid를 이용한 if...else는 컴파일에러를 띄울 것입니다. 따라서 이에 대한 해결책은 함수 오버로딩을 이용하는 것입니다. 이를 통해 컴파일타임에 tag에 따라 서로 다른 함수를 호출하도록 할 수 있고 고로 본래 목표를 이룰 수 있습니다. 너무 멋있죠? 태양의 후예 송중기만큼 멋있습니다.
[이것만은 잊지 말자]
- 특성정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어냅니다. 또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현합니다.
- 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if...else 점검문을 구사할 수 있습니다.
항목 48. 템플릿 메타프로그래밍, 하지 않겠는가?
템플릿 메타프로그래밍은 개쩝니다. 템플릿 메타프로그래밍은 C++ 컴파일러가 실행시키는, C++와는 또 다른 영역의, 그런 프로그래밍 방식입니다. C언어 사용자분들이 이해하기 쉽게 표현하면 일종의 개쩌는 매크로같은 느낌이기도 합니다(개인적인 생각에 ㅎㅎ). 템플릿 메타프로그래밍, 통칭 TMP는 런타임타임에 해야하는 것을 컴파일타임에 할 수 있게 합니다. 항목 47의 특성정보 클래스 기법도 TMP입니다. TMP는 또한 튜링 완전성을 가지고 있습니다. 즉, 범용 프로그래밍 언어처럼 무엇이든 계산할 수 있습니다. 저도 언제 한번 TMP를 공부하고 싶네요... 사실상 C++로 라이브러리/프레임워크를 개발하려면 반드시 알아야하지 않을까 싶습니다 ㅎㅎ// https://gist.github.com/taeguk/5920b5773ae127dc51695cf1edd0f6c2
#include <iostream>
template <unsigned N>
struct IsPrime {
static_assert(N >= 2, "The number must not be less than 2.");
IsPrime() = delete;
static bool value() { return static_cast<bool>(_value); }
private:
template <unsigned D, bool, class Dummy = int>
struct _CheckPrime {
enum { value = _CheckPrime<D - 1, N % D == 0, Dummy>::value };
};
template <unsigned D, class Dummy>
struct _CheckPrime<D, true, Dummy> {
enum { value = 0 };
};
template <class Dummy>
struct _CheckPrime<1, false, Dummy> {
enum { value = 1 };
};
enum { _value = _CheckPrime<N - 1, false>::value };
};
template <unsigned _First, unsigned _Last, unsigned _Increment>
struct TestIsPrime {
static_assert(_First <= _Last, "");
static_assert(_Increment > 0, "");
TestIsPrime() = delete;
static void run() {
_TestIsPrimeLoop<_First, false>::run();
}
private:
template <unsigned _Cur, bool>
struct _TestIsPrimeLoop {
static void run() {
std::cout << _Cur << (IsPrime<_Cur>::value() ? " is prime." : " is not prime.") << std::endl;
_TestIsPrimeLoop<_Cur + _Increment, (_Cur + _Increment > _Last)>::run();
}
};
template <unsigned _Cur>
struct _TestIsPrimeLoop<_Cur, true> {
static void run() {
}
};
};
int main() {
TestIsPrime<2, 100, 1>::run();
}
이대로 그냥 포스팅을 마치기는 아쉬워서 한번 간단하게(?) TMP를 짜보았습니다. 어떤 수가 소수인지 알아내는 IsPrime과 이를 편하게 테스트하게 해주는 TestIsPrime을 만들어보았습니다. (https://gist.github.com/taeguk/5920b5773ae127dc51695cf1edd0f6c2)
템플릿에 익숙하지 않으신 분들은 위 코드가 정말 뭐가뭔지 모르실지도 모르겠습니다만,, 어쨌든 재밌는 건 저 모든것이 "컴파일 타임"에 진행된다는 것입니다! 코드에 대해 몇 가지 comment를 달아보겠습니다. 일단 함수 템플릿을 사용안하고 구조체(클래스) 템플릿을 사용한 이유입니다. 함수 템플릿은 2가지 단점(?)이 있습니다. 첫째는 부분 템플릿 특수화가 안되는 점입니다. 그래서 _CheckPrime과 _TestIsPrimeLoop는 클래스 템플릿을 이용할 수 밖에 없습니다. 둘째는 함수안에는 템플릿 정의를 할 수 없다는 것입니다. 정보은닉과 캡슐화를 위해서 IsPrime과 TestIsPrime은 클래스 템플릿으로 만들 수 밖에 없습니다. 그리고 _CheckPrime에서 Dummy parameter를 사용한 이유는 "explicit specialization in non-namespace scope"이 허용되지 않기 때문입니다. (거지같은 C++)
저도 TMP는 정말 초짜인데요ㅠㅠ 한번 머리를 굴려서 위와 같이 짜보았습니다. 덕분에 시간이 훅 날라갔네요 ㅋㅋㅋ TMP는 정말 야매(?)의 정점같다는 느낌이 들었습니다 ㅋㅋ 천재/괴짜들이 좋아할 만한 분야같네요. 코드를 짜는데 처음에는 저의 부족한 TMP지식을 가지고 어떻게 for loop를 구현해야할 지 막막했습니다. 그러다가 위 코드같이 탈출 조건을 template parameter에 넣음으로서 해결하였습니다. 그리고 원래 sqrt를 구현해야하는데..귀찮아서 그냥 좀 비효율적으로 짰습니다. 어차피 예시일 뿐이니까요! 어쨌든 TMP 나중에 한번 정식으로 배워보고 싶긴 하네요. 아참, C++11부터 추가되어서 발전하고 있는 constexpr를 사용하면 위와 같은 작업을 좀 더 편하고 가독성있게 할 수 있습니다. (TMP가 매크로 함수라면, constexpr는 inline함수 같은 느낌같은 느낌이랄까요?? ㅎㅎㅎㅎ)
[이것만은 잊지 말자]
- 템플릿 메타프로그래밍은 기존 작업을 런타임에서 컴파일 타임으로 전환하는 효과를 냅니다. 따라서 TMP를 쓰면 선행에러 탐지와 높은 런타임 효율을 손에 거머쥘 수 있습니다.
- TMP는 정책 선택의 조합에 기반하여 사용자 정의 코드를 생성하는 데 쓸 수 있으며, 또한 특정 타입에 대해 부적절한 코드가 만들어지는 것을 막는 데도 쓸 수 있습니다.
드뎌 포스팅했습니다! 네 effective c++도 이제 2 챕터 남았네요.. 정말 포스팅 너무 귀찮은 것... 하지만 보람있고 공부도 깊게 하게되고 머리에도 깊숙히 박히는 것 같고 좋습니다 하하하 그럼 다음에 만나요~~
감사합니다! 다른 글들은 전부 "템플릿 매개변수에 종속된 것을 가리켜 의존 이름이라고 한다. 의존 이름이 어떤 클래스 안에 중첩되어 있는 경우가 있는데. 이 경우의 이름을 중첩 의존 (타입) 이름이라고 한다." 라고 설명해서 이해가 안됐는데 바로 되네요 정말 감사합니다!!
답글삭제