이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 332p ~ 340p
이펙티브 C++(332p ~ 340p)
요약
항목 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자
- STL은 컨테이너, 반복자, 알고리즘의 템플릿과 유틸리티 템플릿이 존재한다.
- 이 중 하나가
advance
라는 이름의 템플릿인데, 지정된 반복자를 지정한 거리만큼 이동시켜준다.
// iter를 d 단위만큼 이동
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);
- 간단히 개념만 놓고 볼 때, advance는
iter += n
을 하면 될 것 같지만, 이렇게 구현할 수 없다. - += 연산을 지원하는 반복자는 임의 접근 반복자밖에 없기 때문이다.
- 임의 접근 반복자보다 기능적으로 떨어지는 다른 반복자 타입의 경우에는
++
혹은--
연산을 d번 적용하는 것으로 advance를 구현해야 한다. - STL 반복자는 각 반복자가 지원하는 연산에 따라 다섯 개의 범주로 나눈다.
입력 반복자(input iterator)
- 전진만 가능하다.
- 한 번에 한 칸씩만 이동한다.
- 자신이 가리키는 위치에서 읽기만 가능하며, 한 번만 읽을 수 있다.
- 입력 반복자는 입력 파일에 대한 읽기 전용 파일 포인터를 본떠서 만들었고, C++ 표준 라이브러리의
istream_iterator
가 대표적인 입력 반복자이다.
출력 반복자(output iterator)
- 입력 반복자와 비슷하지만 출력 전용이다.
- C++ 표준 라이브러리의
ostream_iterator
가 대표적인 출력 반복자이다. - 입력 반복자와 출력 반복자는 STL의 5대 반복자 중에서 기능적으로 가정 처진다. 앞으로만 갈 수 있고 자신이 가리키는 위치에서 딱 한 번만 읽거나 쓸 수 있기 때문에, 단일 패스 알고리즘에만 제대로 쓸 수 있다.
순방향 반복자(forward iterator)
- 순방향 반복자는 입력 반복자와 출력 반복자가 하는 일은 기본적으로 다 할 수 있고, 자신이 가리키고 있는 위치에서 읽기와 쓰기를 동시에 할 수 있다. 그것도 여러 번 할 수 있다.
- 순방향 반복자는 다중 패스 알고리즘에 문제 없이 쓸 수 있다.
양방향 반복자(bidirectional iterator)
- 순방향 바복자에 뒤로 갈 수 있는 기능을 추가한 것이다.
임의 접근 반복자(random access iterator)
- 양방향 반복자에 반복자 산술 연산 수행 기능을 추가한 것이다.
- 쉽게 말해 주어진 반복자를 임의의 거리만큼 앞뒤로 이동시키는 일을 상수 시간 안에 할 수 있다는 것이다.
- 반복자 산술 연산 기능은 포인터의 산술 연산과 비슷하며, 임의 접근 반복자는 기본제공 포인터를 본떠서 만들었다.
- C++ 표준 라이브러리에는 지금까지 설명한 다섯 개의 반복자 범주 각각을 식별하는 데 쓰이는 태그 구조체가 정의되어 있다.
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
- 다시 advance 구현 문제로 돌아오면, 반복자들이 종류마다 가능한 것이 있고 불가능한 것이 있기 때문에 구현할 때 신경 쓸 부분이 생긴다.
- 반복자를 주어진 횟수만큼 반복적으로 한 칸씩 이동하는 루프를 돌리는 것이다.
- 이는 선형 시간이 필요하며, 임의 접근 반복자 입장에서는 손해인 부분이다.
- 따라서 advance를 구현할 때 임의 접근 반복자가 매개변수로 들어오면 상수 시간 연산을 이용할 수 있도록 만들고 싶다.
// iter를 d 단위만큼 이동
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (iter가 임의 접근 반복자이다)
{
iter += d;
}
else {
if (d >= 0) { while (d--) ++iter; }
else { while (d--) --iter; }
}
}
- 이제 필요한 것은 iter가 임의 접근 반복자 타입인지 확인해야 하는 것이다.
- 이를 위해 특성정보(traits)를 사용할 수 있다.
- 특성정보는 컴파일 도중에 어떤 주어진 타입의 정보를 얻을 수 있게 하는 객체를 지칭하는 개념이다.
- 특성정보는 C++에 미리 정의된 문법구조가 아니며, 키워드도 아니다. 그냥 C++ 프로그래머들이 따르는 구현 기법이며, 관례이다.
- 특성정보가 되려면 몇 가지 요구사항이 존재한다.
- 기본제공 타입과 사용자 정의 타입에서 모두 돌아가야 한다는 점이 그 중 하나이다.
- 이를테면 advance는 포인터 및 int를 받아서 호출될 때도 제대로 동작해야 한다.
- 이를 정확히 풀어쓰면 특성정보 기법을 포인터 등의 기본제공 타입에 적용할 수 있어야 한다가 된다.
- 어떤 타입의 특성정보는 그 타입의 외부에 존재해야 한다.
- 특성정보를 기본제공 타입에서도 사용하려면 이 방법 밖에 없다. 포인터 안에 별도의 정보를 넣을 수 없기 때문이다.
- 특성정보를 다루는 표준적인 방법은 해당 특성정보를 템플릿 및 그 템플릿의 1개 이상의 특수화 버전에 넣는 것이다.
- 반복자는
iterator_traits
라는 이름으로 준비되어 있다.
// 반복자 타입에 대한 정보를 나타내는 템플릿
template<typename IterT>
struct iterator_traits;
iterator_traits
는 구조체 템플릿이다. 이는 관레이다.iterator_traits
는 구조체로 구현했지만, 특성정보를 위해 구현된 구조체는 특성정보 클래스라고 부른다. 이 또한 관례이다.iterator_traits
클래스가 동작하는 방법은 다음과 같다.iterator_traits<IterT>
안에는 IterT 타입 각각에 대해iterator_category
라는 이름의 typedef 타입이 선언되어 있다. 이 typedef가 IterT의 반복자 범주를 가리킨다.
- 반복자 범주를 두 부분으로 나누어 구현한다. 첫 번째 부분은 사용자 정의 반복자 타입에 대한 구현이다.
- 사용자 정의 반복자 타입으로 하여금
iterator_category
라는 이름의 typedef 타입을 내부에 가질 것을 요구사항으로 든다. - 이 typedef 타입은 해당 태그 구조체에 대응되어야 한다.
- 예를 들어 deque의 반복자는 임의 접근 반복자이므로, deque 클래스에 쓸 수 있는 반복자는 다음과 같은 형태일 것이다.
- 사용자 정의 반복자 타입으로 하여금
template<> // 템플릿 매개변수 생략
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
};
};
- 이
iterator
클래스가 내부에 지닌 중첩 typedef 타입을 똑같이 재생한 것이iterator_traits
이다.
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
};
- 이렇게 되면 사용자 정의 타입에 대해서는 잘 돌아가지만, 반복자의 실제 타입이 포인터인 경우에는 전혀 안 돌아간다.
iterator_traits
의 두 번째 구현 부분은 바로 반복자가 포인터인 경우의 처리이다.- 포인터 타입의 반복자를 지원하기 위해
iterator_traits
는 포인터 타입에 대한 부분 템플릿 특수화를 제공한다.- 포인터의 동작 원리가 임의 접근 반복자와 같으므로 typedef 타입으로
random_access_iterator_tag
를 사용한다.
- 포인터의 동작 원리가 임의 접근 반복자와 같으므로 typedef 타입으로
template<typename IterT>
struct iterator_traits<IterT*> {
typedef random_access_iterator_tag iterator_category;
};
- 특성정보 클래스의 설계 및 구현 방법을 정리하면 다음과 같다.
- 다른 사람이 사용하도록 열어 주고 싶은 타입 관련 정보를 확인한다.(반복자라면 반복자 범주 등이 여기에 해당됨)
- 그 정보를 식별하기 위한 이름을 선택한다.(ex:
iterator_category
) - 지원하고자 하는 타입 관련 정보를 담은 템플릿 및 그 템플릿의 특수화 버전을 제공한다.(ex:
iterator_traits
)
- advance는 이제 다음과 같이 다듬을 수 있다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
if (typeid(typename std::iterator_traits<IterT>::iterator_category)
== typeid(random_access_iterator_tag))
{
iter += d;
}
else {
// ...
}
}
- 뭔가 될 것 같지만, 우선 컴파일 문제가 발생한다.(항목 48에서 자세히 살펴봄)
- 일단 지금은 그보다 더 근본적인 문제를 해결해야 한다.
IterT
타입은 컴파일 도중에 파악되기 때문에iterator_traits<IterT>::iterator_category
를 파악할 수 있는 시점 역시 컴파일 도중이다.- 하지만 if 문은 프로그램 실행 도중에 평가된다.
- 지금 우리에게 필요한 것은 주어진 타입에 대한 평가를 컴파일 도중에 수행하는 조건처리 구문요소이다.
- 오버로딩을 이용하면 이 문제를 해결할 수 있다.
- 어떤 함수 f를 오버로딩한다는 것은 매개변수 리스트가 다르지만 f라는 이름은 같은 오버로드 버전을 여러 개 만든다는 것이다.
- 이 상태에서 f를 호출하면 컴파일러는 넘긴 인자를 보고 호출 시의 전후관계에 가장 잘 맞는 오버로드 버전을 골라낸다.
- 즉, 컴파일러가 함수의 매개변수를 보고 어떤 함수 f를 선택할지 컴파일 타임에 결정한다는 것과 같다.
// 임의 접근
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, random_access_iterator_tag)
{
iter += d;
}
// 양방향
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, bidirectional_iterator_tag)
{
if (d >= 0) { while (d--) ++iter; }
else { while (d--) --iter; }
}
// 입력 반복자
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, input_iterator_tag)
{
if (d < 0)
throw std::out_of_range("Negative distance");
while (d--) ++iter;
}
doAdvance
함수의 오버로딩을 통해 컴파일러는 함수의 매개변수 리스트를 통해 어떤 함수를 선택하여 호출할지 컴파일 타임에 결정할 수 있게 된다.- advance 함수에서는 오버로딩된
doAdvance
를 호출해주는 것 밖에 없다.
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
doAdvance(iter, d, std::iterator_traits<IterT>::iterator_category);
}
- 특성정보 클래스를 어떻게 사용하는지 정리하면 다음과 같다.
- 작업자(worker) 역할을 맡을 함수 혹은 함수 템플릿(doAdvance)을 특성정보 매개변수를 다르게 하여 오버로딩한다. 그리고 전달되는 해당 특성정보에 맞추어 각 오버로드 버전을 구현한다.
- 작업자를 호출하는 마스터(master) 역할을 맡을 함수 혹은 함수 템플릿(advance)을 만든다. 이때 특성정보 클래스에서 제공되는 정보를 넘겨서 작업자를 호출하도록 구현한다.
- 특성정보는 C++ 표준 라이브러리에서 흔하게 쓰인다.
메모
항목 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자
- 특성정보 클래스는 컴파일 도중에 사용할 수 있는 타입 관련 정보를 만들어낸다. 또한 특성정보 클래스는 템플릿 및 템플릿 특수 버전을 사용하여 구현한다.
- 함수 오버로딩 기법과 결합하여 특성정보 클래스를 사용하면, 컴파일 타임에 결정되는 타입별 if else 점검문을 구사할 수 있다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(350p ~ 361p) (1) | 2024.02.12 |
---|---|
이펙티브 C++(340p ~ 350p) (1) | 2024.02.10 |
이펙티브 C++(312p ~ 320p) (1) | 2024.02.08 |
이펙티브 C++(320p ~ 331p) (1) | 2024.02.08 |
이펙티브 C++(306p ~ 312p) (1) | 2024.02.06 |