Item 13. iterator보다 const_iterator를 선호해라

C++에서는 값을 변경할 의도가 없는 변수에 가능한 const를 붙여서 코드의 안정성을 높이는 방식을 권유한다.

이는 컨테이너를 가리키는 iterator에서도 마찬가지이다.


const_iterator

const_iterator는 가리키는 원소의 상수성(constness)을 보장하는 iterator이다.

const_iterator를 사용하면 const_iterator가 가리키는 원소의 값을 변경할 수 없다.

즉, const T* 의 STL 버전이라고 볼 수 있다.

std::vector<int> v1 = {1};
std::vector<int>::iterator it1 = v1.begin();
*it1 = 10; // 가능, iterator가 가리키는 vector의 원소값을 변경

std::vector<int> v2 = {1};
std::vector<int>::const_iterator it2 = v2.begin();
*it2 = 10; // 불가능, const_iterator가 가리키는 vector의 원소값을 변경할 수 없음


C++98에서 const_iterator를 사용하기 힘든 이유

C++98에서는 const_iterator를 획득하고 활용하기 쉽지 않았다.

vector 컨테이너에서 특정한 원소를 찾아서 그 위치에 새로운 원소를 삽입하는 코드를 구현해보자.

std::vector<int> values;

// values의 원소값이 1983인 원소를 찾아서 가리키는 iterator 생성 (없으면 value.end()를 가리키고 있음) 
std::vector<int>::iterator it = std::find(values.begin(), values.end(), 1983);

// iterator가 가리키는 위치에 새로운 원소 삽입, 만약 찾은 원소가 없었으면 맨 뒤에 삽입
values.insert(it, 1998);

다음과 같은 상황에서는 iterator가 가리키는 원소를 수정하지 않으므로 const_iterator를 쓰는 것이 바람직하다.

// 타입 이름이 너무 기니까 C++98 스타일로 타입 별칭 생성
typedef std::vector<int>::iterator IterT;
typedef std::vector<int>::const_iterator ConstIterT;

std::vector<int> values;

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

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

하지만 막상 C++98에서 const_iterator를 사용하면 다음과 같은 제약들이 존재한다.

const가 아닌 컨테이너에서 const_iterator를 획득할 수 없음

C++98에서는 begin(), end() 함수를 호출한 컨테이너가 const가 아닐 경우 const_iterator를 획득할 수 없다.

따라서 다음과 같이 iteratorconst_iterator로 형변환해서 사용해야한다.

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

const가 아닌 컨테이너에 const_iterator를 삽입할 수 없음

C++98에서 컨테이너는 삽입, 삭제 위치를 오로지 iterator로만 지정할 수 있었다.

따라서 다음과 같이 const_iteratoriterator로 형변환해서 사용해야한다.

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

하지만 이렇게 형변환한 코드조차 제대로 컴파일이 안될 수 있다.

const_iterator에서 iterator로 형변환이 불가능함

C++98에서는 const_iterator에서 iterator로 이식성 있는 변환을 허용하지 않는다. (이는 C++11에서도 마찬가지이다.)

즉, C++98에서 아무리 const_iterator를 활용하려고 해봤자 고생만 할 뿐 제대로 사용할 수 없다.


C++11부터 const_iterator를 사용하는 방법

C++11부터는 const_iterator를 획득하기도 쉽고 활용하기도 쉬워졌다.

컨테이너 멤버 함수인 cbegin(), cend()const_iterator를 반환할 수 있고 const가 아닌 컨테이너에서 삽입, 삭제 위치를 const_iterator로 지정할 수 있게 되었다.

std::vector<int> values;

// const_iterator를 반환하는 cegin, cend 멤버 함수 사용
auto it = std::find(values.cbegin(), values.cend(), 1983);

// const가 아닌 컨테이너를 const_iterator로 위치를 지정할 수 있음
values.insert(it, 1998);


템플릿에서 const_iterator를 사용할 때 주의사항

템플릿을 통해 보편적인(general) 상황에서 모두 사용할 수 있는 코드를 구현하고 싶을 때 주의해야 할 점이 있다.

바로 iterator를 반환하는 함수(begin, end, cbegin, cend, rbegin 등)를 멤버 함수가 아닌 비멤버 함수로 사용하는 것이 좋다는 점이다.

그 이유는 모든 객체가 iterator를 반환하는 멤버 함수를 가지고 있다는 보장을 할 수 없기 때문이다.

앞서 말한 내용을 바탕으로 컨테이너에서 특정한 원소를 찾아서 그 위치에 새로운 원소를 삽입하는 템플릿 코드를 구현해보자.

template <typename C, typename V>
void findAndInsert(C& container, const V& targetVal, const V& insertVal) {
  using std::cbegin;
  using std::cend;

  // 비 멤버 함수인 cbegin, cend를 사용
  auto it = std::find(cbegin(container), cend(container), targetVal);

  container.insert(it, insertVal);
} 

이 템플릿은 C++14에서는 잘 동작하지만 C++11에서는 동작하지 않는다.

C++11에서는 비 멤버 함수로 begin, end는 추가했지만 cbegin, cend, rbegin, rend, crbegin, crend는 추가되지 않았다.

따라서 C++11에서 템플릿 코드가 동작하려면 다음과 같은 코드를 추가해야 한다.

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

컨테이너 타입 Cconst C&로 참조할 경우 begin 함수는 const_iterator 타입을 반환한다.

이를 통해 비 멤버 함수로 begin 함수만 제공해도 cbegin 함수를 사용할 수 있다.

해당 템플릿은 컨테이너 타입 C가 내장 배열 타입이어도 동작한다.


카테고리:

업데이트: