C++中的map find()如何转换为可能插入的操作?如何优化这个过程?

36

我使用STL的map数据结构,在我的代码中首先调用find():如果键之前不存在于map中,它会调用insert(),否则什么也不做。

map<Foo*, string>::iterator it;
it = my_map.find(foo_obj);   // 1st lookup

if(it == my_map.end()){
  my_map[foo_obj] = "some value";  // 2nd lookup
}else{
  // ok do nothing.
}
我在想是否有更好的方法,因为据我所知,在这种情况下,当我想要插入一个尚未存在的键时,我需要在映射数据结构中执行2个查找操作:一个用于find(),一个用于insert()(对应于operator[])。

提前感谢任何建议。

3个回答

40

通常,如果您进行查找和插入操作,那么如果已经存在,则要保留(并检索)旧值。如果您只想覆盖旧值,map[foo_obj]="some value"可以实现这一点。

以下是如何使用一个 map 查找获取旧值或插入新值(如果不存在):

typedef std::map<Foo*,std::string> M;
typedef M::iterator I;
std::pair<I,bool> const& r=my_map.insert(M::value_type(foo_obj,"some value"));
if (r.second) { 
    // value was inserted; now my_map[foo_obj]="some value"
} else {
    // value wasn't inserted because my_map[foo_obj] already existed.
    // note: the old value is available through r.first->second
    // and may not be "some value"
}
// in any case, r.first->second holds the current value of my_map[foo_obj]

这是一个常见的习语,您可能希望使用一个辅助函数:

template <class M,class Key>
typename M::mapped_type &
get_else_update(M &m,Key const& k,typename M::mapped_type const& v) {
    return m.insert(typename M::value_type(k,v)).first->second;
}

get_else_update(my_map,foo_obj,"some value");

如果您有一个昂贵的计算方式用于v,如果它已经存在就想要跳过它(例如记忆化),那么您也可以进行推广:

template <class M,class Key,class F>
typename M::mapped_type &
get_else_compute(M &m,Key const& k,F f) {
   typedef typename M::mapped_type V;
   std::pair<typename M::iterator,bool> r=m.insert(typename M::value_type(k,V()));
   V &v=r.first->second;
   if (r.second)
      f(v);
   return v;
}

例如,其中

struct F {
  void operator()(std::string &val) const 
  { val=std::string("some value")+" that is expensive to compute"; }
};
get_else_compute(my_map,foo_obj,F());

如果映射类型不可默认构造,则需要使F提供一个默认值,或者在get_else_compute中添加另一个参数。

好的,这确实是唯一的方法,之前关于operator[]的评论是错误的(如果键已经在映射中,则会覆盖先前的键/值对)。 - puccio

13

有两种主要方法。第一种方法是使用插入函数,该函数接受一个值类型并返回一个迭代器和一个布尔值,指示是否进行了插入,并返回一个迭代器,该迭代器指向具有相同键或新插入元素的现有元素。

map<Foo*, string>::iterator it;
it = my_map.find(foo_obj);   // 1st lookup

my_map.insert( map<Foo*, string>::value_type(foo_obj, "some_value") );

这种方法的优点是简单易行。主要缺点是,无论是否需要插入,您始终会构造第二个参数的新值。对于字符串来说,这可能并不重要。但是如果您要构造的值很昂贵,那么这可能比必要的浪费更加浪费。

解决这个问题的方法是使用insert的“提示”版本。

std::pair< map<foo*, string>::iterator, map<foo*, string>::iterator >
    range = my_map.equal_range(foo_obj);

if (range.first == range.second)
{
    if (range.first != my_map.begin())
        --range.first;

    my_map.insert(range.first, map<Foo*, string>::value_type(foo_obj, "some_value") );
}

仅当元素立即在提供的迭代器之后插入时,插入操作才保证以摊销常数时间完成,这就是为什么需要使用 -- 的原因(如果可能)。

编辑

如果这种需要使用--的情况看起来很奇怪,那么这确实是这样。标准中存在一个未解决的缺陷(233),虽然关于 map 的问题描述在重复的问题246中更为清晰。


1
"hint"版本对于hash_map不可用,但对于map来说可能比为我的get_else_compute定义一个lambda函数更简单,其中我默认构造然后修改返回的迭代器的值(当然你可以在一行内完成这个操作,而不需要使用lambda函数)。 - Jonathan Graehl
通过hash_map,我想我指的是tr1或C++0x中的std::unordered_map。 - Jonathan Graehl
确实,如果插入恰好发生在提示之前,那么提示不能保证是摊销常数时间,这也让我感到困扰,因为这只能在容器的开头和结尾起作用。 - CB Bailey
Dinkumware std::map确保“如果插入点紧接在前面或后面,则插入可以在摊销常数时间内发生,而不是对数时间。” - pgast

2

在您的例子中,您希望在未找到时进行插入。如果默认构造并在此之后设置值不是很昂贵的话,我建议使用更简单的版本,只需1次查找:

string& r = my_map[foo_obj];    // only lookup & insert if not existed
if (r == "") r = "some value";  // if default (obj wasn't in map), set value
                                // else existed already, do nothing

如果您的示例说明了您实际想要的内容,请考虑将该值添加为str Foo :: s ,因为您已经拥有该对象,所以不需要进行查找,只需检查它是否具有该成员的默认值即可。并且将objs保留在std :: set 中。即使扩展class FooWithValue2也可能比使用map更便宜。

但是,如果真的需要通过map连接数据,或者如果只想在存在时更新数据,则Jonathan有答案。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接