2015. 8. 27. 20:32ㆍ프로그래밍/Effective STL
돌발퀴즈!
두 개의 백터 v1과 v2가 있다. v1의 내용을 v2의 뒤쪽 반과 똑같이 만드는 가장 빠른 방법이 뭘까?
답
1 | v1.assign(v2.begin() + v2.size()/2, v2.end()); | cs |
퀴즈의 의미
첫번째 의미는 assign이라는 멤버 함수가 있다는 사실을 알려주기 위해. 아주 편리한 녀석!
- 표준 시퀀스 컨테이너라면 모두 사용 가능(vector, string, deque, list)
- 프로그래머들은 컨테이너의 내용을 완전히 교체하고 싶다라고 생각하면 먼저 떠오르는건 대입(assignment)
- 컨테이너에 어떤 값의 집합을 한꺼번에 채우려 할 때 operator=가 원하는 대로 동작하지 않는다.
두 번째 의미는 제목의 실 예를 보여주는 것.
왜 범위 멤버 함수가 더 좋은가?
- 범위 멤버 함수를 사용한 코드가 대개 짧다.(손이 덜 아프다.)
- 범위 멤버 함수는 훨씬 명확하고 간결한 의미를 전달한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include <iostream> #include <vector> #include <iterator> const int numValue = 5; void main() { int data[numValue] = {0, 1, 2, 3, 4}; std::vector<int> v; std::vector<int>::iterator insertLoc(v.begin()); /* 루프를 직접 사용한 단일 요소단위로 동작 for (int i = 0; i < numValue; i++) { insertLoc = v.insert(insertLoc, data[i]); } */ // 요소의 범위 단위로 동작 std::copy(data, data+numValue,std::inserter(v, v.begin())); // 출력 for (int i : v) { std::cout<<v.at(i); } } | cs |
범위 단위를 사용했을 때 장점(3대 요인)
첫째는 불필요한 함수 호출을 줄인다.
- 예제에서 for루프를 돌면 numValue만큼 돌지만 std::copy는 이거 한번만 부르면 된다.
- insert가 인라인 함수로 되어있다면 괜찮지만 안 그런 경우도 존재한다.
둘째는 땡김처리로 인한 부하를 막을 수 있다.
- 삽입시 n*numValue번의 이동이 일어난다.
- 객체를 담았다고 가정하면 대입 연산자나 복사 생성자가 호출된다. (대부분 복사생성자 호출, 마지막 요소는 복사생성자 호출)
- 새 객체를 하나씩 넣는다면 n*numValue번의 호출과 (n-1)*numValue번의 객체의 대입 연산자,
numValue번의 복사 생성자가 호출된다.
- insert는 컨테이너 안에 들어 있는 요소를 마지막 위치까지 바로 옮기도록 되어있다.
셋째는 메모리 할당에 관한 것이다.
- 단일 요소 루프를 이용하면 numValue개의 새 데이터 요소를 삽입하기 위해 메모리 할당을 log2numValue번 하게된다.
- 범위 버전은 여러번 수행하지 않는다.(크기를 예상할 수 있으므로)
모든 시퀀스 컨테이너들이 이런가?
- vector와 string은 동일한 양상을 보인다.
- deque의 경우는 비슷하긴 하지만 메모리 관리 방식이 조금 달라 '반복적인 메모리 재할당'에 관해선 조금 다르다.
- list도 범위 버전이 성능이 우수하다. 단일 버전은 next와 prev가 새 노드가 추가 될 때마다 다음과 이전 노드를 업데이트 하는 비용이 든다. 하지만 범위 버전은 해당 노드가 몇 개인지 알 수 있으므로 이 비용이 들지 않는다.
범위를 지원하는 멤버 함수는 어떤 것이 있는지 정리해보자!
용어
iterator는 컨테이너의 반복자 타입, container::iterator.
InputIterator는 어떤 입력 반복자도 받아들일 수 있다는 뜻.
범위 생성(Range Construction)
- 모든 표준 컨테이너는 다음과 같은 형태의 생성자를 지원한다.
1 | container::container(InputIterator begin, InputIterator end); // range begin, range end | cs |
*해당 생성자에 넘겨진 반복자가 istream_iterator 이거나 istreambuf_iterator이면 C++에서만 볼 수 있는 황당한 분석 결과가 생긴다. 컴파일러는 이것을 컨테이너 객체의 정의로 보지 않고 함수 선언으로 이해한다. (항목 6에서 자세히 다룬다.)
범위 삽입(Range Insertion)
- 모든 표준 컨테이너는 다음과 같은 형태의 insert를 지원하고 있다.
1 | void container::insert(iterator position, InputIterator begin, InputIterator end); | cs |
연관 컨테이너의 경우 자신이 가지고 있는 비교 함수를 사용한다.
삽입될 요소가 놓일 위치를 결정하기 때문에, 위치 매개 변수를 가지고 있지 않은 시그니쳐를 제공한다.
1 | void container::insert(InputIterator begin, InputIterator end); | cs |
push_front, push_back을 호출하는 루프, front_inserter, back_inserter를 매개 변수로 받는 copy는
주저하지 말고 insert 버전으로 바꾼다!
범위 삭제(Range Erasure)
- 표준 컨테이너에서 범위 버전의 erase를 제공한다. 그러나 반환 타입은 시퀀스와 연관이 각각 다르다.
1 2 3 | iterator container::erase(iterator begin, iterator end); // sequence void container::erase(iterator begin, iterator end); // associateve | cs |
다른 이유?
연관 컨테이너는 erase에서 반복자를 반환하게 하면 납득하기 힘든 수행 성능 저하가 생긴다.(이 점은 의견이 분분)
- erase도 단일 요소 버전이 범위 버전 보다 여전히 호출 회수가 많다.
- 단일 버전은 erase를 사용하면 내부 데이터 요소가 한 위치씩 마지막 위치까지 이동한다.
- 범위 버전은 erase를 한 번의 이동으로 마지막 위치까지 옮겨 놓는다.
범위 대입(Range Assignment)
- 모든 표준 시퀀스 컨테이너는 범위 버전의 assign을 제공한다.
1 | void container::assign(InputIterator begin, InputIterator end); | cs |
'프로그래밍 > Effective STL' 카테고리의 다른 글
[effective STL] 항목 08 : auto_ptr의 컨테이너는 절대 말들지 말기 (0) | 2015.09.06 |
---|---|
[effective STL] 항목 07 : 포인터를 컨테이너 담을 때 주의 (0) | 2015.09.06 |
[effective STL] 항목 06 : C++ 컴파일러의 어이없는 컴파일 조심! (2) | 2015.09.05 |
size() == 0? empty()!! (effective STL 04) (0) | 2015.08.25 |
복사는 컨테이너 안의 객체에 맞게 비용 최소화, 동작은 정확히(effective STL 03) (0) | 2015.08.25 |
컨테이너 독립적인코드라는 환상 조심!(effective STL 02) (0) | 2015.08.25 |
적재적소 알맞은 컨테이너(effective STL 01) (2) | 2015.08.25 |