본문 바로가기
PROGRAMMING/Effective Modern C++

13. iterator보다 const_iterator를 선호하라.

by La-KanTo 2020. 12. 26.

- itorator보다 const_iterator를 선호하라.

가능한 한 항상 const를 사용하는 것이 좋다. 하지만 프로그래머들은 가능한 한 항상 const를 사용하지 않는다. 실용적일 때에만 항상 사용할 뿐이다. 그리고 C++98에서 const_iterator는 그리 시용적이지 못하다.

 

왜냐하면 C++98에서는 비상수 컨테이너로부터 const_iterator를 얻는 방법도 까다로웠고, 삽입/삭제 위치를 iterator로만 지정할 수 있었기 때문이다. 하지만 C++11에서는 C++98에서의 문제점들이 개선되었다. 따라서 C++11 환경에서 프로그래밍을 한다면 가능한 한 iterator 보다 const_iterator를 사용하는 것이 좋다.

 

예를 들어 std::vector<int>에서 1983이라는 값이 처음 나오는 지점을 찾고 그 곳에 1998이라는 값을 삽입한다고 하자.
벡터에 1983이 하나도 없으면 1998을 벡터의 끝에 삽입해야 한다. C++98에서는 iterator를 사용하여 간단하게 구현할 수 있다.

std::vector<int> values;
std::vector<int>iterator it = std::find(values.begin(), values.end(), 1983);
values.insert(it, 1998);

하지만 const_iterator를 사용하면 여러가지 문제가 많이 생긴다.

typedef std::vector<int>::iterator IterT;                // typedef 들
typedef std::vector<int>::const_iterator ConstIterT;    // (C++98 코드이므로)

std::vector<int> values;

ConstIterT ci = std::find(static_cast<ConstIterT>(values.begin()), static_cast<ConstIterT>(values.end()), 1983);

values.insert(static_cast<IterT>(ci), 1998);    // 컴파일이 안 될 수 있음

std::find에서 캐스팅을 사용한 이유는 C++98에서 비상수 컨테이너로부터 상수 반복자를 얻을 수 있는 방법이 없기 때문이다.
insert에서 다시 비상수 반복자로 캐스팅을 한 이유는, 삽입(과 삭제) 함수가 오직 iterator만을 받아들이기 때문이다.
그런데 상수 반복자를 비상수 반복자로 이식성을 유지한 채 변환하는 방법 또한 존재하지 않는다(이는 C++11도 마찬가지이다). 즉 캐스팅이 안된다는 말이다. 이런 불편함 때문에 C++98에서는 const_iterator를 잘 사용하지 않았다.

 

하지만 C++11에서는 비상수 컨테이너로부터 const_iterator를 얻을 수 있는 멤버 함수가 추가되었다.
cbegin과 cend가 그것이다. 그리고 삽입(과 삭제) 함수가 const_iterator를 받아들일 수 있도록 바뀌었다.
이제 iterator를 사용하는 기존 C++98 코드를 C++11에서 const_iterator를 사용하도록 개정하는 과정은 아주 간단하다.

std::vector<int> values;
auto it = std::find(values.cbegin(), values.cend(), 1983);
values.insert(it, 1998);

이것이 바로 const_iterator를 사용하는 실용적인 코드이다.

 

- 최대한 이반적인 코드에서는 begin, end, rbeging 등의 비멤버 버전들을 해당 멤버 함수들보다 선호하라.

일반적인 코드를 작성할 때, begin, end, rbegin 등의 멤버 함수를 갖지 않은 컨테이너도 고려해야 하므로, 이 멤버 함수들의 비멤버 버전들을 사용하는 것이 좋다.

 

예를 들어, 다음은 앞에서 본 검색 및 삽입 코드를 findAndInsert라는 하나의 템플릿으로 일반화한 것이다.

template <typename C, typename V>
void findAndInsert라는(C& container, const V& targetVal, const V& insertVal)        
{                              

    // targetVal의 첫 출현을 찾고, 그 위치에 insertVal을 삽입한다.
    using std::cbegin;
    using std::cend;

    auto it = std::find(cbegin(container) /*비멤버 cbegin*/, cend(container) /*비멤버 cend*/, targetVal);

    container.insert(it, insertVal);
}

이 템플릿은 C++14에서는 잘 작동하지만, 안타깝게도 C++11에서는 그렇지 않다.
C++11 표준화 과정에서 비멤버 함수 begin과 end는 표준에 추가했지만, cbegin과 cend, rbegin, rend, crbegin, crend는 빼먹고 추가하지 않았기 때문이다.
C++11에서 이런 비멤버 함수들을 사용하고 싶다면, 여러분이 직접 구현하는 것도 어렵지 않다.

 

다음은 비멤버 cbegin의 한 구현이다.

template <class C>
auto cbegin(const C& container)->decltype(std::begin(container))
{
    return std::begin(container);
}

비멤버 cbegin이 멤버 cbegin을 호출하지 않는다는 점에 놀랄 수도 있다.

그러나 이는 논리적이다. 이 cbegin 템플릿은 컨테이너 같은 자료구조를 대표하는 임의의 인수 타입 C를 받고, 해당 const 참조 매개변수 container를 통해서 그 자료구조에 접근한다.
만일 C가 통상적인 컨테이너 타입이면 container는 그 컨테이너의 const 버전에 대한 참조가 된다.

그러한 const 컨테이너에 대해 비멤버 begin 함수를 호출하면 const_iterator 타입의 반복자가 반환된다.
이런 구현 방식의 장점은, begin 멤버 함수를 제공하지만 cbegin 멤버 함수는 제공하지 않는 컨테이너에 대해서도 작동한다는 것이다.

심지어 C가 내장 배열 타입일 때에도 작동한다.

C++11은 내장 배열에 특화된 버전의 비멤버 begin을 제공하기 때문이다.

댓글