항목 30 : 알고리즘의 데이터 기록 범위는 충분히 크게 잡자

int transmogrify(int x);

std::vector<int> values;
...
std::vector<int> results;
std::transform(values.begin(), values.end(), results.end(), transmogrify);

위의 코드의 의도는 함수의 반환값을 results.end부터 삽입하라는 의미이다.

하지만 동작하지 않는다. 왜냐면 results.end()는 객체가 존재하지 않기 때문이다.

 

std::transform(values.begin(), values.end(), std::back_inserter(results), transmogrify);

대신 삽입 반복자를 사용한다. back_inserter는 push_back을 호출하도록 되어있기 때문에 push_back을 지원하는 컨테이너에만 사용할 수 있다.

back_inserter 말고도 front_inserter(앞에서 추가), inserter(임의 위치부터 추가)도 존재한다.

 

if (results.size() < values.size()) results.resize(values.size());

std::transform(values.begin(), values.end(), results.begin(), transmogrify);

만약 결과값을 기존 컨테이너에 덮어씌우고 싶다면 resize를 통해서 최소한의 크기는 확보하는게 안전하다.

혹은 results를 clear한 이후 삽입 반복자를 호출하면 된다.

 

결론적으로 목적지 범위를 지정하여 수행하는 알고리즘은 범위의 크기를 미리 확보하거나 자동으로 증가되도록 만들어야 한다.

 

 

항목 31 : 정렬시의 선택 사항들을 제대로 파악해 놓자

전체 정렬이 필요한 경우 sort, 전체 정렬을 하되 순서 유지성이 필요하면 stable_sort, 부분 정렬이 필요한 경우 partial_sort, 상위 값이 필요하되 정렬여부는 관계 없다면 nth_element를 사용하면 된다.

 

// 전체 정렬
std::sort(widget.begin(), widget.end(), qualityCompare);

// 전체 정렬 (순서 유지)
std::stable_sort(widget.begin(), widget.end(), qualityCompare);

// 부분 정렬 (상위 20개)
std::partial_sort(widget.begin(), widget.begin() + 20, widget.end(), qualityCompare);

// 상위 값 추출 (단, 지정된 반복자만 정렬 순서 보장. 이경우 widget.begin() + 20)
std::nth_element(widget.begin(), widget.begin() + 20, widget.end(), qualityCompare);

 

partial_sort, nth_element는 값이 동등관계인 경우 아무거나 골라낸다.

nth_element는 상위 n개의 요소를 찾아내는 것 외에 범위 내의 중앙값을 찾거나 특정한 백분위 위치에 있는 값을 찾아내는 데에도 사용할 수 있다.

 

다만 위의 알고리즘들은 임의 접근 반복자를 지원해주는 컨테이너에만(vector, deque) 사용이 가능하다. (list는 사용불가)

 

한가지 알고리즘이 더 있다. 컨테이너의 요소를 특정 조건을 기준으로 좌우로 분할한다.

 

bool hasAcceptableQuality(const Widget& w)
{
    // Widget 객체의 등급이 2등급 이상인지 판별
}

auto goodEnd = std::partition(widget.begin(), widget.end(), hasAcceptableQuality);

어떤 조건을 기준으로 조건을 만족한 값은 begin~goodEnd 사이에, 만족하지 못한 값은 goodEnd~end 사이에 위치하게 된다.

partition은 위의 알고리즘과 다르게 양방향 반복자 이상이면 사용할 수 있으므로 표준 시퀀스 컨테이너 모두에 대해 동작한다.

 

◾ 전체 정렬 (sort, stable_sort) : vector, string, deque, 배열

◾ 부분 정렬 (partial_sort) : vector, string, deque, 배열

◾ 상위 N개 요소 (nth_element) : vector, string, deque, 배열

◾ 기준에 따른 요소 구분 (partition, stable_partition) : 표준 시퀀스 컨테이너

 

partition -> stable_partition -> nth_element -> partial_sort -> sort -> stable_sort 순으로 성능이 점점 나빠진다.

 

 

항목 32 : 요소를 정말로 제거하고자 한다면 remove 류의 알고리즘에는 꼭 erase를 붙여 사용하자

remove는 이름과 달리 범위 내의 요소를 재배치할 뿐이다. 다만 삭제하기 원치 않은 요소들을 앞으로 당겨오며 덮어쓸 뿐이다.

