Matt Kulukundis (kfm@google.com) 작성
최초 게시일: 2017년 3월 30일
최종 업데이트: 2019년 11월 25일
빠른 링크: abseil.io/tips/132
“이곳이 바로 스나크가 있을 곳이야!” 선장이 외쳤다.
그는 조심스럽게 그의 승무원들을 상륙시키며,
조수의 꼭대기에 머물며 머리카락을 손가락으로 얽어 쥐고 있었다.
“이곳이 바로 스나크가 있을 곳이야! 나는 두 번 말했다.
그것만으로도 승무원들에게 용기를 줄 것이다.
이곳이 바로 스나크가 있을 곳이야! 나는 세 번 말했다.
내가 세 번 말한 것은 사실이다.”
– 루이스 캐럴, The Hunting of the Snark에서 발췌
C++에서 연관 컨테이너(associative containers)는 매우 자주 사용되는 추상화 도구입니다. 그러나 종종 불필요한 작업을 수행하게 되는 경우가 많습니다. 이번 글에서는 이러한 추가 비용을 피할 수 있는 몇 가지 요령을 소개합니다.
결과 누적하기
종종 map은 공통 키를 수집하고 정보를 누적하는 데 사용됩니다. 예를 들어:
// 좋지 않은 코드: 문자열 키가 두 map에 복사됨
absl::flat_hash_map<std::string, int> word_counts;
absl::flat_hash_map<std::string, std::vector<std::string>> word_origins;
for (const auto& [origin, words] : origin_to_words) {
for (const std::string& w : words) {
if (!word_counts.contains(w)) { // 첫 번째 조회
InsertOrDie(&word_counts, w, 0); // 두 번째 조회; 추가 문자열 복사
}
++word_counts[w]; // 세 번째 조회
if (!word_origins.contains(w)) { // 네 번째 조회
InsertOrDie(&word_origins, w, {}); // 다섯 번째 조회; 추가 문자열 복사
}
word_origins[w].push_back(origin); // 여섯 번째 조회
}
}
operator[]
가 이미 존재하지 않는 키에 대해 값 초기화된 인스턴스를 삽입한다는 사실을 활용하면, 다음과 같이 간결하게 작성할 수 있습니다:
// 문자열 키는 두 map에 복사됨
absl::flat_hash_map<std::string, int> word_counts;
absl::flat_hash_map<std::string, std::vector<std::string>> word_origins;
for (const auto& [origin, words] : origin_to_words) {
for (const std::string& w : words) {
++word_counts[w]; // 첫 번째 조회
word_origins[w].push_back(origin); // 두 번째 조회
}
}
더 나아가, 키 중복 조회와 저장을 피하기 위해 두 map을 하나의 구조체로 결합할 수도 있습니다.
struct WordInfo {
int count = 0;
std::vector<std::string> origins;
};
absl::flat_hash_map<std::string, WordInfo> words_to_info;
for (const auto& [origin, words] : origin_to_words) {
for (const std::string& w : words) {
auto& info = words_to_info[w];
++info.count;
info.origins.push_back(origin);
}
}
이 패턴은 기본값이 누적의 초기 상태와 일치하는 경우에 유용합니다. 또한 코드 가독성도 높아집니다.
초기화 작업 한 번에 처리하기
때로는 map이 복잡한 객체나 무거운 연산의 결과를 캐싱하기 위해 사용됩니다.
// 좋지 않은 코드
class CobblerCache {
public:
const CobblerInterface& GetCobbler(const std::string& key) {
if (!cobblers_.contains(key)) { // 첫 번째 조회
InsertOrDie(&cobblers_, key, FluevogMaker::Create()); // 두 번째 조회
}
return *FindOrDie(cobblers_, key); // 세 번째 조회
}
private:
absl::flat_hash_map<std::string, std::unique_ptr<CobblerInterface>> cobblers_;
};
operator[]
가 새로운 값을 삽입할 때 std::unique_ptr
이 기본적으로 null로 초기화된다는 점을 활용할 수 있습니다.
class CobblerCache {
public:
const CobblerInterface& GetCobbler(const std::string& key) {
auto& cobbler = cobblers_[key];
if (cobbler == nullptr) {
cobbler = FluevogMaker::Create();
}
return *cobbler;
}
private:
absl::flat_hash_map<std::string, std::unique_ptr<CobblerInterface>> cobblers_;
};
cobbler
는 map 내부 값을 참조하므로 operator[]
호출 이후 추가 작업 없이 값을 설정할 수 있습니다.
안전한 조회
때로는 map에서 항목을 찾고, 실패할 경우 안전하게 빠져나오고 싶을 때가 있습니다.
// 좋지 않은 코드
class CobblerCache {
public:
std::unique_ptr<Shoe> MaybeMakeShoe(const std::string& key,
const ShoeSpec& spec) {
if (!cobblers_.contains(key)) return nullptr; // 첫 번째 조회
return FindOrDie(cobblers_, key)->MakeShoe(spec); // 두 번째 조회
}
private:
absl::flat_hash_map<std::string, std::unique_ptr<CobblerInterface>> cobblers_;
};
다음과 같이 작성하는 것이 더 나은 방법입니다.
class CobblerCache {
public:
std::unique_ptr<Shoe> MaybeMakeShoe(const std::string& key,
const ShoeSpec& spec) {
auto it = cobblers_.find(key);
if (it == cobblers_.end()) return nullptr;
return it->second->MakeShoe(spec);
}
};
중복 항목 세기
때로는 map에 없는 항목을 삽입하고, 그렇지 않은 경우 특정 작업을 수행하고 싶을 수 있습니다.
// 좋지 않은 코드
int duplicates = 0;
absl::flat_hash_set<std::string> seen;
for (const std::string& id : ids) {
if (seen.contains(id)) { // 첫 번째 조회
++duplicates;
} else {
seen.insert(id); // 두 번째 조회
}
}
absl::flat_hash_set::insert
는 삽입된 요소의 반복자와 삽입 여부를 나타내는 bool 값을 반환하므로 이를 활용할 수 있습니다.
int duplicates = 0;
absl::flat_hash_set<std::string> seen;
for (const std::string& id : ids) {
if (!seen.insert(id).second) {
++duplicates;
}
}
최선의 사용법
연관 컨테이너를 효율적으로 사용하는 동시에 가독성을 희생하지 않는 방법이 종종 있습니다. 이러한 API와 사용법을 배우고 활용하세요. 컨테이너 유형은 매우 자주 사용되기 때문에 이러한 기법에 익숙하다고 가정해도 좋습니다.