항목 1 : 적재적소에 알맞은 컨테이너를 사용하자

◾ 표준 STL 시퀀스 컨테이너 : vector, string, deque, list

◾ 표준 STL 연관 컨테이너 : set, multiset, map, multimap

 

vector는 일반적, list는 시퀀스의 중간에 삽입/삭제가 빈번하게 일어날 때, deque는 시퀀스의 끝에서 삽입/삭제가 빈번하게 일어날 때 사용된다.

 

◾ 연속 메모리(배열 기반) 컨테이너 : vector, string, deque

요소가 삽입되거나 삭제될 때 발생하는 밀어내기로 인해 수행 성능이 떨어지거나 예외 안전성에 영향을 미친다.

 

◾ 노드 기반 컨테이너 : list 및 표준 연관 컨테이너

요소의 삽입이나 삭제가 이루어져도 밀어내기가 발생하지 않는다.

 

 

항목 2 : "컨테이너에 독립적인 코드"라는 환상을 조심하자

각 컨테이너를 모두 지원하는 코드 작성은 무의미한 행동이다. STL의 컨테이너는 서로 바꾸어서 쓸 수 있도록 설계되어 있지 않기 때문이다.

 

 

항목 3 : 복사는 컨테이너 안의 객체에 맞게 비용은 최소화하며, 동작은 정확하게 하자

컨테이너에 객체를 담을때는 원본이 아닌 복사본이 들어가고 객체를 꺼낼때는 참조자를 얻어서 복사되어 꺼내진다.

복사되어 들어가고, 복사되어 나오는 것이 STL의 방식이다.

배열 기반 컨테이너에 insert, erase, next_permutation 등의 함수를 호출하면 내부적으로 복사를 통해 위치가 바뀐다.

이 때 객체들의 복사는 해당 클래스의 복사 멤버 함수를 사용하게 된다.

상속된 객체의 경우 복사중에 데이터가 슬라이스 되는 경우도 발생한다.

 

객체 대신 포인터를 컨테이너에 저장하면 슬라이스도 발생하지 않고 성능 이점이 생긴다.

 

STL은 복사를 많이 하긴 하지만 불필요한 복사는 피하도록 설계되어있다.

 

Widget w[maxNumWidgets]; // maxNumWidgets만큼 기본 생성자 호출

vector<Widget> vw;
vw.reserve(maxNumWidgets); // maxNumWidgets만큼의 크기만 미리 할당

 

 

항목 4 : size()의 결과를 0과 비교할 생각이라면 차라리 empty를 호출하자

if (c.size() == 0)
if (c.empty())

두 코드는 본질적으로 같다.

하지만 empty는 모든 컨테이너에서 상수 시간에 수행되는 반면에 size는 선형 시간에 수행되는 컨테이너가 존재한다.

예를 들어 list의 size()와 splice()는 둘 중 하나가 반드시 선형 시간이어야 한다. 그리고 보통 splice가 상수 시간으로 구현되어있다.

 

 

항목 5 : 단일 요소를 단위로 동작하는 멤버 함수보다 요소의 범위를 단위로 동작하는 멤버 함수가 더 낫다

v1.assign(v2.begin() + v2.size() / 2, v2.end()); // v2의 뒤쪽 절반 복사

iterator를 사용한 범위 단위 동작이 아니라 단일 요소 단위로 같은 동작을 하려면 필연적으로 반복문을 사용할 수 밖에 없다.

범위 단위 멤버 함수가 코드도 더 간결해지고 가독성도 높아진다.

 

/* data: 배열, numValues: 배열크기 */
std::copy(data, data + numValues, std::inserter(v, v.begin()));

배열의 복사를 위와 같이 구현하는 경우 시퀀스 컨테이너의 성능에 있어서 발목을 붙잡는 요소들이 몇 가지 발생한다.

첫 번째, inserter는 배열 요소 개수만큼 호출된다.

두 번째, inserter가 호출될때마다 기존에 삽입된 v의 요소가 한칸씩 뒤로 계속 밀린다. (복사가 빈번하게 일어남) 범위 단위 멤버 함수인 insert라면 한 번에 마지막 위치까지 옮긴다.

세 번째, vector의 경우 복사되는 요소의 갯수가 많다면 크기 확장이 빈번하게 일어나서 메모리 할당의 횟수가 많아진다.

 

string, deque, list의 경우에는 조금씩 다르지만 어쨌든 공통적으로 범위 단위 insert가 성능면에서 우수하다.

 