대신 remove가 이루어지고 나면 새로운 end 반복자를 반환해주기 때문에 여기서부터 본래 반복자의 end까지를 erase 시켜주면 실제 의도한대로 삭제가 이루어진다.

 

v.erase(std::remove(v.begin(), v.end(), 99), v.end());

remove_if나 unique 역시 동일하게 동작하기 때문에 erase를 같이 써주어야한다.

 

예외적으로 list의 멤버 함수로 제공되는 remove, unique는 실제로 삭제가 이루어진다.

 

 

항목 33 : remove와 비슷한 알고리즘을 포인터의 컨테이너에 적용할 때에는 각별히 조심하자

동적 할당된 포인터를 별도의 해제 없이 erase-remove하게 된다면 remove에서 요소가 앞으로 당겨지며 덮어씌워질 때, 메모리 누수가 발생한다. 덮어 씌워진 포인터는 찾을 방법이 아예 없어진다.

대신 partition으로 나누고 뒷부분에 대해서 delete를 실행해준 뒤 erase를 하면 문제가 없을 것이다.

 

더 좋은 방법으로는 스마트 포인터를 사용하는 것이다. 딱히 고려할것도 없이 바로 erase-remove를 수행해도 된다.

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

함수자, 함수 객체, 함수, 기타 등등 (1)  (0) 2022.12.20
알고리즘(Algorithms) (2)  (0) 2022.12.20
반복자(Iterators)  (0) 2022.12.20
STL 연관 컨테이너 (2)  (0) 2022.12.20
STL 연관 컨테이너 (1)  (0) 2022.12.20

항목 26 : const_iterator나 reverse_iterator, const_reverse_iterator도 좋지만 역시 쓸만한 것은 iterator이다

iterator->const_iterator의 변환은 가능하지만 역은 성립이 되지 않기 때문에 요소의 삽입이나 삭제 위치를 지정하는데 있어서 상수 반복자는 대개 쓸모가 없다.

'대개' 쓸모가 없다는 것이지 아예 쓸모가 없는건 아니다. 알고리즘은 반복자의 종류에 상관없이 동작하기 때문이다.

다만 어떤 형태의 insert와 erase는 꼭 iterator만을 넘겨야만 한다.

 

typedef std::deque<int> IntDeque;
IntDeque::iterator i;
IntDeque::const_iterator ci;
...

if (i == ci)

위의 코드는 iterator를 const_iterator로 암시적인 변환을 해주지 않는다면 컴파일러에 따라 오류를 발생시킬 수도 있다.

 

웬만하면 반복자의 타입을 이것저것 사용하지 않고 iterator만 사용하는 것이 낫다.

 

 

항목 27 : const_iterator를 iterator로 바꾸는 데에는 distance와 advance를 사용하자

const_iterator에 const_cast를 한다고 상수성이 제거되지는 않는다. (vector, string은 예외)

하지만 생각보다 쉽게 안전하고 이식성 있게 변환시킬 수 있다.

const_iterator의 거리를 구해서 iterator를 그만큼 이동시키면 끝이다.

 

std::deque<int> d;
typedef std::deque<int>::iterator Iter;
std::deque<int>::const_iterator CIter; // d의 요소를 가리킨다
...

