맴버 함수는 단일 요소 단위 보단 요소의 범위 단위로 (effective STL 05)

2015. 8. 27. 20:32프로그래밍/Effective STL

728x90
728x90

돌발퀴즈!

두 개의 백터 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] = {01234};
 
    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


728x90
반응형