연관 컨테이너에서는 단일 요소 버전과 범위 멤버 함수의 효율성을 따지기 어렵다.

다만 범위 멤버 함수는 간결한 코드와 가독성에 있어서 확실하게 이점이 있다.

 

/* 범위 생성 */
container::container(iterator begin, iterator end);

/* 범위 삽입 */
void container::insert(iterator position, iterator begin, iterator end);

모든 표준 컨테이너는 begin, end를 인자로 갖는 생성자와 동일한 형태의 insert 멤버 함수를 지원한다.

(추가: 책에서는 시퀀스 컨테이너와 연관 컨테이너의 erase 반환값이 달랐지만 모던 C++은 동일하다.)

 

void container::assign(iterator begin, iterator end);

모든 표준 시퀀스 컨테이너는 동일한 범위 버전의 assign 멤버 함수를 지원한다.

 

최종적으로 정리하자면 범위 멤버 함수는 작성이 쉽고, 가독성이 좋고, 성능도 월등히 좋다.

 

 

항목 6 : C++ 컴파일러의 어이없는 분석 결과를 조심하자

std::ifstream dataFile("ints.dat");
std::list<int> data(std::istream_iterator<int>(dataFile), std::istream_iterator<int>());

위의 코드는 컴파일은 되지만 런타임에 아무 일도 하지 않는다. 그 이유는 리스트가 아닌 함수의 선언으로 분석하기 때문이다. 반환타입이 std::list<int>이고 매개변수가 두개인 함수 data의 선언이다.

해결 방법은 익명 객체가 아니라 각 반복자 객체를 정의해서 인자로 넘겨주는 것이다.

 

std::ifstream dataFile("ints.dat");
std::istream_iterator<int> dataBegin(dataFile);
std::istream_iterator<int> dataEnd;
std::list<int> data(dataBegin, dataEnd);

익명 객체를 사용하지 않는 것은 프로그래밍 스타일에 적합하다고 볼 수 없지만 혼란을 주지 않기 위해서는 어쩔 수 없다.

 

 

항목 7 : new로 생성한 포인터의 컨테이너를 사용할 때에는 컨테이너가 소멸되기 전에 포인터를 delete하는 일을 잊지 말자

컨테이너는 자신이 소멸할 때 각 요소 자체를 없애주기는 하지만 요소가 포인터인 경우 포인터에 대해 delete를 수행하지 않기 때문에 소멸자가 호출되지 않는다.

 

struct DeleteObject {
    template<typename T>
    void operator()(const T* ptr) const { delete ptr; }
};

std::for_each(dssp.begin(), dssp.end(), DeleteObject()); // 자동으로 타입이 추론된다

이런 방식은 타입 안전성은 있지만 예외 안전성이 없기 때문에 스마트 포인터를 사용해 주어야 한다.

 

void doSomeThing() {
    typedef std::shared_ptr<Widget> SPW;
    std::vector<SPW> vwp;
    for (int i = 0; i < SOME_MAGIC_NUMBER; ++i)
        vwp.push_back(SPW(new Widget));
}

스마트 포인터는 유효 범위를 벗어날 때 참조 횟수가 줄어들고 0이 되면 소멸자를 호출하기 때문에 예외 안전성이 보장된다.

 

(추가: auto_ptr의 컨테이너는 사용하지 말라고 한다. 하지만 요즘은 auto_ptr이 삭제되었다.)

 

 

항목 8 : auto_ptr의 컨테이너는 절대로 만들지 말자

(추가: 우선 언급하자면 auto_ptr은 C++11부터 사용 자제 권고가 계속 내려졌고 C++17부터 아예 제거되었다.)

 

auto_ptr이 담긴 컨테이너는 컴파일 조차 되어서는 안된다. auto_ptr은 복사가 허용되지 않기 때문이다.

예를 들어 auto_ptr이 담긴 컨테이너를 정렬한다고 했을 때, sort함수 내부적으로 요소를 임시 지역 변수로 '복사' 하는 과정이 있다. 이 때 소유권이 이동되기 때문에 컨테이너의 값이 소실된다.

'도서 > Effective STL' 카테고리의 다른 글

반복자(Iterators)  (0) 2022.12.20
STL 연관 컨테이너 (2)  (0) 2022.12.20
STL 연관 컨테이너 (1)  (0) 2022.12.20
vector와 string  (0) 2022.12.18
효과적인 컨테이너 요리법 (2)  (0) 2022.12.16

+ Recent posts