Iter i(d.begin();
CIter ci = d.find(3);

std::advance(i, std::distance(i, ci));
// std::advance(i, std::distance<CIter>(i, ci));
// const_iterator->iterator 변환이 안된다면 템플릿 인자로 명시한다

또 하나 언급하자면 vector같은 임의 접근 반복자는 상수시간, 양방향 반복자는 선형시간에 특정 반복자에 접근할 수 있기 때문에 정말 필요한게 아니라면 const_iterator를 iterator로 변환하지 않는것이 좋다.

 

 

항목 28 : reverse_iterator에 대응되는 기점 반복자를 사용하는 방법을 정확하게 이해하자

 

std::vector<int> v;

for(int i = 1; i <= 5; ++i) {
    v.push_back(i);
}
std::vector<int>::reverse_iterator ri = std::find(v.rbegin(), v.rend(), 3);
std::vector<int>::iterator i(ri.base());

 

코드를 실행하면 iterator와 reverse_iterator는 그림과 같이 배치된다. ri.base()는 순향 반복자 기준 다음 위치를 반환해준다.

 

◾ 만약 요소의 삽입이 이뤄진다면?

insert는 iterator가 가리키는 위치 바로 앞에서 이뤄진다. 그래서 base로 반환받은 순향 반복자에 별다른 연산을 할 필요없이 바로 삽입을 수행하면 된다.

 

◾ 만약 요소의 삭제가 이뤄진다면?

삽입과 다르게 가리키고 있는 요소를 삭제하기 때문에 대응되지 않는다. 그래서 base로 반환받은 반복자의 한칸 앞의 요소를 삭제해주어야 한다.

그런데 vector나 string의 경우, 반복자가 const T*로 되어있기 때문에 아예 역방향 반복자를 한칸 더 이동시키고 base를 반환받아서 사용해야한다.

 

v.erase((++ri).base());
// --ri.base() 가 아닌 이유 : 
// 벡터는 반복자가 const T*로 이뤄져있기 때문에 반환받은 반복자에 연산을 할 수 없다

 

 

항목 29 : 문자 단위의 입력에는 istreambuf_iterator의 사용도 적절하다

std::ifstream inputFile("interestingData.txt");
std::string fileData((std::istream_iterator<char>(inputFile)), std::istream_iterator<char>());

아쉽게도 위의 코드는 공백 문자를 복사하지 못한다. istream_iterator는 operator<<를 사용하고 공백 문자를 건너뛰기 때문이다.

 

inputFile.unsetf(std::ios::skipws);

입력 스트림의 skipws 플래그를 해제하면 공백 문자를 건너뛰지 않겠지만 생각보다 복사 시간이 빠르지 않다.

 

std::ifstream inputFile("interestingData.txt");

std::string fileData((std::istreambuf_iterator<char>(inputFile)), std::istreambuf_iterator<char>());

대신 istreambuf_iterator를 사용하면 스트림 자체의 버퍼를 직접 건드려서 다음 문자들을 바로 읽어들인다.

skipws의 플래그도 해제할 필요가 없다.

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

알고리즘(Algorithms) (2)  (0) 2022.12.20
알고리즘(Algorithms) (1)  (0) 2022.12.20
STL 연관 컨테이너 (2)  (0) 2022.12.20
STL 연관 컨테이너 (1)  (0) 2022.12.20
vector와 string  (0) 2022.12.18

항목 22 : set과 multiset에 저장된 데이터 요소에 대해 키를 바꾸는 일은 피하자

잠깐만 생각해봐도 당연한 얘기다. 데이터가 삽입될 때 정렬되어서 관리되는데, 이미 삽입된 요소의 값을 바꾸면 정렬 상태가 무너진다.

다만 map과 다르게 const T가 아닌 그냥 T가 저장되기 때문에 정렬 기준과 상관없는 값을 바꿀수는 있다.

 

class Employee {
public:
    ...
    void setTitle(const std::string& title);
    int idNumber() const; // 정렬 기준
};

auto i = se.find(selectedID);
...
i->setTitle("Corporate Deity"); // 변경 가능

객체에 데이터가 여러개있고 일부 데이터만 정렬 기준(key)이 된다면 그 외의 데이터는 수정되어도 정렬 상태가 무너지지 않는다.

만약 set의 반복자 역참조가 T&가 아닌 const T&로 반환한다면, 캐스팅을 통해 참조자를 반환받은뒤에 값을 수정하면 된다.

 

map의 경우는 키값을 저장할때부터 상수화시키기 때문에 함부로 캐스팅을 통해 상수 성질을 없애지 않는것이 좋다

 

만약 저장된 요소를 바꾸고 싶다면 아래 단계를 순서대로 거치면 호환성과 안전성을 챙길 수 있다.

 

  1. 변경하고자 하는 컨테이너 요소의 위치를 찾는다.
  2. 해당 요소의 복사본을 만든다.
  3. 원래 컨테이너에서 해당 요소를 삭제한다.
  4. 복사본의 데이터를 수정한다.
  5. 복사본을 컨테이너에 새로 삽입한다. 만약 새로 삽입되는 요소가 기존 요소와 위치가 같거나 바로 옆이라면 3번에서 삭제될 때의 반복자를 넘겨서 삽입 시간을 상수 시간으로 단축시킬 수 있다.

 

 

항목 23 : 연관 컨테이너 대신에 정렬된 vector를 쓰는 것이 좋을 때가 있다

데이터 셋업, 데이터 탐색, 데이터 재구성 순으로 자료 구조를 사용한다면 연관 컨테이너보다 벡터가 훨씬 나을 수 있다.

 

연관 컨테이너는 균형 이진 탐색(AVL) 트리를 사용하는데, 연속 메모리 구조가 아니라 트리 노드로 구현되어 있기 때문에 요소 하나 저장하는데 최소 세 개의 포인터를 사용하므로 오버헤드가 발생한다.

근데 벡터는 재할당이 이루어지지 않는 한 오버헤드가 없다.

 

만약 저장해야 하는 데이터의 수가 매우 많다고 할 때, 연관 컨테이너는 벡터와 같은 데이터의 양을 저장하더라도 오버헤드로 인해 사용 메모리가 많아지고, 페이지 하나당 저장되는 데이터 개수의 더 적기 때문에 캐시 적중률이 떨어지고, 페이지 부재도 더 빈번하게 발생할 수 있다.

 

그래서 정렬된 벡터에 한해서는 연관 컨테이너보다 더 나은 성능을 보여줄 가능성이 높다.

제약사항이 한가지 더 있긴 하다. 삽입, 삭제, 탐색이 빈번하지 않은 경우여야 한다는 점이다.

 

typedef std::pair<std::string, int> Data;

class DataCompare {
public:
    bool operator()(const Data& lhs, const Data& rhs) const { // 정렬용 비교 함수
        return keyLess(lhs.first, rhs.first);
    }
    bool operator()(const Data& lhs, const Data::first_type& k) const { // 탐색용 비교 함수1
        return keyLess(lhs.first, k);
    }
    bool operator()(const Data::first_type& k, const Data& rhs) const { // 탐색용 비교 함수2
        return keyLess(k, rhs.first);
    }
private:
    bool keyLess(const Data::first_type& k1, const Data::first_type& k2) const { // 실제 비교 함수
        return k1 < k2;
    }
};

덧붙여서 벡터를 써서 맵을 대신하려면 키값이 const여서는 안된다. 정렬될 때 각 요소의 값이 대입 연산을 통해 이동되기 때문이다. 만약 구현하게 된다면 정렬용 비교 함수 외에도 탐색용 비교 함수를 따로 만들어 두어야 한다.

 

 

항목 24 : map::operator[]나 map::insert는 효율 문제에 주의하여 선택하자

class Widget {
public:
    Widget();
    Widget(double weight);
    Widget& operator=(double weight);
    ...
};

std::map<int, Widget> m;
m[1] = 1.50; // == m.operator[](1)
m[2] = 3.67; // 만약 key에 2가 없다면?
...

맵은 key-value 페어로 이루어져 있다. 이미 존재하는 key라면 value의 참조자를 반환하여 value를 수정하고 없는 key라면 새로운 key-value 페어를 생성한다.

새로운 페어를 생성할 때, value_type의 기본 생성자를 이용해서 새로운 객체를 만들어낸다.

 

m[1] = 1.50;

/* 위의 코드는 아래로 풀이된다 */

std::pair<std::map<int, Widget>::iterator, bool> result = 
    m.insert(std::map<int, Widget>::value_type(1, Widget()));
    
result.first->second = 1.50;

키 검사->페어 생성->기본 생성자로 임시객체 생성->value에 복사->임시객체 소멸->value.operator=호출 순으로 이루어진다.

만약 기본 생성자 대신 원하는 값을 직접 매개변수에 넘겨주면 호출 횟수가 많이 줄어들 것이다.

 

m.insert(std::map<int, Widget>::value_type(1, 1.50));

 

만약 추가가 아닌 갱신이라면 오히려 insert보다 operator[]가 더 효율적이다.

 

template<typename MapType, typename KeyArgType, typename ValueArgType>
typename MapType::iterator efficientAddOrUpdate(MapType& m, const KeyArgType& k, const ValueArgType& v) {
    typename MapType::iterator lb = m.lower_bound(k); // 이진 탐색

    // 값 갱신
    if (lb != m.end() && !(m.key_comp()(k, lb->first))) { // 탐색 성공 && 동등성 검사
    // 탐색이 성공했다고 해서 값이 같은것은 아니기 때문에 동등성 검사가 필요하다
        lb->second = v;
        return lb;
	}
    // 값 추가
    else {
        typedef typename MapType::value_type MVT;
        return m.insert(lb, MVT(k, v));
    }
}

하나의 함수로 추가와 갱신을 판단해서 각자 효율적인 동작을 선택하도록 구현해보면 대략적으로 위와 같다.

 

 

항목 25 : 현재는 표준이 아니지만, 해쉬 컨테이너에 대해 충분히 대비해 두자

(참고: 지금은 표준이다. unordered_map, unordered_set)

해쉬 컨테이너는 다른 연관 컨테이너와 다르게 비교 함수를 동등성 검사가 아닌 상등성 검사로 한다.

정렬 순서가 무의미하기 때문이다.

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

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

연관 컨테이너는 시퀀스 컨테이너와 다르게 상등성이 아닌 동등성의 관점으로 요소를 처리한다.

또한 자동으로 요소가 정리된다.

 

항목 19 : 상등 관계(equality)와 동등 관계(equivalence)의 차이를 파악하자

find 알고리즘과 set의 insert는 기본적으로 "두 값이 같은가?"를 알아내는것은 같지만 동작 방식은 다르다.

"같다"의 정의는 find는 상등성, insert는 동등성에 두고있다.

조금 더 쉽게 얘기하면 상등성은 operator==(같다, 틀리다), 동등성은 operator<(크다, 작다)로 판별한다.

 

◾ 상등성

개념적 뿌리는 operator==에 두고있다.

x==y가 참이면 x, y는 같다. 하지만 x, y가 같다고 해서 모든 데이터 멤버가 같다는 보장은 없다.

 

class Widget {
...
private:
    TimeStamp lastAccessed;
};

bool operator==(const Widget& lhs, const Widget& rhs)
{
    // lastAccessed 필드 고려 X
}

lastAccessed 필드를 고려하지 않았기 때문에 lastAccessed 값이 다르더라도 다른 데이터 멤버가 동일하면 두 객체는 같다는 판정을 내린다.

 

◾ 동등성

정렬된 범위 안에 있는 개체들의 상대적인 순서로 판단한다.

객체 x, y가 있을 때, x, y 모두가 서로의 앞에 오지만 않으면 동등하다고 본다.

 

!(w1 < w2) && !(w2 < w1)

이 식이 성립하면 동등하다고 본다. 서로의 값이 서로의 앞에 오지 않으면 두 값은 특정한 정렬 기준에 대해서 동등한 것이다.

 

실제로 연관 컨테이너에 사용되는 비교 함수는 operator<가 아니라 기본적으로 less 이다.

 

!c.key_comp()(x,y) && !c.key_comp()(y,x)

c.key_comp는 비교용 함수를 반환해준다.

 

대소문자를 구분하지 않는 set에서 문자열 "STL"과 "stL"은 상등 관계가 아니지만 동등 관계이다.

표준 연관 컨테이너가 상등성이 아닌 동등성에 기반을 두고 만들어진 이유는 비교 함수를 딱 하나만 지정해주기 위해서이다. 만약 상등성에 기반을 두었다면 정렬용 비교 함수 말고도 값이 같은지를 알아내는 함수가 하나 더 필요하다.

 

연관 컨테이너에 값을 넣을때는 동등 관계로 넣고 값을 찾을때는 상등 관계로 찾는다.

 

 

항목 20 : 포인터를 저장하는 연관 컨테이너에 대해서는 적합한 비교(비교 함수자) 타입을 정해주자

연관 컨테이너는 요소를 삽입할 때 자동으로 정렬이 된다.

만약 타입이 포인터라면 포인터 값을 기준으로 정렬을 하기 때문에 별도의 비교 함수를 정해주어야 한다.

 

std::set<string*> ssp;
ssp.insert(new std::string("Anteater"));
...

auto i = ssp.begin();
*i; // 포인터 값
**i; // 데이터

역참조를 두번 하면 데이터에 접근할 수 있지만 애초에 정렬 기준이 데이터가 아니라서 저장 순서도 뒤죽박죽이다.

 

struct StringPtrLess : public std::binary_function<const std::string*, const std::string*, bool> {
    bool operator()(const std::string* ps1, const std::string* ps2) const {
        return *ps1 < *ps2;
    }
};

기본으로 지정되는 비교 함수자 대신에 포인터가 가리키는 요소를 비교하는 임의의 비교 함수 객체를 만들어서 선언시 지정해주면 된다.

 

typedef std::set<std::string*, StringPtrLess> StringPtrSet;

StringPtrSet ssp;
ssp.insert(new std::string("Anteater"));
...

string의 포인터를 저장하기 때문에 여전히 데이터에 접근하려면 두 번의 역참조가 이뤄져야 하지만 정렬 기준은 의도한대로 데이터를 기준으로 정렬된다.

 

struct DereferenceLess {
    template<typename PtrType>
    bool operator()(PtrType pT1, PtrType pT2) const {
        return *pT1 < *pT2;
    }
};

std::set<std::string*, DereferenceLess> ssp;

멤버 함수를 템플릿으로 만들면 각 포인터의 타입별로 함수 객체를 만들 필요 없이 범용적으로 사용이 가능하다.

 

꼭 원시 포인터가 아니더라도 포인터처럼 동작하는 스마트 포인터나 반복자 역시 동일하게 적용해야한다.

 

 

항목 21 : 연관 컨테이너용 비교 함수는 같은 값에 대해 false를 반환해야 한다

항목 19 내용의 연장선이다.

set의 비교 함수는 기본적으로 less이다. 만약 less_equal일때의 상황을 가정해보자.

 

std::set<int, std::less_equal<int>> s;
s.insert(10);
s.insert(10); // 중복 값 삽입 시도

 

!(10A <= 10B) && !(10B <= 10A)
// !true && !true => false

중복된 값의 삽입 시도를 했음에도 둘의 값은 동등하지 않다는 결론을 내리고 삽입을 하게된다.

 

연관 컨테이너의 비교 함수는 같은 값에 대해 항상 false를 반환해야 구조가 무너지지 않는다.

관련 정보(strict weak ordering) : https://panty.run/strict-weak-ordering/

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

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

항목 13 : 동적으로 할당한 배열보다는 vector와 string이 낫다

배열을 동적으로 할당할일이 있으면 그대신 vector나 string을 고려한다. 메모리 할당과 해제에 대한 부담이 없기 때문이다.

시퀀스 컨테이너의 필수사양을 모두 가지고 있기 때문에 STL에서 지원되는 모든 알고리즘 역시 그대로 적용할 수 있다.

 

한가지 여담으로 string은 대개 참조 카운트 방식으로 구현되어있는데, 멀티 스레드 환경에서의 안전성 구현으로 인한 오버헤드가 발생해서 성능이 하락할 수도 있다.

이를 회피하는 방법으로 1)참조 카운팅 기능을 끄거나 2) 참조 카운팅을 사용하지 않는 string을 구현하거나 3)vector<char>를 대신 사용한다.

