[Modern C++] 항목 13: iterator보다 const_iterator를 선호하라

게시:     수정

카테고리:

태그: ,

이 글은 아래의 책을 정리하였습니다. 이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역

가독성이 떨어지는 직역들을 수정하며 정리하였습니다. e.g. 연역 → 추론, 중복적재 → 오버로딩

📦 3. 현대적 C++에 적응하기

👉🏻 항목 13: iterator보다 const_iterator를 선호하라

🔍 const_iterator란?

  • const_iteratorconst를 가리키는 포인터의 STL 버전이다. 가리키는 값을 수정할 수 없다.
  • “가능한 한 항상 const를 사용하라”는 원칙은 반복자에도 동일하게 적용된다.
    • 반복자가 가리키는 것을 수정할 필요가 없을 때에는 항상 const_iterator를 사용하는 것이 바람직하다.

⚠️ C++98에서의 문제점

C++98에서는 const_iterator로 이 원칙을 지키는 것이 불편했다.

std::vector<int> values;
...
std::vector<int>::iterator it =
  std::find(values.begin(), values.end(), 1983);
values.insert(it, 1998);
  • 이 코드는 iterator가 가리키는 것을 전혀 수정하지 않으므로, const_iterator를 써야 마땅하다.
  • 그러나 C++98에서는 비const 컨테이너로부터 const_iterator를 얻는 간단한 방법이 없었다.
// C++98 방식 (개념적으로는 맞지만 실제로는 불편함)
typedef std::vector<int>::iterator IterT;
typedef std::vector<int>::const_iterator ConstIterT;

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); // 컴파일 안 될 수 있음
  • C++98에서는 삽입/삭제 위치를 iterator로만 지정할 수 있었고, const_iterator는 허용되지 않았다.
  • 심지어 const_iteratoriterator로 캐스팅하는 이식성 있는 방법도 존재하지 않았다.
  • 결론: C++98에서 const_iterator는 너무 불편해서 대부분의 경우 그냥 쓰지 않는 것이 낫다는 분위기였다.

✅ C++11에서의 개선

C++11에서는 const_iterator를 얻기도 쉽고 사용하기도 쉬워졌다.

std::vector<int> values;
...
auto it =
  std::find(values.cbegin(), values.cend(), 1983); // cbegin과 cend를 사용

values.insert(it, 1998);
  • 컨테이너 멤버 함수 cbegin()cend()const 컨테이너에서도 const_iterator를 돌려준다.
  • STL 멤버 함수들(insert, erase 등)도 이제 실제로 const_iterator를 사용한다.
  • 이것이 바로 const_iterator를 사용하는 실용적인 C++11 코드다.

📋 최대한 일반적인 코드 작성 (C++14)

일반성을 극대화한 라이브러리 코드를 작성할 때는, 멤버 함수 대신 비멤버 함수 버전을 사용하는 것이 좋다. 컨테이너뿐만 아니라 배열, 서드파티 라이브러리 등에도 동작하기 때문이다.

template<typename C, typename V>
void findAndInsert(C& container,
                   const V& targetVal,
                   const V& 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에서는 비멤버 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을 호출하지 않는 것이 의아할 수 있지만, 이는 논리적이다.
    • 타입 Cstd::vector<int>라면, containerconst std::vector<int>&가 된다.
    • const 컨테이너에 비멤버 begin을 호출하면 const_iterator 타입의 반복자가 반환된다.
  • begin 멤버 함수는 제공하지만 cbegin 멤버 함수는 제공하지 않는 컨테이너에도 동작하는 장점이 있다.
  • C가 내장 배열 타입일 때에도 작동한다.

🧐 정리

  • iterator보다 const_iterator를 선호하라.
  • 최대한 일반적인 코드에서는 begin, end, cbegin, cend, rbegin 등의 비멤버 버전을 멤버 함수보다 선호하라.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기