C++ 기초부터 마스터까지 — 10화
STL 컨테이너와 optional 선택하기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
게임에는 파티 목록, 몬스터 검색, 스폰 대기열, 처치 기록처럼 서로 다른 형태의 데이터가 존재한다. 모든 데이터를 vector 하나로 해결할 수는 있지만 목적에 맞는 자료구조를 쓰는 편이 코드가 더 명확하다.
vector
std::vector는 연속된 메모리를 사용하는 동적 배열이다.
std::vector<int> damages;
damages.reserve(10);
damages.push_back(10);
damages.emplace_back(20);
for (int damage : damages)
{
std::cout << damage << '\n';
}
원소는 [] 또는 at()으로 접근한다. at()은 범위를 검사하지만 []는 검사하지 않는다.
vector의 용량이 부족하면 더 큰 메모리를 새로 만들고 기존 원소를 이동하는 재할당이 발생한다. 이때 기존 원소를 가리키던 포인터, 참조, 반복자는 무효화될 수 있다.
unordered_map
아이템 ID로 아이템을 빠르게 찾으려면 해시 테이블인 std::unordered_map이 어울린다.
std::unordered_map<int, Item> itemDatabase;
itemDatabase.emplace(1, Item{1, "Sword", 100});
auto iter = itemDatabase.find(1);
if (iter != itemDatabase.end())
{
std::cout << iter->second.name << '\n';
}
find()가 실패하면 end()를 반환한다. 찾았는지 확인하지 않고 반복자를 역참조하면 안 된다.
queue와 stack
queue는 먼저 들어온 값이 먼저 나오는 FIFO 구조이다. 스폰 대기열에 잘 맞는다.
if (!spawnQueue.empty())
{
auto monster = spawnQueue.front();
spawnQueue.pop();
}
stack은 마지막에 들어온 값이 먼저 나오는 LIFO 구조이다. 최근 처치 기록이나 실행 취소에 사용할 수 있다.
if (!killHistory.empty())
{
int lastMonsterId = killHistory.top();
killHistory.pop();
}
pop은 값을 반환하지 않는다. front 또는 top으로 값을 먼저 얻은 다음 pop을 호출해야 한다.
optional
검색 결과가 없을 수 있다면 특수한 오류값 대신 std::optional을 사용할 수 있다.
std::optional<Item> FindItem(int id) const
{
auto iter = itemDatabase.find(id);
if (iter == itemDatabase.end())
{
return std::nullopt;
}
return iter->second;
}
if (auto item = FindItem(1))
{
std::cout << item->name << '\n';
}
값이 있을 수도 있고 없을 수도 있다는 사실이 반환형에 드러난다. 빈 optional에 무작정 value()를 호출하면 안 되며 먼저 존재 여부를 검사해야 한다.
자료구조를 선택하는 기준은 유행이 아니라 사용 목적이다. 순서 있는 목록은 vector, 키 검색은 unordered_map, 대기 순서는 queue, 최근 기록은 stack, 값의 부재는 optional로 표현할 수 있다.