vector는 참조 카운팅 방식이 아니기때문에 멀티 스레드 환경에서의 오버헤드가 발생하지 않는다.

 

 

항목 14 : reserve는 필요 없이 메모리가 재할당되는 것을 막아 준다

vector와 string은 용량이 가득 차면 할당-복사-소멸-해제의 단계를 거쳐서 재할당된다. 그런데 이 과정이 상당한 비용이 소모될수 있기 때문에 최대한 덜 이루어지는 것이 좋다.

확장되는 크기를 어느정도 예측해서 미리 넉넉하게 잡아주면 재할당 횟수가 최소화 될 수 있을것이다.

이것을 reserve 멤버 함수가 해준다.

 

reserve 멤버 함수는 당장 사용하지 않더라도 메모리를 미리 할당함으로써 재할당 횟수를 최소화 시킬 수 있다.

더불어서 재할당으로 인한 반복자, 포인터, 참조자의 무효화로 발생하는 비용도 줄일 수 있다.

 

◾ size() : 컨테이너에 담긴 요소의 개수

◾ capacity() : 컨테이너의 용량 (담길 수 있는 요소의 총 개수)

◾ resize(size_t n) : 컨테이너에 담긴 요소 개수 재설정

◾ reserve(size_t n) : 컨테이너의 용량 재설정

 

