- 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을 제공하기 때문이다.
'PROGRAMMING > Effective Modern C++' 카테고리의 다른 글
15. 가능하면 항상 constexpr을 사용하라. (0) | 2020.12.27 |
---|---|
14. 예외를 방출하지 않을 함수는 noexcept로 선언하라. (0) | 2020.12.27 |
12. 재정의 함수들을 override로 선언하라. (0) | 2020.12.26 |
11. 정의되지 않은 비공개 함수보다 삭제된 함수를 선호하라. (0) | 2020.12.25 |
10.범위 없는 enum보다 범위 있는 enum을 선호하라. (0) | 2020.12.25 |
댓글