1. size()의 결과를 0과 비교할 생각이라면 차라리 empty를 호출하자
- size()의 결과와 0을 비교하는 것은 empty()를 호출하는 것과 본질적으로 똑같습니다. 하지만 size()는 항상 상수 시간에 수행된다는 보장이 없고, empty()는 항상 상수 시간에 수행됩니다.이유는 list의 splice 함수와 밀접한 관련이 있습니다. splice 함수는 객체의 복사를 하지 않고, list의 특정 요소들을 다른 list로 옮길 수 있는 함수입니다.
list의 size()가 상수 시간에 수행되도록 하려면, list 객체 내에 리스트의 요소가 몇 개 존재하는지를 담아두는 멤버를 하나 준비하고, 요소의 수를 변경시킬 수 있는 모든 경우에 이 멤버 변수의 값을 갱신 해야 합니다.
물론 splice 함수도 마찬가지로 요소의 수를 담는 멤버 변수를 갱신 해야 하고, 이를 위해서 splice가 호출될 때마다 옮긴 요소의 수를 세어야 하므로, splice 함수가 상수 시간에 수행되도록 만들 수가 없게 됩니다.
여러 STL 제품에서 list를 다르게 구현해 놓고 있습니다. 예상하고 있겠지만 구현을 맡은 개발자가 size와 splice 중 어디에 비중을 두고 있는지에 따라, splice가 상수 시간에 수행되는 대신 size가 상수 시간에 수행되지 않을 수도 있고 그 반대일 수도 있습니다.
지금 쓰는 라이브러리의 list에서 size()가 상수 시간에 수행된다고 하더라도 나중에 라이브러리를 교체하거나, 다른 플랫폼으로 포팅할 일이 생길 지도 모릅니다. 그러므로 size()의 결과를 0과 비교하기보다는 empty()를 호출하는 것이 좋습니다.
2. 단일 요소를 단위로 동작하는 멤버 함수보다 요소의 범위를 단위로 동작하는 멤버 함수가 더 낫다
- 단일 요소를 단위로 동작하는 멤버 함수(이하 단일 요소 함수)보다 요소의 범위를 단위로 동작하는 멤버 함수(이하 범위 멤버 함수)가 성능 면에서 더 낫습니다. 만일 단일 요소 함수를 사용하여 여러 요소를 삽입하려면, 반복문을 사용할 수 밖에 없습니다. 반복문을 사용하여 요소를 하나씩 삽입하면 타자량도 많고, 가독성도 그다지 좋다고 할 수 없습니다.
- vector<Widget> v1, v2; // v1과 v2는 Widget을 담는 벡터라고
- // 가정합시다.
- ...
- // v1의 내용을 v2의 뒷쪽 반과
- // 똑같이 만드는 가장 빠른 방법은
- // 무엇일까요?
- // 정답은 assign을 사용하여
- // 다음과 같이 짧게 작성할 수 있습니다.
- v1.assign(v2.begin() + v2.size() / 2, v2.end());
- // 반복문을 사용하면
- v1.clear();
- for (vector<Widget>::const_iterator ci = v2.begin() + v2.size() / 2; ci != v2.end(); ++ci) {
- v1.push_back(*ci);
- }
- // 하지만 assign을 호출하는 것이
- // 손이 덜 간다는 것은 뻔합니다.
- // 루프를 피하는 한 가지 방법은
- // 알고리즘을 사용하는 것입니다.
- v1.clear();
- copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
- // 타자량이 훨씬 많이 줄긴 했지만,
- // assign을 사용한 예제보다는
- // 여전히 타자량이 많습니다
- // (약간이지만...).
- // 게다가 copy의 내부를 보면
- // 결국 루프를 사용하여 구현되어 있습니다.
- // 삽입 연산자(inserter, back_inserter,
- // front_inserter)를 사용해서 복사 대상
- // 범위를 지정하는 copy는 거의 모두
- // 범위 멤버 함수로 바꿀 수 있고,
- // 바꿔야 합니다.
- // 위의 copy는 insert의 범위 함수
- // 버전으로 대체할 수 있습니다.
- v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
- // copy는 복사의 의미에 초점이 맞춰진 반면,
- // insert는 삽입의 의미에 초점이 맞춰져
- // 좀 더 명확하게 의미 전달이 됩니다.
단일 요소 멤버 함수보다 범위 멤버 함수가 더 좋은 이유를 이미 두 가지나 확인했습니다.- 범위 멤버 함수를 사용한 코드가 대개 짧다(즉, 프로그래머의 손이 덜 아프다).
- 범위 멤버 함수는 훨씬 명확하고 간결한 의미를 전달한다.
- 만일 배열에 있는 데이터를 벡터의 앞 부분으로 옮긴다고 생각해 봅시다. 이 경우 단일 요소 함수를 쓰는 것 보다 범위 멤버 함수를 쓰는 것이 좋은 이유가 3 가지나 있습니다. 첫째, 단일 요소 함수는 배열의 멤버 수 만큼 호출되어야 하지만, 범위 멤버 함수는 딱 한 번만 호출되면 되므로 함수 호출 비용이 적게 듭니다. 물론 인라인 함수인 경우에는 차이가 없지만 모든 단일 요소 함수가 인라인인 것은 아닙니다.
- 둘째, 벡터에 들어 있던 기존의 데이터들을 미는 횟수에서 차이가 납니다. 단일 요소 함수는 배열의 요소들을 하나 씩 삽입하기 때문에 총 복사 횟수는 (배열의 총 요소 수) * (벡터에 들어 있던 요소의 수) 만큼이 됩니다. 반면 범위 멤버 함수의 경우 몇 개가 삽입되는지를 미리 알 수 있기 때문에, (벡터에 들어 있던 요소의 수) 만큼만 복사(딱 한번만 밉니다)가 일어납니다.
- 셋째, 메모리 할당에 관한 것입니다. 대부분의 경우 벡터는 메모리가 꽉 찰 때마다 자신의 용량을 두 배로 늘리도록 구현이 되어 있습니다. 즉 n개의 새 데이터 요소를 하나씩 삽입하려고 하면 메모리 할당을 log2n번이나 하게 되는 셈입니다. 반면 범위 멤버 함수를 쓰면 삽입할 요소의 수를 미리 알 수 있으므로 딱 한 번 필요한 메모리를 할당하면 됩니다.
- vector에 대해서 설명드린 내용은 string에서도 동일하게 적용됩니다. deque의 경우에는 비슷하긴 하지만 vector나 string과는 다른 메모리 관리 방식을 취하고 있어서 "반복적인 메모리 재할당"에 관한 이야기는 맞지 않습니다. 하지만 불필요하게 빈번한 컨테이너 내 요소의 이동이나, 불필요한 함수 호출에 관한 이야기는 일반적으로 맞습니다.
- list 역시, 범위 멤버 함수가 단일 요소 함수보다 수행 성능에서 우수합니다. 되풀이되는 함수 호출에 있어서는 역시 범위 버전이 좋습니다. 하지만 list는 노드 기반으로 동작하기 때문에 메모리 할당에 관한 사항은 딱 맞지 않습니다. 그 대신에 리스트의 노드를 연결하는 next 포인터와 prev 포인터 값이 불필요하게 되풀이해서 세팅되는 문제가 생깁니다.
- 최소한 표준 시퀸스 컨테이너에 대해서는, 단일 요소 버전의 삽입이냐, 범위 버전의 삽입이냐를 선택하는데 있어서 "프로그래밍 스타일"을 압도하는 많은 요인들을 내새울 수 있게 되었습니다. 그렇다면 연관 컨테이너에 대해서는 어떨까요? 단일 요소 버전의 insert에서 여전히 반복 함수 호출의 오버헤드가 있긴 하지만 딱 부러지게 효율이 어떻다라고는 말씀드리기 힘듭니다. 게다가 몇 가지 특수한 종류의 범위 삽입 함수들의 최적화의 여지를 가지고 있지만 이론적으로만 이러한 최적화가 존재합니다.하지만 연관 컨테이너에서 범위 멤버 함수를 쓴다고 해서 효율이 뒤진다든지 하는 것은 없으니 지금 쓰셔도 잃는 것은 없습니다.
굳이 효율 문제가 아니더라도, 타자수를 줄여주고 나중에 읽기도 편해 이해하기 좋기 때문에 연관 컨테이너에서도 범위 멤버 함수를 쓰는 것이 좋습니다. - 범위를 지원하는 멤버 함수는 어떤 것인지 미리 알아놓고 정리해 두면, 나중에 이것들을 사용할 기회를 포착하기가 매우 쉬울 것입니다.
- // 다음에 나온 시그너쳐(signature)에서,
- // 매개 변수 타입인 iterator는 말 그대로
- // 컨테이너의 반복자 타입, 즉 container::iterator
- // 란 뜻입니다.
- // 한편 InputIterator는 어떤 입력
- // 반복자도 받아들일 수 있다는 뜻입니다.
- //-----------------------------------------------------------------------------
- // ※ 범위 생성(Range Construction)
- // 모든 표준 컨테이너는 다음과 같은
- // 형태의 생성자를 지원하고 있습니다.
- container::container(InputIterator begin, // 범위의 시작
- InputIterator end); // 범위의 끝
- // 이 생성자에 넘겨진 반복자가
- // istream_iterator 혹은
- // istreambuf_iterator이면
- // C++에서만
- // 볼 수 있는 가장 황당한 분석
- // 결과(parse)가 생깁니다.
- // 이것 때문에 컴파일러는 이것을
- // 컨테이너 객체의 정의로 보지 않고
- // 함수 선언으로 이해하고 말지요.
- //-----------------------------------------------------------------------------
- // ※ 범위 삽입(Range Insertion)
- // 모든 표준 컨테이너는 다음과 같은
- // 형태의 insert를 지원하고 있습니다.
- void container::insert(iterator position, // 범위를 삽입할 위치
- InputIterator begin, // 삽입할 범위의 시작
- InputIterator end); // 삽입할 범위의 끝
- // 연관 컨테이너는 자신이 가지고 있는
- // 비교 함수를 사용하여 삽입될 요소가
- // 놓일 위치를 결정하기 때문에 위치
- // 매개 변수를 가지고 있지 않은
- // 시그너쳐를 제공합니다.
- void container::insert(InputIterator begin, InputIterator end);
- // 단일 요소 버전의 insert를
- // 범위 버전으로 교체할 부분을 찾을 때
- // 잊지 말아야 할 것이 있습니다.
- // 바로 단일 요소 함수 중에 몇몇은
- // 다른 함수 이름으로 위장하고
- // 있다는 사실입니다.
- // 예를 들어, push_front와
- // push_back은 하나의 요소를
- // 컨테이너에 넣는 함수이지만 "삽입"
- // 류로 불리지 않지요.
- //-----------------------------------------------------------------------------
- // ※ 범위 삭제(Range Erasure)
- // 역시 표준 컨테이너에서 범위 버전의
- // erase를 제공하고 있지만, 반환 타입은
- // 시퀸스 컨테이너와 연관 컨테이너에
- // 대해서 각각 다릅니다.
- // 시퀸스 컨테이너에선 다음과 같은
- // 형태를 쓸 수 있고,
- iterator container::erase(iterator begin, iterator end);
- // 반면에 연관 컨테이너에서는 다음과
- // 같은 형태를 쓸 수 있습니다.
- void container::erase(iterator begin, iterator end);
- // 반환 타입이 다른 이유는
- // 연관 컨테이너 버전의 erase에서
- // 지워진 요소의 바로 뒤에 있는
- // 요소를 가리키는 반복자를
- // 반화하게 하면 납득하기 힘든
- // 수행 성능 저하가 생길 수 있다고
- // 합니다.
- // vector와 string의 insert에 대한 이야기가
- // erase에서 통하지 않는 부분은 반복되는
- // 메모리 할당에 있습니다.
- // vector와 string의 메모리는
- // 새 데이터 요소를 넣을 때에는 자동으로
- // 커지지만 내부의 요소 수가 줄어들
- // 때에는 자동으로 작아지지 않기
- // 때문입니다
- //-----------------------------------------------------------------------------
- // ※ 범위 대입(Range Assignment)
- // 모든 표준 시퀸스 컨테이너는
- // 범위 버전의 assign을 제공하고
- // 있습니다.
- void container::assign(InputIterator begin, InputIterator end);
참조 : http://ajwmain.iptime.org/programming/book_summary/%5B02%5Deffective_stl/effective_stl.html#I04
Effective STL 정리
매우 긴 문자열을 담을 수 있는 string 류의 컨테이너(string-like container for very large strings) 이 컨테이너는 rope(로프) 라고 불립니다. SGI는 로프를 이렇게 규정하고 있습니다. 로프는 확장성을 갖춘 문
ajwmain.iptime.org
'언어공부 > C++' 카테고리의 다른 글
[ C++ ] STL 정리하기 1. 컨테이너의 종류와 선택팁 (0) | 2022.07.28 |
---|---|
[ C++ ] 기초정리 8. 객체와 클래스 (0) | 2022.07.19 |
[ C++ ] 기초정리 6. 함수 (0) | 2022.07.19 |
[ C++ ] 기초정리 5. 논리적 자료 표현 - 구조체 (0) | 2022.07.19 |
[ C++ ] 기초정리 4. 고급 변수 사용 (0) | 2022.07.18 |