Effective C++/Effective STL

[Effective STL] 2. 컨테이너 독립성(Container-Independent)

김디트 2024. 10. 30. 11:29
728x90

https://delightlane.tistory.com/161항목 2. "컨테이너에 독립적인(container-independent) 코드"라는 환상을 조심하자

 

 

 

과한 일반화(generalization)의 적용
  • 코드 작성 시 일반화를 고려하게 될텐데, 이를 과하게 적용시키려 하는 것은 문제이다.
  • 컨테이너에 독립적인(container-independent) 코드를 작성하려는 욕심.
    • 모든 컨테이너에 대해 사용할 수 있도록 코드를 만든다.
    • 예컨대 vector를 사용하는 부분을 만들면서 언제든지 deque나 list를 쓸 수 있는 여지를 남긴다던가.

 

 

 

시퀀스 컨테이너와 연관 컨테이너를 일반화한다?
  • 대다수의 멤버 함수들은 한쪽 컨테이너에만 치우쳐 들어있다.
    • push_back, push_front 등 : 시퀀스 컨테이너에서만 지원
    • count, lower_bound 등 : 연관 컨테이너에서만 지원
    • insert, erase 등 : 양쪽 모두에 있지만, 동작 원리가 다르다.
      • insert : 시퀀스 컨테이너에서는 객체의 위치가 유지되지만, 연관 컨테이너는 자체 정렬 방식에 맞춰 객체를 옮긴다.
      • erase : 시퀀스 컨테이너에서는 반복자가 새로 리턴되지만, 연관 컨테이너에서는 아무것도 리턴하지 않는다.(항목 9 참조)
  • 그러므로 일반화는 불가능하다.

 

 

 

가장 많이 쓰이는 시퀀스 컨테이너인 vector, deque, list에 대해서만 일반화 한다?
  • 각 컨테이너의 공통 함수만을 사용하도록 코딩한다.
    • 즉, 공통되지 않은 기능들을 사용하지 않겠다는 의미이다.
    • deque, list에서 제공하지 않는 기능
    • list에서 제공하지 않는 기능
      • operator[]를 사용할 수 없다.
      • 즉, 임의 접근 반복자를 사용하는 알고리즘도 사용할 수 없다는 뜻이다.
    • vector에서 제공하지 않는 기능
      • push_front, pop_front를 사용할 수 없다.
    • vector, deque에서 제공하지 않는 기능
      • splice, 멤버 함수 sort를 사용할 수 없다.
    • 결국 따져보면, 일반화 시퀀스 컨테이너에 대해 호출할 수 있는 sort는 아무것도 없다.
  • 또한 반복자, 포인터, 참조자를 무효화시키는 방식이 각 컨테이너마다 다르다.
    • insert를 호출하면 모든 반복자, 포인터, 참조자가 무효화된다고 봐야 한다.
      • deque::insert는 모든 반복자를 무효화하며, capacity가 지원되지 않는다.
      • vector::insert는 모든 포인터와 참조자를 무효화한다.
      • deque는 포인터와 참조자를 그대로 두고 반복자를 무효화한다.
    • 결국, erase 역시 마찬가지로 모두 무효화된다고 봐야 한다.
  • C 인터페이스로 컨테이너 데이터를 넘기는 것도 불가능해진다.
  • bool 타입을 관리하는 컨테이너를 템플릿 인스턴스로 만들 수도 없다.
    • vector<bool>은 절대로 vector처럼 동작하지 않고, 실제로 bool 데이터를 저장하지도 않는다. (항목 18 참조)
  • list에서 상수 시간에 이루어지는 요소 삽입과 삭제를 기대해선 안된다.
    • vector와 deque에서는 선형 시간의 복잡도를 가지기 때문이다.

 

 

 

셋 중 list를 지원하지 않도록 하면?
  • 위 문제들 중 여전히 남는 문제들..
    • reserve, capacity, push_front, pop_front는 사용할 수 없다.
    • insert와 erase는 선형 시간이 걸리고 모든 것이 무효화된다.
    • C와의 데이터 호환성이 불가능하다.
    • bool 데이터를 저장하는 컨테이너가 불가능하다.

 

 

 

