스토리텔링 개발자

[Effective STL] 22. set 요소의 key 바꾸기 금지 본문

개발/Effective STL

[Effective STL] 22. set 요소의 key 바꾸기 금지

김디트 2024. 12. 5. 11:14
728x90

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

 

 

 

이유
  • set과 multiset은 요소를 정렬해서 관리하며, 정렬됨을 간주하고 동작한다.
  • 임의로 값을 바꾸면 그 정렬이 올바를 리가 없기 때문이므로 임의로 키값을 바꾸면 안 된다.
  • 어차피 모던 c++에서는 set은 cbegin만 지원하므로 const가 아닌 요소에 접근하기 쉽지 않다.

 

 

 

map에서는?
map<int, string> m;
...
m.begin()->first = 10; // 컴파일 에러

multimap<int, string> mm;
...
mm.begin()->first = 20; // 역시 컴파일 에러
  • map, multimap은 애초에 pair<const K, V> 값을 저장하기 때문에 변경할 수 없다.
  • 물론 const_cast를 써서 억지로 바꿀 수는 있겠지만..
  • 어쨌건 set은 const가 붙어있지 않으므로 마음만 먹으면 맘대로 바꿀 수 있다.

 

 

 

set의 요소가 const가 아닌 이유
// 요소
class Employee
{
public:
    ...
    const string& name() const;
    void setName(const string& name);
    const string& title() const;
    void setTile(const string& title);
    int idNumber() const;
};

// 비교 함수 객체
struct IDNumberLess
{
    bool operator()(const Employee& lhs, const Employee& rhs) const
    {
        return lhs.idNumber() < rhs.idNumber();
    }
};

using EmplDSet = set<Employee, IDNumberLess>;
EmplDSet se; // 식별 번호로 정렬되는 직원 데이터의 set

Employee selectedID;
...
EmplDSet::iterator i = se.find(selectedID);
if(i != se.end())
{
    i->setTitle("Corporate Deity"); // 키 값을 바꾼 것은 아니므로 문제 없음
}
  • 즉, 이런 식으로 키가 아닌 값이 바뀌어야 하는 상황이 생기므로 const가 아닌 것이다.
  • map / multimap도 키값에 이런 방식을 사용할 수 있는 거 아닌가?
    • 하지만 표준화 위원회는 map은 키 값을 const로, set은 키 값을 const가 아닌 것으로 정하였다.
    • 참고) 지금은 반대로 set 역시 키 값이 const인 것을 사용하도록(cbegin) 사양이 변경되었다.(C++11)
  • 컴파일러가 set에서 const가 붙은 키값을 제공하는 경우도 있는데 key 값을 막 바꿔도 될까?
    • 이식성에 별 생각이 없다면 키 값 외의 부분을 그냥 바꿔버리자.
    • 이식성을 고려한다면 set 요소를 건들지 말자.
  • 이식성을 고려해서 const_cast를 사용한다면?
    • 참조자(reference) 캐스팅을 사용하여 처리하자.
EmplDSet::iterator i = se.find(selectedID);
if(i != se.end())
{
    const_cast<Employee&>(*i).setTitle("Corporate Deity");
}
  • 캐스팅 실패 사례
EmplDSet::iterator i = se.find(selectedID);
if(i != se.end())
{
    static_cast<Employee>(*i).setTitle("Corporate Deity"); // 실패 1
    
    ((Employee)(*i)).setTitle("Corporate Deity"); // 실패 2
}
  • 위 두 방법은 똑같은 기계어 코드를 만들고, 동일한 이유로 오동작한다.
    • 수행한 결과는 *i의 사본인 임시(temporary) 객체이며, 여기에 대해 setTile을 호출한다.
  • map, multimap의 경우 key가 const로 못박혀 있으므로 const_cast로 상수성을 날리는 것에 대한 고려는 되어있지 않을 수 있다.
    • 이론적으로 읽기 전용의 메모리 위치에 기록하도록 구현될 수도 있으므로 상수성을 날리는 건 위험할 수 있다.

 

 

 

연관 컨테이너 값을 안전하게 바꾸기
  1. 변경하고자 하는 컨테이너 요소의 위치를 탐색한다.(항목 45 참조)
  2. 탐색한 요소의 복사본을 만든다.
  3. 컨테이너에서 그 요소를 없앤다. 대개 erase를 호출한다.(항목 9 참조)
  4. 만들어둔 복사본의 정보를 바꾼다.
  5. 복사본을 컨테이너에 새로 삽입한다.
    • 삽입할 위치가 제거했던 위치와 같거나 그 옆이면 insert에 단계 1에서 얻은 반복자를 넘기면
    • 삽입 시간을 로그 시간에서 상수 시간으로 단축시킬 수 있다.
EmplDSet se;
Employee selectedID;

...
EmplDSet::iterator i = se.find(selectedID); // 1. 요소 탐색
if(i != se.end())
{
    Employee e(*i); // 2. 복사
    se.erase(i++); // 3. 삭제, 그리고 반복자 유효성을 유지하기 위해 후위 증가
    e.setTitle("Corporate Deity"); // 4. 복사본 수정
    se.insert(i, e); // 5. 재삽입
}

 

 

 

결론
  • 연관 컨테이너는 요소 정보를 바꿀 때에는 바꾼 후에도 반드시 모든 요소가 정렬되어 있도록 유지해야 한다.
728x90
Comments