reserve를 사용해서 재할당을 피하는 방법은 컨테이너의 사용량을 정확하거나 대략적으로 알고있는 경우. 또는 필요한 최대량으로 할당한 후에 컨테이너에 모두 담기면 남은 용량을 잘라내는 두 가지 방법이 있다.

 

 

항목 15 : 잊지 말자! string은 여러 가지 방식으로 구현되어 있다는 사실을...

라이브러리별로 string의 구현 방식이 다르다. sizeof(string)의 크기 역시 다르게 나온다.

 

 

항목 16 : 기존의 C API에 vector와 string을 넘기는 방법을 알아두자

vector는 연속 메모리 컨테이너이기 때문에 C API의 인자로 넘겨주는것이 그리 어렵지 않다.

 

void doSomething(const int* pInts, size_t numInts);

if (!v.empty()) doSomething(&v[0], v.size());

단, empty일때를 주의해야한다. 존재하지도 않는 요소의 주소를 넘겨줄수는 없다.

 

void doSomething(const char* pString);

doSomething(s.c_str());

string은 조금 다르다. empty여도 NULL을 반환하기 때문에 사용에 문제가 없다.

 

vector나 string이나 공통적으로 넘겨지는 매개변수는 const에 대한 포인터이다. 읽기만 가능할 뿐 수정이 불가능하다.