연관 컨테이너에 대해서만 일반화 한다?
  • 이 경우 set, map에 대해 코딩하는 것은 불가능하다.
    • set은 하나의 객체, map은 객체의 pair을 저장하므로 일반화할 수 없다.
    • insert 멤버 함수의 리턴 타입 역시 다르므로 여전히 일반화 불가능하다.
  • set, multiset(혹은 map, multimap)에 대해 코딩하는 것도 힘들다.
    • 컨테이너에 값의 사본이 몇 개나 저장될지를 판단할 수 없어지기 때문이다.
  • operator[]를 사용할 수 없다. 이는 map에서만 지원된다.

 

 

 

컨테이너 캡슐화
  • STL 컨테이너는 각자 자신만의 장단점이 있다.
  • 애초에 서로 바꾸어서 쓸 수 있도록 설계되지 않았다.
  • 그렇다면, 만약 코딩 중 컨테이너를 바꿔야 하는 상황이 생긴다면 어떻게 해결해야 하나?
    • 새로 바꾼 컨테이너를 사용하는 모든 코드를 테스트하며, 수행 성능, 반복자나 포인터나 참조자의 무효화 방식 등등을 모두 체크해야 한다.
    • 예컨대 vector를 다른 것으로 바꾸었다면, 우선 C와 호환되는 메모리 배열 구조에 의존했던 코드를 바꿔야 할 것이고, 다른 것을 쓰다가 vector로 바꾸었다면, bool을 저장하던 것들을 다른 방식으로 수정해야 할 것이다.
    • 즉, 너무 많은 품이 든다.
  • 경우에 따라 수시로 컨테이너 타입을 바꿀 수밖에 없다면, 캡슐화를 사용하자.

 

 

 

방법 1 : 컨테이너와 반복자 타입에 대해 typedef를 사용한다.
class Widget { ... };

// 원래는 이렇게 코딩할 것을
vector<Widget> vw;
Widget bestWidget;
...
vector<Widget>::iterator i = find(vw.begin(), vw.end(), bestWidget);

// 이렇게 수정한다.
typedef vector<Widget> WidgetContainer;
typedef WidgetContainer::iteractor WCIterator;
WidgetContainer vw;
Widget bestWidget;
...
WCIteractor i = find(vw.begin(), vw.end(), bestWidget);
  • 이 경우 컨테이너 타입을 일일이 수정하는 것보다는 훨씬 쉽게 수정할 수 있다.
  • 컨테이너에 커스텀 할당자(allocator)를 붙이는 경우도 훨씬 손쉬워진다.
class Widget { ... };
template<typename T>
SpecialAllocator { ... };

typedef vector< Widget, SpecialAllocator<Widget> > WidgetContainer; // 할당자 적용
// 아래 코드는 수정할 필요가 없다.
typedef WidgetContainer::iterator WCIterator;
WidgetContainer vw;
Widget bestWidget;
...
WCIterator i = find(vw.begin(), vw.end(), bestWidget);
  • 반복자를 typedef로 하는 경우, 이 긴 타입명을 일일이 칠 필요가 없다는 장점이 있다.
    • 하지만 모던 C++ 에서는 auto를 사용하면 해결되는 문제이다.
  • 하지만 typedef는 무척 제한적인 캡슐화이다.
    • 할 수 없는 것을 하지 못하도록 막는 장치가 없다.

 

 

 

방법 2 : 클래스를 사용하여 캡슐화한다.
  • 클래스에 컨테이너를 숨기고, 필요한 기능만 인터페이스를 제공한다.
// 상품 구매자 리스트로 list<Customer> 캡슐화
class CustomerList
{
private:
    typedef list<Customer> CustomerContainer;
    typedef CustomerContainer::iterator CCIterator;
    
    CustomerContainer customers;
public:
    ...
};

 

  • 임의 접근 기능이 필요해졌다고 하면, vector나 deque로 변경할 수 있을 것이다.
  • 허나, 여전히 변경 시에는 후처리가 필요하다.
    • CustomerList의 멤버 함수와 모든 friend 멤버를 조사해서 변경 후의 영향(수행 성능, 반복자/포인터/참조자 무효화 등)에 대해 점검해야 한다.
728x90