A Tour of C++ : 6장 템플릿
.
.
책 읽으면서 정리한 메모
6. 템플릿
파라미터화된 타입 : 제한된 템플릿 인자, 값 템플릿 인자, 템플릿 인자 추론
파라미터화된 연산 : 함수 템플릿, 함수 객체, 람다 표현식
템플릿 메커니즘 : 가변 템플릿, 별칭, 컴파일 시간 if
6.1 소개
템플릿은 타입이나 값의 집합을 파라미터화한 클래스나 함수.
6.2 파라미터화된 타입
template<typename T> 는 template<class T> 와 동일.
“모든 타입 T에 대해”라는 C++ 표현.
template<typename T>
class Vector {
// ...
}
Vector<char> vc(200); // 문자 200개를 포함하는 벡터
Vector<string> vs(17); // 문자열 17개
Vector<list<int>> vli(45); // 정수의 리스트 45개
템플릿+인자 = 인스턴스화 or 특수화
컴파일 후반부에 인스턴스화 시간 (instantiation time) 에 코드 생성. 타입 안전성을 보장하나, 컴파일 과정의 후반부에 수행됨.
6.2.1 제한된 템플릿 인자 (C++20)
template<Element T>
class Vector {
...
}
수학적으로 ‘Element(T) 를 만족하는 모든 T에 대해”라는 개념의 C++ 표현.
Element = Vector에서 요구하는 성질 T를 만족하는지 확인하는 술어
= 컨셉 = 제한된 인자 = 제한된 템플릿.
// Vector 가 복사 연산자를 가질 때
Vector<int> v1; // 정상 : int 는 복사할 수 있음
Vector<thread> v2; // 에러 : thread 는 복사 불가
컨셉은 C++ 20 부터 지원.
6.2.2 값 템플릿 인자
템플릿 타입 인자에 값 인자(Value arguments)도 받을수 있다.
template<typename T, int N>
struct Buffer {
//...
T a[N];
}
Buffer<char, 1024> blob;
6.2.3 템플릿 인자 추론
표준 pair 를 예로
pair<int, double> p = {1, 5.2};
일일이 타입지정 안 해도 됨
auto p = make_pair(1, 5.2);
생성자 인자로 템플릿 파라미터 추론 가능. C++17
pair p = {1, 5.2}; // p는 pair<int, double>
코드가 단순해지지만 유의 필요.
Vector<string> vs1 = { ”Hello”, “World” }; // Vector<string> 명확
Vector vs { ”Hello”, “World” }; // Vector<const char*> 로 추론. (실수일까?)
Vector vs2 { ”Hello”s, “World”s }; // Vector<string> 으로 추론
Vector vs3 { ”Hello”s, “World” }; // 에러. 타입이 다름. string, const char*
“문자열” 의 기본 타입은 const char* 이다. 접미사 s 를 붙여 string 으로 만들어야함.
인자 추론이 어렵다면, 생성자에 추론 가이드(deduction guide) 를 제공할 수 있다.
but, 추론 가이드는 미묘한 면이 있으므로 추론 가이드를 사용할 필요가 없게 클래스 템플릿을 설계하는 것이 가장 좋다.
6.3 파라미터화된 연산
타입과 알고리즘 모두 파라미터화할 때 사용.
- 함수 템플릿
- 함수 객체 : 데이터를 포함하면서 함수처럼 호출 가능한 객체
- 람다 표현식(lambda expression) : 함수 객체를 간단하게 표기하는 방법
6.3.1 함수 템플릿
for 구분으로 모든 시퀀스 합 구하기
template<typename Sequence, typename Value>
Value sum(const Sequence& s, Value v)
{
for (auto x : s)
v += x;
return v;
}
단, 함수 템플릿은 멤버가 될 수 있지만, virtual 멤버 될 수 없다. 컴파일러가 vtbl 을 만들지 못함.
6.3.2 함수 객체
함수처럼 호출할 수 있는 객체 정의.
template<typename T>
class Less_than {
const T val; // 비교 대상
public:
Less_than(const T& v) : val{v} {}
bool operator()(const T& x) const { return x<val; } // 연산자 호출
};
operator() 로 () 연산자 구현.
Less_than lti {43}; // x 를 42에 비교
Less_than lts {“Backus”s };
Less_than<string> lts2 {“Naur”};
객체를 그냥 함수처럼 호출
int n = 10;
string s = “Test”;
bool b1 = lti(n);
bool b2 = lts(s);
함수 객체는 알고리즘 인자로도 널리 사용됨. 특정 술어에 대해 true 요소 개수 알기
template<typename C, typename P> // Sequence<C>, Callable<P, Value_type<P>>
int count(const C& c, P pred)
{
int cnt = 0;
for (const auto& x : c)
if (pred(x))
++cnt;
return cnt;
}
count 에서 Less_than 을 사용한 것처럼, 알고리즘에서 핵심 동작의 의미를 지정하지 위해 사용한 함수 객체를 “정책 객체”라고 함. Policy Objects
6.3.3 람다 표현식
알고리즘과 별개로 Less_than 객체를 만들었는데, 불편할 수 있고, 암묵적으로 만들 수 있음.
cout<< count( vec, [&]( int a ){ return a<x; } )
cout<< count( lst, [&]( const string& a ){ return a<s; } )
[&]( int a ) { return a<x; } 같은 표기법을 람다 표현식이라고 함. Less_than<int>{x} 와 유사한 함수 객체를 생성. [&]는 캡처 리스트.람함다 몸체 안에서 사용하는 (x와 같은)모든 지역 이름이 참조로 접근됨을 명시. 복사는 [=]. x만 캡처하려면 [&x]. x를 복사한 객체를 만들고 싶다면 [=x]. []는 아무것도 캡처하지 않음.
사용은 간편하나 모호함 유발. 표현식이 간단하지 않으면 연산에 이름을 부여해서 목적을 명확히 하고 여러 곳에서 사용할 수 있도록.
draw_all(), rorate_all() 처럼 포인터나 unique_ptr 을 포함하는 vector(Container) 요소마다 어떤 연산 적용하는 함수를 여러 벌 만들기 귀찮을 때, 이럴 때 함수 객체(특히 람다)를 사용, 컨테이너 순회 작업과 각 요소로 어떤 일을 할지 분리할 수 있다.
먼저, 포인터를 포함하는 컨테이너의 각 요소가 가리키는 객체에 특정 연산을 적용하는 함수가 필요.
template<typename C, typename Oper>
void for_all( C& c, Oper op )
{
for (auto& x : c)
op( x );
}
이제 일련의 _all() 시리즈 대신 위 함수를 활용.
void user2()
{
vector<unique_ptr<Shape>> v;
while (cin)
v.push_back( read_shape(cin) );
for_all( v, [](unique_ptr<Shape>& ps){ ps->draw(); }); // draw_all()
for_all( v, [](unique_ptr<Shape>& ps){ ps->rotate(45); }); // rotate_all(45)
}
람다도 함수처럼 제네릭할 수 있다.
template<class S>
void rotate_end_draw( vector<S>& v, int r)
{
for_all( v, [](auto& s){ s->rotate(r); s->draw(); } );
}
auto 파라미터를 이용하면 람다를 템플릿으로 만들 수 있고, 이를 일컬어 제네릭 람다(Generic lambda)
표준위원회의 정치적 이유 때문인지 함수 인자에서 auto 사용은 아직 허용되지 않는다.
람다를 이용하면 어떤 구문이든 표현식을 만들 수 있는데, 주로 인자값을 바탕으로 어떤 값을 계산하는 연산 제공하는 데 주로 쓰임. 용도는 광범위.
복잡한 초기화도.
switch ( mode )
case : v = val; break;
case : v = val; break;
자료 구조를 초기화하기 위해 여러 대안 중 하나를 선택, 각 대안마다 연산을 해야하는 경우. ‘효율성’이란 미명하에 지저분한 코드와 버그의 원인을 만들어냄.
- 의도된 값을 얻기 전에 변수가 사용될 수 있다.
- 초기화 코드가 다른 코드와 섞이면 이해하기 어렵다.
- 초기화 코드가 다른 코드와 섞이면 각 대안에 대한 case를 잊어버리기 쉽다.
- 초기화가 아닌 대입에 가깝다.
이 코드 대신 람다를 이용할 수 있다.
vector<int> v = [&] {
switch( m )
case zero:
return vector<int>(n); // n개 요소를 0으로 초기화
case seq:
return vector<int>{p,q}; // 시퀀스 [p:q]에서 복사
case cpy:
return arg;
}
};
6.4 템플릿 메커니즘
좋은 템플릿 정의를 위해 필요한 언어 기능
- 타입에 의존적인 값 : 가변 템플릿 variable templates
- 타입과 템플릿의 별칭 : 별칭 템플릿 alias templates
- 컴파일 시간 선택 메커니즘 : if constexpr
- 타입과 표현식의 속성을 질의할 수 있는 컴파일 시간 메커니즘 : requires 표현식
constexpr함수, static_asserts도 템플릿 설계와 활용에 종종 쓰임. 이런 기본 메커니즘들은 일반적, 기본적 추상화 도구로 사용
6.4.1 가변 템플릿
특정 타입 사용할 때 상수, 변수가 필요하듯 템플릿도 마찬가지.
template <class T>
constexpr T viscosity = 0.4; // 템플릿 상수
template <class T>
constexpr space_vector<T> external_acceleration = { T{}, T{-9.8}, T{} }; // space_vector 는 3차원 벡터
auto vis2 = 2*viscosity<double>;
auto acc = external_acceleration<float>; // float 3개 벡터.
6.4.2 별칭
타입, 템플릿의 동의어를 만들면 유용한 경우가 많음.
표준 헤더 <cstddef> 의 별칭 size_t 정의
using size_t = unsigned int;
size_t 라는 이름의 실제 타입은 구현마다 다름. size_t 가 unsigned long 일 수 있다. 이식성을 높이기 위해 사용하기 좋음.
일반적인 타입 파라미터화
template<typename T>
class Vector {
public:
using value_type = T;
}
모든 표준 라이브러리 컨테이너는 값 타입이 value_type
template<typename C>
using Value_type = typename C::value_type; // 템플릿C의 요소 타입을 Value_type 으로
template<typename Container>
void algo(Container& c)
{
Vector<Value_type<Container>> vec;
// ...
}
새로운 템플릿 타입도 재정의 가능
template<typename Value>
using String_map = Map<string, Vaule>;
String_map<int> m; // m 타입은 Map<string, int>
6.5 조언
[1] 다양한 인자 타입에 적용 가능한 알고리즘은 템플릿으로
[4] 템플릿은 타입 안전성을 보장하지만, 타입 확인이 컴파일 시간상 늦음
[6] 알고리즘 인자로 함수 객체 사용
[7] 간단한 함수 객체를 한 곳에서만 쓴다면 람다로.
[8] 가상 함수 멤버는 템플릿 멤버 함수가 될 수 없다.
[9] 표기 방식은 간단히, 상세 구현을 숨기기 위해 템플릿 별칭 사용 6.4.2
.
.
.