[EMC++] Item 13. iterator보다 const_iterator를 선호해라
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
를 획득할 수 없다.
따라서 다음과 같이 iterator
를 const_iterator
로 형변환해서 사용해야한다.
ConstIter ci = std::find(static_cast<ConstIterT>(values.begin(),
static_cast<ConstIterT>(value.end()),
1983);
const가 아닌 컨테이너에 const_iterator를 삽입할 수 없음
C++98에서 컨테이너는 삽입, 삭제 위치를 오로지 iterator
로만 지정할 수 있었다.
따라서 다음과 같이 const_iterator
를 iterator
로 형변환해서 사용해야한다.
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);
}
컨테이너 타입 C
를 const C&
로 참조할 경우 begin
함수는 const_iterator
타입을 반환한다.
이를 통해 비 멤버 함수로 begin
함수만 제공해도 cbegin
함수를 사용할 수 있다.
해당 템플릿은 컨테이너 타입
C
가 내장 배열 타입이어도 동작한다.