Matt Kulukundis (kfm@google.com) 작성
최초 게시일: 2017년 3월 30일
최종 업데이트: 2019년 11월 25일

빠른 링크: abseil.io/tips/132


“이곳이 바로 스나크가 있을 곳이야!” 선장이 외쳤다.
그는 조심스럽게 그의 승무원들을 상륙시키며,
조수의 꼭대기에 머물며 머리카락을 손가락으로 얽어 쥐고 있었다.

“이곳이 바로 스나크가 있을 곳이야! 나는 두 번 말했다.
그것만으로도 승무원들에게 용기를 줄 것이다.
이곳이 바로 스나크가 있을 곳이야! 나는 세 번 말했다.
내가 세 번 말한 것은 사실이다.”

– 루이스 캐럴, The Hunting of the Snark에서 발췌


C++에서 연관 컨테이너(associative containers)는 매우 자주 사용되는 추상화 도구입니다. 그러나 종종 불필요한 작업을 수행하게 되는 경우가 많습니다. 이번 글에서는 이러한 추가 비용을 피할 수 있는 몇 가지 요령을 소개합니다.


결과 누적하기

종종 map은 공통 키를 수집하고 정보를 누적하는 데 사용됩니다. 예를 들어:

CPP
// 좋지 않은 코드: 문자열 키가 두 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[]가 이미 존재하지 않는 키에 대해 값 초기화된 인스턴스를 삽입한다는 사실을 활용하면, 다음과 같이 간결하게 작성할 수 있습니다:

CPP
// 문자열 키는 두 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을 하나의 구조체로 결합할 수도 있습니다.

CPP
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이 복잡한 객체나 무거운 연산의 결과를 캐싱하기 위해 사용됩니다.

CPP
// 좋지 않은 코드
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로 초기화된다는 점을 활용할 수 있습니다.

CPP
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에서 항목을 찾고, 실패할 경우 안전하게 빠져나오고 싶을 때가 있습니다.

CPP
// 좋지 않은 코드
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_;
};
클릭하여 더 보기

다음과 같이 작성하는 것이 더 나은 방법입니다.

CPP
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에 없는 항목을 삽입하고, 그렇지 않은 경우 특정 작업을 수행하고 싶을 수 있습니다.

CPP
// 좋지 않은 코드
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 값을 반환하므로 이를 활용할 수 있습니다.

CPP
int duplicates = 0;
absl::flat_hash_set<std::string> seen;

for (const std::string& id : ids) {
  if (!seen.insert(id).second) {
    ++duplicates;
  }
}
클릭하여 더 보기

최선의 사용법

연관 컨테이너를 효율적으로 사용하는 동시에 가독성을 희생하지 않는 방법이 종종 있습니다. 이러한 API와 사용법을 배우고 활용하세요. 컨테이너 유형은 매우 자주 사용되기 때문에 이러한 기법에 익숙하다고 가정해도 좋습니다.


라이선스

저작자: Jaehun Ryu

링크: https://jaehun.me/posts/abseil-tip-132-avoid-redundant-map-lookups/

라이선스: CC BY 4.0

이 저작물은 크리에이티브 커먼즈 저작자표시 4.0 국제 라이선스에 따라 이용할 수 있습니다. 출처를 밝히면 상업적 목적을 포함해 자유롭게 이용 가능합니다.

댓글

검색 시작

검색어를 입력하세요

↑↓
ESC
⌘K 단축키