A Tour of C++ : 7장 컨셉과 제네릭 프로그래밍
.
.
7. 컨셉과 제네릭 프로그래밍
7.1 소개
템플릿의 기능
- 타입을 정보의 손실 없이 인자로 전달. 인라인을 활용할 기회 커짐
- 인스턴스화 시점에 여러 경우에 따라 정보를 달리함으로 최적화
- 상수 값을 인자로 전달, 컴파일 시간 계산 수행 가능.
컴파일 시간에 계산과 타입 조작 메커니즘 제공, 코드를 명료하고 효율적으로 만듬. 타입(클래스)은 코드와 값을 모두 포함할 수 있다는 것이 중요.
가장 우선적/일반적 사용 목적은 제네릭 프로그래밍 지원. “일반적”인 알고리즘 설계, 구현에 집중.
7.2 컨셉(C++20)
어떤 템플릿 함수의 첫 템플릿 인자가 시퀀스의 한 종류이고, 두 번째 템플릿 인자가 수의 한 종류인 경우. 이런 요구 사항을 컨셉(concept)이라 함.
7.2.1 컨셉의 사용
template<typename Seq, typename Num>
Num sum(Seq s, Num v) { ... }
->
template<Sequence Seq, Number Num>
Num sum(Seq s, Num v) { ... }
->
template<Sequence Seq, Number Num>
requires Arithmetic<Value_type<Seq>, Num>
Num sum(Seq s, Num n);
Arithmetic<X,Y> 는 수치 타입 X, Y 를 이용한 산술 연산이 가능함을 나타내는 컨셉. vector<int>, <double> 은 허용, vector<string> 은 방지.
requirements 절 = requires Arithmetic<Value_type<Seq>, Num>
절충안
=>
template<Sequence Seq, Arithmetic<Value_type<Seq>> Num>
Num sum(Seq s, Num n);
컨셉을 못쓴다면, 아래와 같이 명명 관례와 주석을 활용.
template<typename Sequence, typename Number>
// requires Arithmetic<Value_type<Seq>, Num>
Number sum(Sequence s, Number n);
이처럼 템플릿 인자에 의미 있는 제약을 주도록 설계하자.
// 다음 소제목들은 스킵
7.2.2 컨셉 기반 오버로딩
7.2.3 유효한 코드
7.2.4 컨셉 정의
컨셉은 하나 이상의 타입을 어떻게 사용할 수 있는지를 지정하는 컴파일 시간 술어.
7.3 제네릭 프로그래밍
서로 다른 데이터 표현에 적용 가능한 제네릭 알고리즘 만들기가 주.
이런 기본적인 연산, 데이터 구조 표현하는 추상화 = 컨셉
7.3.2 템플릿을 이용한 추상화
훌륭한 추상화는 구체적인 예로부터 생겨난다.
모든 필요와 기술에 대비한 추상화 시도는 부주의함과 쓸데없이 긴 코드만 남을 뿐이다.
실제 용례로부터 시작하고 불필요한 세부 사항은 제거하자.
double sum(const vector<int>& v)
{
double res = 0;
for (auto x : v)
res += x;
return res;
}
무엇이 일반성을 떨어뜨리는가?
왜 int?
왜 vector?
왜 double?
왜 0부터?
왜 덧셈?
앞 네 개의 질문 => 구체 타입을 템플릿 인자로 교체
template<typename Iter, typename Val>
Val accumulate(Iter first, Iter last, Val res)
{
for (auto p = first; p != last; ++p)
res += *p;
return res;
}
- 반복자 한 쌍으로 어떤 자료구조 탐색할지 추상화
- 누산기 타입을 파라미터로 받음
- 초깃값을 인자로 받아, 초깃값 타입은 누산기 타입을 따름
void use( const vector<int>& vec, const list<double>& lst )
{
auto sum = accumulate( begin(vec), end(vec), 0.0 ); // 0.0 을 인자로 넣어, double 에 더하기
auto sum2 = accumulate( begin(lst), end(lst), sum );
동일한 성능을 유지하면서 구체적인 코드를 일반화하는 과정 = 리프팅(lifting)
템플릿 개발 가장 좋은 과정은 일반적으로 거꾸로 진행됨.
1. 구체 버전 만들기
2. 디버그, 테스트, 측정
3. 구체 타입을 템플릿 인자로 교체.
begin(), end() 반복도 단순화
template<Range R, Number Val>
Val accumulate( R r, Val res = 0 )
{
for (auto p = begin(r); p ~= end(r); ++p )
res += *p;
return res;
}
완벽한 일반화를 위해 += 연산도 대체 가능. 14.3절 참조.
7.4 가변 템플릿
인자 개수를 가변적으로 받아들이는 템플릿.
전통적 가변 템플릿 = 첫 번째 인자와 나머지를 분리하고, 인자 목록 끝에 인자를 재귀적 호출.
template< typename T, typename ... Tail >
void print( T head, Tail... tail)
{
cout << head << ‘ ‘;
print(tail...);
}
typename ... 은 Tail 이 타입의 시퀀스, Tail... 은 tail 이 Tail에 속하는 타입의 값들로 이뤄진 시퀀스임을 나타냄. ... 선언은 파라미터 팩(parameter pack).
가변 템플릿의 단점
- 재귀적인 구현 고치기 어렵다.
- 컴파일 시간 비용이 의도치 않게 클 수 있다.
- 인터페이스의 타입 검사하려면 어려운 템플릿 프로그램이 필요할 수 있다.
표준 라이브러리에서 많이 사용되지만, 같은 이유로 남용되기도.
7.4.1 접힘 표현식
가변 템플릿 구현 단순화 위해 C++17 파라미터 팩 요소 순회 방법 제한된 형태로 제공.
template<Number ... T>
int sum( T... v ) {
return (v + ... + 0); // 접힘 표현식 오른쪽 접힘.
// v[0] + (v[1] + ( ... ( v[n] + 0 ) ) ) 와 같다.
// return (0 + ... + v); // 왼쪽 접힘.
}
현재 C++ 의 접힘 표현식은 가변 템플릿 구현 단순화 용도로만 제한됨.
template<typename ... T>
void print(T&&... args)
{
(std::cout << ... << args) << ‘\n’; // 모든 인자 출력
}
print( “Hello!”s, ‘ ‘, “World “, 2017 );
// (((((std::cout << “Hello!”s) << ‘ ‘) << “World”) << 2017) << ‘\n’);
7.4.2 인자 포워딩
template<typename Transport>
requires concepts::InputTransport<Transport>
class InputChannel {
public:
InputChannel(TransportArgs&&... transportArgs)
: _transport( std::forward<TransportArgs>(transportArgs)... )
{}
Transport _transport;
}
표준 라이브러리 함수 forward() (13.2.2절) 이용, _transport 생성자에 그대로 전달. InputChannel 작성자는 필요 인자 무엇인지 모른 채로 Transport 타입 객체 생성 가능.
기본 라이브러리에서 인자 포워딩 널리 사용.(일반성, 런타임 부하 감소.)
7.5 템플릿 컴파일 모델
컨셉 사용 이전의 코드에서는 모든 타입 검사를 인스턴스화 시간에 수행. 덕 타이핑(duck typing)문제를 유발. 컨셉을 사용하면 모든 컨셉을 확인한 후에 인스턴스화가 이루어짐. 또, 모듈(3.3절)을 사용하여 템플릿 함수의 코드를 정리할 수 있어 텍스트 포함(textual inclusion)문제로부터 보호된다.
7.6 조언
[1] 템플릿은 컴파일 시간 프로그래밍에 필요한 일반적인 메커니즘 제공
[3] 템플릿 설계 시, 템플릿 인자에서 가정하는 개념(요구사항)을 고려
[4] 컨셉을 설계 도구로
[7] 한 곳에서만 필요한 간단한 표현식이라면 람다를
[9] 템플릿으로 컨테이너와 구간 표현
[13] 동일한 타입의 인자 목록은 가변 템플릿 대신 초깃값 목록을
.
.
.