vector는 그나마 컨테이너 요소의 주소를 넘기기 때문에 const로 넘기지 않아도 요소의 값 변경이 가능하긴 하지만, string은 c_str로 반환된 포인터가 컨테이너에 존재하는 요소의 포인터라는 보장이 없기 때문에 오직 읽기만 가능하다.

 

C API를 통해 STL 객체를 초기화 하고 싶은 경우에는 vector를 활용하면 된다. vector만 유일하게 C/C++ 배열과 동일한 메모리 배열 구조를 가졌기 때문이다.

 

/* vector */
size_t fillArray(double* pArray, size_t arraySize);

std::vector<double> vd(maxNumDoubles);
vd.resize(fillArray(&vd[0], vd.size());


/* string */
size_t fillString(char* pArray, size_t arraySize);

std::vector<char> vc(maxNumChars);
size_t charsWritten = fillString(&vc[0], vc.size());
std::string s(vc.begin(), vc.begin() + charsWritten);

 

 

항목 17 : 쓸데없이 남은 용량은 "바꿔치기(swap) 묘수"를 써서 없애 버리자

예를 들어 어떤 오디션 프로그램의 참가 신청자의 목록을 vector로 관리한다고 했을 때, 처음에는 수많은 신청자 덕분에 용량을 매우 크게 할당하겠지만 프로그램이 진행됨에 따라 인원이 점점 줄어들게 될 것이다.

하지만 실제로 줄어든것은 size일뿐 capacity가 아니다. 더 이상 사용되지도 않는 메모리를 계속 가지고 있는것은 낭비이기 때문에 지속적으로 크기를 줄일 필요가 있다.

 

std::vector<Contestant>(contestants).swap(contestants);

Contestant 타입은 참가자, contestants는 실제 참가자 목록이고 해당 목록을 복사하여 임시 객체를 만들어서 실제 참가자 목록과 swap하면 딱 맞는 메모리의 재할당이 이루어진다.

임시 객체는 rvalue이기 때문에 해당 문장의 실행이 끝나면 메모리에서 소멸된다.

 

std::string s;
...
std::string(s).swap(s);

string에도 사용할 수 있다.

 

std::vector<Contestant> v;
std::string s;
...
std::vector<Contestant>().swap(v); // v 초기화 및 용량 최소화
std::string().swap(s); // s 초기화 및 용량 최소화

기본 생성자로 생성된 임시 객체와 바꿔치기 함으로써 구현 코드가 허용하는 최소치로 줄일수도 있다.

 

바꿔치기 될 때, 반복자, 포인터, 참조자도 모두 바뀌는것을 주의해야한다.

 

std::vector<int> v;
auto i = v.begin();
...
std::vector<int>().swap(v);
/* i는 무효화 된다 */

 

 

항목 18 : vector<bool> 보기를 돌같이 하자

vector<bool>은 STL 컨테이너처럼 보이지만 실제로는 아니다. 템플릿 특수화로 만들어진 특별한 타입이다.

 

T* p = &c[0];

c가 타입 T의 객체를 담는 컨테이너라면 위의 코드는 당연히 성립되어야 한다.

 

bool b[10]; // 요소당 1바이트
std::vector<bool> vb; // 요소당 1비트
...
bool* pb = &v[0]; // error

그런데 bool은 성립되지 않는다. bool이 1바이트를 사용하는것과 달리 vector<bool>은 1비트를 사용하기 때문에 데이터 표현 방식이 맞지 않기 때문이다.

vector<T>:operator[]의 반환 타입은 T&여야 하는데 비트에 대한 참조자는 반환할 수 없다.

 

만약 진짜 bool을 저장하고 싶다면 deque를 사용해야 한다.

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

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

+ Recent posts