일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- operator new
- 반복자
- effective stl
- UE4
- 예외
- more effective c++
- c++
- 암시적 변환
- 영화
- 영화 리뷰
- Smart Pointer
- reference
- 스마트 포인터
- 게임
- virtual function
- implicit conversion
- 언리얼
- Vector
- Effective c++
- 티스토리챌린지
- 루아
- 오블완
- 다형성
- 비교 함수 객체
- 상속
- lua
- resource management class
- exception
- 참조자
- 메타테이블
Archives
- Today
- Total
스토리텔링 개발자
[Effective STL] 5. 범위 멤버 함수(Range Member Function) 본문
728x90
v1에 v2의 뒤쪽 반을 채우는 가장 빠른 방법은?
v1.assign(v2.begin() + v2.size() / 2, v2.end());
- assign 멤버 함수
- 컨테이너의 대용을 완전히 교체하고 싶다면 이 함수를 우선적으로 고려하자.
- 이는 범위(두 반복자를 사용하여 나타내는 요소 묶음) 단위로 동작하는 멤버 함수이다.
- 범위 멤버 함수를 사용하지 않고 이 문제를 풀려면 루프를 사용하지 않을 수가 없다.
- 직접 루프를 사용하는 건 지양하자. (항목 43 참조)
범위 멤버 함수(range member function)
- 두 개의 반복자를 시작과 끝으로 하는 범위를 매개변수로 하는 멤버 함수를 말한다.
다른 해결법들
- 다른 방법 1. 루프와 단일 요소를 사용한 코드
vector<Widget> v1, v2;
...
v1.clear();
for(vector<Widget>::const_iterator ci = v2.begin() + v2.size() / 2; ci != v2.end())
{
v1.push_back(*ci);
}
- 다른 방법 2. 알고리즘 함수 사용 (항목 43 참조)
v1.clear();
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
// back_inserter(반복자 어댑터)는 아래에서 추가 설명
- 아무래도 이들은 기존 assign 방식에 비해 타자량이 많다.
- 사실, 사용한 방법들은 모두 루프를 사용한다.
- assign이나 copy 역시 내부적으로는 루프가 숨어있는 방법이다.(항목 43 참조)
반복자 어댑터
- copy에서는 각 요소를 직접 operator=로 삽입하는데, 뒤에 이어서 삽입하려면 다음처럼 귀찮은 과정을 거쳐야 한다.
// v1에 v2의 뒷쪽 절반을 추가한다.
v1.clear();
v1.resize(v2.size() / 2); // 필요한 만큼 공간 확보
copy(v2.begin() + v2.size() / 2,
v2.end(),
v1.begin() // 추가를 시작할 반복자 위치
);
// 헌데, 기존 벡터에 이어서 추가하고자 하면?
// 아래처럼 코드가 복잡해진다.
v1.resize(v1.size() + (v2.size() / 2)); // 미리 추가 공간을 확보해야 한다.
copy(v2.begin() + v2.size() / 2,
v2.end(),
v1.end() - (v2.size() / 2) // 추가를 시작할 반복자 위치
);
- 각각 copy 함수의 용법이 다르다.
- 하지만 반복자 어댑터를 쓰면 위 두 상황 모두 이 코드로 정리된다.
copy(v2.begin() + v2.size() / 2, v2.end(), back_inserter(v1));
- 반복자 어댑터의 종류
- inserter : 컨테이너의 insert 함수를 사용하여 요소 추가
- back_inserter : 컨테이너의 push_back 함수를 사용하여 요소 추가
- front_inserter : 컨테이너의 push_front 함수를 사용하여 요소 추가
- 사실, 반복자 어댑터를 사용하는 복사 대상 범위 지정 copy는 범위 멤버 함수로 바꿀 수 있다.(되도록 바꾸자.)
v1.insert(v1.end(), v2.begin() + v2.size() / 2, v2.end());
- 바꾼 코드의 장점
- copy에 비해 v1에 insert 하고 있다는 의미가 분명해진다.
단일 요소 로직보다 범위 멤버 함수가 더 좋은 이유
- 범위 멤버 함수를 사용한 코드가 대개 짧다.
- 범위 멤버 함수가 훨씬 명확하고 간결하다. 이해가 쉽다.
- 범위 멤버 함수의 성능이 더 좋다.
int 배열을 vector<int>의 앞으로 복사해보자
- 방법 1. vector의 범위 버전 insert를 사용하면 쉽게 해결된다.
int data[numValues];
vector<int> v;
...
v.insert(v.begin(), data, data + numValues); // data를 v 앞에 삽입
- 방법 2. 루프 및 단일 요소로 구현하면 아래와 같을 것이다.
vector<int>::iterator insertLoc(v.begin());
for(int i = 0 i < numValues; ++i)
{
insertLoc = v.insert(insertLoc, data[i]);
}
- 위 코드에서 insertLoc이 제대로 갱신되지 않으면 생기는 문제
- 루프의 가장 처음에만 제대로 동작했을 것이다.
- insert로 요소가 삽입되고 나면 기존 반복자는 무효화 되기 때문이다.
- 만일 무효화되지 않았더라도, 매번 v.begin() 위치에 값을 삽입하므로 data가 역순으로 삽입될 것이다.
- 루프의 가장 처음에만 제대로 동작했을 것이다.
- 방법 3. 위 코드의 루프를 copy로 바꾸면 아래와 같을 것이다.(항목 43 참조)
copy(data, data + numValues, inserter(v, v.begin()));
루프 및 단일 요소로 구현된 로직(방법 2)의 비효율 3가지
- 불필요한 함수 호출
- v에 numValue개의 요소를 한 번에 하나씩 넣는다.
- insert가 numValue번 불린다는 의미이다.
- 물론 인라인 함수라면 생기지 않을 비용이지만..
- 호출 함수는 인라인 일수도, 아닐 수도 있다.
- v에 numValue개의 요소를 한 번에 하나씩 넣는다.
- 새로운 요소를 넣기 위해 v에 들어 있던 요소들을 뒤로 미는 비용
- 새 값을 넣기 위해 요소마다 한 자리씩 뒤로 민다.
- 이 예제는 int이므로 memmove(메모리를 큰 단위로 옮기는 함수)가 호출되는 것으로 마무리될 수 있지만..
- v가 복잡한 객체를 담고 있다면 이동 시마다 매번 대입 연산자, 복사 생성자가 호출될 것이다.
- 반면 표준안에 따르면 범위 기반 insert는 내부 요소를 마지막 위치로 한번에 옮기도록 되어 있다.
- 사실, 모든 상황의 범위 기반 insert에 사실이라곤 할 수 없다.
- 두 반복자의 위치를 잃지 않고 그 사이 거리를 알아낼 수 있어야 하는데..
- 이는 모든 순방향 반복자에서 지원되는 기능이긴 하지만,
- 입, 출력 반복자는 순방향 반복자를 지원하지 않는다.
- 즉, insert에 입력 반복자(예컨대 istream_iterator) 같은 게 넘겨졌을 때는 거짓인 셈.
- 사실, 모든 상황의 범위 기반 insert에 사실이라곤 할 수 없다.
- 벡터의 메모리 할당
- 메모리가 꽉 찬 벡터에 새 요소를 넣으려고 하면
- 벡터는 메모리가 두 배가 되도록 재할당된다.(항목 14 참조)
- 만약 numValue개의 새 요소를 삽입하려고 하면 log₂numValue 번 하게 된다는 의미이다.
- 하지만 범위 버전 삽입 함수는 삽입 전에 필요한 메모리량을 예측할 수 있으므로 메모리 할당을 여러번 반복할 필요가 없다.
- 메모리가 꽉 찬 벡터에 새 요소를 넣으려고 하면
표준 시퀀스 컨테이너 전체로 보자면?
- 위에서는 vector가 예시였지만 string도 완전히 동일하다.
- deque는 메모리 재할당 외 나머지는 일반적으로 맞다.
- list도 불필요한 함수 호출 부분만 맞다.
- 허나, 뒤로 미는 비용 대신 노드의 포인터 설정에서 비용이 발생한다.
list에서 insert 시 노드 포인터 설정 비용
- 리스트에 요소가 추가되면..
- 새 노드의 next 포인터와 prev 포인터를 갱신하여 앞 뒤 노드를 설정한다.
- 앞 노드는 next 포인터를, 뒤 노드는 prev 포인터를 갱신한다.
- insert 함수로 단일 요소를 한번에 하나씩 넣는다면?
- 가장 마지막에 삽입하는 노드 외의 노드들은 계속해서 next 포인터를 업데이트 해야 한다.
- 처음 삽입 시, 노드 A를 가리키는 포인터가 되었다가
- 다음 노드를 삽입하면 이전에 삽입한 이 노드의 next는, 새 노드를 가리키도록 갱신되어야 하므로.
- 노드 A 역시 prev 포인터를 끊임없이 갱신해줘야 한다.
- 새로 들어오는 노드의 포인터로 계속해서 갱신되어야 한다.
- 즉, 한번만 설정하면 될 포인터(next 포인터)가 두 번씩 업데이트 된다.
- 가장 마지막에 삽입하는 노드 외의 노드들은 계속해서 next 포인터를 업데이트 해야 한다.
- 삽입된 노드의 next 포인터와 A의 prev 포인터가 각각 (추가하는 요소 갯수 - 1) 번이나 쓸데없이 갱신된다.
- 그러므로 list의 범위 버전 insert를 사용하는 것이 좋다.
- 이 함수는 최종적으로 삽입될 노드의 개수를 파악할 수 있으므로 불필요한 포인터 세팅을 피한다.
연관 컨테이너에서는 어떨까?
- 반복 함수 호출 오버헤드는 분명히 있다.
- 하지만, 다른 효율적 부분은 딱 부러지게 그렇다고 말하기 힘든 부분이 있다.
범위 버전 컨테이너 함수 정리
- 참고
- 매개 변수 타입 iterator는 컨테이너의 반복자를 뜻한다.
- InputIteractor는 어떤 입력 반복자도 받을 수 있다는 뜻이다.
- 범위 생성(Range Construction)
- 모든 표준 컨테이너는 다음 형태의 생성자를 지원한다.
-
container::container(InputIteractor begin, InputIterator end);
- 여기에 istream_iteractor나 istreambuf_iterator를 넘기면 컴파일러가 잘못 해석한다.(항목 29 참조)
- 컨테이너 객체의 정의가 아니라 함수 선언으로 이해한다.(항목 6 참조)
- 범위 삽입(Range Insertion)
- 모든 표준 컨테이너는 다음 형태의 insert를 지원한다.
-
void container::insert(iterator position, // 범위 삽입 위치 InputIteractor begin, // 삽입할 범위 시작 InputIterator end); // 삽입할 범위 끝 // 연관 컨테이너는 컨테이너 내부 비교 함수로 요소 위치를 결정하므로 // 위치 매개 변수를 받지 않는다. void container::insert(InputIteractor begin, // 삽입할 범위 시작 InputIterator end); // 삽입할 범위 끝
- 범위 삭제(Range Erasure)
- 이 함수의 반환 타입은 시퀀스 컨테이너와 연관 컨테이너 각각이 다르다.
- C++11에서는 개선된 사항이다.
-
// 시퀀스 컨테이너 iterator container::erase(iteractor begin, iterator end); // 연관 컨테이너 // 허나 C++11 부터는 연관 컨테이너도 반복자를 반환하게 변경. void container::erase(iterator begin, iterator end);
- 왜 다르냐?
- 연관 컨테이너가 iterator를 반환하면 수행 성능 저하가 있을 수 있다고 표준안은 말하고 있다.
- erase와 remove를 함께 사용하는 erase-remove idiom에서 범위 버전 erase의 특성을 확실히 볼 수 있다.(항목 32 참조)
- 이 함수의 반환 타입은 시퀀스 컨테이너와 연관 컨테이너 각각이 다르다.
- 범위 대입(Range Assignment)
- 모든 표준 시퀀스 컨테이너는 범위 버전 assign을 제공한다.
-
void container::assign(InputIterator begin, InputIterator end);
728x90
'개발 > Effective STL' 카테고리의 다른 글
[Effective STL] 7. 컨테이너 요소 new, delete 짝 맞춤 문제 (0) | 2024.11.11 |
---|---|
[Effective STL] 6. 컴파일러의 분석 오류 (0) | 2024.11.08 |
[Effective STL] 4. 빈 컨테이너인지 확인하기 (0) | 2024.11.04 |
[Effective STL] 3. 컨테이너 요소 복사 (0) | 2024.10.31 |
[Effective STL] 2. 컨테이너 독립성(Container-Independent) (2) | 2024.10.30 |
Comments