게임 서버와 클라이언트에서 어떤 리스트(명단)을 주고 받을때 자료 구조를 만들어서 쓰기 보다는 주로 STL을 쓴다. 그중에서 많이 쓰이는 것들이 list와 vector인데, 이번에도 역시 서버에서 리스트를 받으면서 vector를 사용하였다.
뭐 워낙 클라이언트에서도 많이 쓰이고 있고, 해당 기능을 구현 하는 곳에서도 많이 쓰였기 때문에 별 생각 없이 코드를 작성, QA 도중에 이상한 문제가 발생 하기 시작했다.
구현된 기능중에 벡터 컨테이너에 저장된 구조체 배열 중에서 특정 값을 가진 아이템들을 전부 삭제 하는 기능이 있었다. 예를 들어서 유저 목록이라고 한다면 A라는 class id를 가진 모든 유저를 삭제 하는것과 같은 기능이다.
처음에 구현된 코드는 다음과 같았다.
iterator iterCur;
iterator iterEnd;
iterCur = this->MemberList.begin();
iterEnd = this->MemberList.end();
for ( ; iterCur != iterEnd ; iterCur++ )
{
if (iterCur->type == ipPacket->type) {
this->MemberList.erase(iterCur);
}
}
그런데 이 코드를 통해서는 정상적으로 기능이 구현 되지 않았다. 테스트 된 환경중에 2개의 데이터를 벡터에 넣어두고 둘다 지워질 수 밖에 없는 경우의 값을 대입시켰을때 조건이 명확함에도 한개만 삭제 되어졌다. VS의 디버거를 통해 알아본 결과 루프가 1회만 돌고 있기 때문이었다.
1회만 도는 원인이 반복자의 무효화 관계가 있을 거라고 생각하고는 erase 이후에 반복자를 다시 대입시켜 주는 코드로 변경해보았다.
iterator iterCur;
iterator iterEnd;
iterCur = this->MemberList.begin();
iterEnd = this->MemberList.end();
for ( ; iterCur != iterEnd ; iterCur++ )
{
if (iterCur->type == ipPacket->type) {
this->MemberList.erase(iterCur);
iterCur = this->MemberList.begin();
iterEnd = this->MemberList.end();
}
}
허나 이 역시 매한가지. 그리고는 곰곰히 생각해보았다. 그냥 벡터의 .size()를 통해서 루프를 돌릴까.. 하는 고민을 했으니 배열로 접근 할 경우 삭제가 불가능하기에 접고 저렇게 작동하는 원인을 찾기 위해 노력해 보았다. 그리고는 다음과 같은 결론을 내렸다.
일단 처음의 두 코드의 루프가 전부 돌지 않았던 것은 for 문 자체의 증감식문 때문이었다. 만약 처음의 첫 코드를 이용 할 경우 반복자의 무효화가 발생 할 경우 stl 내부에서 참조 오류를 내며 crash 되어버린다.
그것은 이미 무효화 되어 버린 반복자에 for의 증감식문이 영향을 줘서 그렇다. (처음 두 코드에서 for에 있는 iterCur++ 문) 따라서 루프 속에서 iterCur or iterEnd or iterEnd의 무효화가 일어날 경우를 대비해서 다시 대입 시켜 주어야 한다.
즉, 반복문 안에서 두가지 경우가 일어 날 수 있게 되는 것이다. 현재 반복문의 증감식으로 부여 받은 이터레이터가 유효한 경우는 지워지지 않은 경우이므로 증감식문을 이용하여 증감 시키고, 무효화가 일어나는 경우, 즉 삭제가 될 경우는 별도의 값으로 해당 이터레이터를 대입 시켜 주어 증감식문의 영향을 받지 않게 해야 한다.
하지만 만약 벡터가 삭제가 된 이터레이터 외에 앞 뒤 아이템의 위치 역시 변경 될 경우는 난감할 지도 모르겠다. (이런 경우가 있나?)
어쨋든 대략 이런 구조로 작성 하면 되겠다.
iterBegin = this->MemberList.begin();
iterEnd = this->MemberList.end();
for ( iterCur = iterBegin; iterCur != this->MemberList.end(); iterCur = iterNext ) {
iterNext = iterCur;
if (iterCur->type == ipPacket->type) {
MemberList.erase(iterCur);
반복문의 증감식을 이용 할 수 없으므로 별도의 값을 대입. 벡터 전체의 재정렬(전체 무효화)가 생기지 않는다면 루프의 증감식문의 영향을 받지 않은 정상 반복자를 별도로 저장 하고 해당 값을 대입해도 된다. 아래와 같이 begin을 대입하면 삭제가 이루어 질때마다 전체를 처음부터 다시 돌것이므로 성능 저하가 온다.
iterNext= this->MemberList.begin();
} else {
반복문의 증감식 대용
++iterNext;
}
어쨋든, for의 증감식문을 전혀 생각 안하고 버릇대로 손이 갔다가 고생좀 했다 ;;
---------------------------------
아래 리플을 보고 알았습니다. -_-. visual assist 쓰면서 몰랐다니. ㅡㅡ;
추가 vector의 erase의 반환값이 있었나 봅니다 -_-. 반환값이 current iterator인가 봅니다~
iterCur = this->MemberList.erase(iterCur);
으로 삭제 된 후의 이터레이터를 받아 올수 있는거 같군요.
--