在STL映射中,使用map :: insert比[]更好吗?


201

前一段时间,我与一位同事讨论了如何在STL 映射中插入值。我更喜欢, map[key] = value; 因为它感觉自然并且易于阅读,而他更喜欢 map.insert(std::make_pair(key, value))

我只是问他,我们俩都不记得插入效果更好的原因,但是我确信这不仅是样式偏好,还有技术上的原因,例如效率。在SGI STL参考只是说“严格地说,这个成员函数是不必要的:它的存在只是为了方便。”

谁能告诉我这个原因,还是我只是梦想有一个原因?


2
感谢您的所有宝贵意见-他们真的很有帮助。这是一个最佳的堆栈溢出演示。我为应该接受哪个答案而感到沮丧:netjeff对不同的行为更为明确,格雷格·罗杰斯(Greg Rogers)提到了性能问题。希望我能同时勾选两个。
danio

6
实际上,对于C ++ 11,最好使用map ::
emplace

@einpoklum:实际上,Scott Meyers在他的演讲“不断发展的对有效C ++的搜索”中提出了其他建议。
Thomas Eding

3
@einpoklum:嵌入新构建的内存时就是这种情况。但是由于对地图的某些标准要求,所以出于技术原因,Emplace可能比插入慢。演讲可以在youtube上免费获得,例如,此链接youtube.com/watch?v=smqT9Io_bKo @〜38-40分钟。对于一个SO链接,这里的stackoverflow.com/questions/26446352/...
托马斯Eding

1
实际上,我会与Meyers提出的一些观点进行辩论,但这超出了此评论主题的范围,无论如何,我想我必须撤回我先前的评论。
einpoklum 2015年

Answers:


240

当你写

map[key] = value;

无法判断是否替换valuefor key或是否使用创建了新keyvalue

map::insert() 只会创建:

using std::cout; using std::endl;
typedef std::map<int, std::string> MyMap;
MyMap map;
// ...
std::pair<MyMap::iterator, bool> res = map.insert(MyMap::value_type(key,value));
if ( ! res.second ) {
    cout << "key " <<  key << " already exists "
         << " with value " << (res.first)->second << endl;
} else {
    cout << "created key " << key << " with value " << value << endl;
}

对于我的大多数应用程序,我通常不在乎是创建还是替换,因此我使用了易于阅读的map[key] = value


16
应该注意的是map :: insert永远不会替换值。在一般情况下,我会说最好使用(res.first)->second而不是value第二种情况。
dalle

1
我更新了一下,以更清楚地知道map :: insert永远不会取代。我离开了,else因为我认为使用value方法比迭代器更清晰。仅当值的类型具有异常的副本ctor或op ==时,它才会有所不同,并且使用Map这样的STL容器,该类型将导致其他问题。
netjeff

1
map.insert(std::make_pair(key,value))应该是map.insert(MyMap::value_type(key,value))。返回make_pair的类型与所采用的类型不匹配,insert并且当前解决方案需要转换
大卫·罗德里格斯- dribeas

1
有一种方法可以告诉您是插入还是刚刚分配了operator[],只需比较前后的大小即可。Imho只能调用map::operator[]默认的可构造类型更为重要。
idclev 463035818'2

@DavidRodríguez-dribeas:好的建议,我在答案中更新了代码。
netjeff

53

对于地图中已存在的键,两者具有不同的语义。因此它们并不是真正可直接比较的。

但是operator []版本需要默认构造值,然后进行赋值,因此,如果这要比复制构造昂贵,那么它将更加昂贵。有时默认构造没有意义,因此不可能使用operator []版本。


1
make_pair可能需要一个复制构造函数-比默认构造函数差。还是+1。

1
正如您所说,最主要的是它们具有不同的语义。因此,哪一种都不比另一种更好,只要使用一种可以满足您需求的工具即可。
jalf

为什么复制构造函数比默认的构造函数(赋值后)差?如果是的话,那么编写该类的人会错过一些东西,因为无论operator =做什么,他们都应该在复制构造函数中完成相同的操作。
史蒂夫·杰索普

1
有时默认构造与分配本身一样昂贵。自然地,分配和复制构造将是等效的。
格雷格·罗杰斯

@Arkadiy在优化的构建中,编译器通常会删除许多不必要的副本构造函数调用。
antant

35

另一件事要注意std::map

myMap[nonExistingKey];会在地图中创建一个新条目,将nonExistingKey其设置为初始化为默认值。

当我第一次看到它时,这让我感到非常恐惧(同时将我的脑袋撞向一个残旧的错误)。没想到的。对我来说,这看起来像是get操作,而且我没想到会有“副作用”。不想map.find()从你的地图时获得。


3
这是一个不错的视图,尽管哈希映射对于这种格式非常普遍。这可能是那些“古怪,没有人觉得奇怪”的,只是因为他们是如何广泛使用相同的公约之一
斯蒂芬Ĵ

19

如果默认构造函数的性能影响不成问题,那么出于对上帝的热爱,请使用可读性更高的版本。

:)


5
第二!要标记这个。太多的人为了获得纳秒级的加速而牺牲了钝度。怜悯我们必须维持这种暴行的可怜的灵魂!
Ree先生

6
正如格雷格·罗杰斯(Greg Rogers)所说:“在地图上已经存在的密钥上,这两种语言具有不同的语义。因此,它们并不是真正可直接比较的。”
dalle

这是一个古老的问题和答案。但是“更具可读性的版本”是一个愚蠢的原因。因为最容易阅读取决于人。
vallentin

14

insert 从异常安全性的角度来看更好。

该表达式map[key] = value实际上是两个操作:

  1. map[key] -使用默认值创建地图元素。
  2. = value -将值复制到该元素中。

第二步可能会发生异常。结果,该操作将仅部分完成(将新元素添加到map中,但该元素未使用初始化value)。当操作未完成但系统状态被修改时的情况称为“有副作用”的操作。

insert操作提供了有力的保证,这意味着它没有副作用(https://en.wikipedia.org/wiki/Exception_safety)。insert是完全完成还是使地图保持未修改状态。

http://www.cplusplus.com/reference/map/map/insert/

如果要插入单个元素,则在发生异常的情况下容器中不会有任何更改(强烈保证)。


1
更重要的是,insert不需要值是默认可构造的。
UmNyobe

13

如果您的应用程序对速度有严格要求,我会建议您使用[]运算符,因为它会创建原始对象的总共3个副本,其中2个是临时对象,并且早晚会被破坏为。

但是在insert()中,创建了原始对象的4个副本,其中3个是临时对象(不一定是“临时”)并被销毁了。

这意味着需要更多时间:1.一个对象内存分配2.一个额外的构造函数调用3.一个额外的析构函数调用4.一个对象内存释放

如果您的对象很大,则构造函数很典型,析构函数会释放大量资源,超过这些数量甚至更多。关于可读性,我认为两者都足够公平。

我想到了同样的问题,但不是可读性而是速度。这是一个示例代码,通过它我了解了我提到的要点。

class Sample
{
    static int _noOfObjects;

    int _objectNo;
public:
    Sample() :
        _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside default constructor of object "<<_objectNo<<std::endl;
    }

    Sample( const Sample& sample) :
    _objectNo( _noOfObjects++ )
    {
        std::cout<<"Inside copy constructor of object "<<_objectNo<<std::endl;
    }

    ~Sample()
    {
        std::cout<<"Destroying object "<<_objectNo<<std::endl;
    }
};
int Sample::_noOfObjects = 0;


int main(int argc, char* argv[])
{
    Sample sample;
    std::map<int,Sample> map;

    map.insert( std::make_pair<int,Sample>( 1, sample) );
    //map[1] = sample;
    return 0;
}

使用insert()时的输出 使用[]运算符时的输出


4
现在,在启用全面优化的情况下再次运行该测试。
antant

2
另外,请考虑运算符[]的实际作用。它首先在地图上搜索与指定键匹配的条目。如果找到一个,则用指定的那个覆盖该条目的值。如果没有,它将插入具有指定键和值的新条目。地图越大,运算符[]搜索地图所花费的时间就越长。在某些时候,这将足以弥补额外的复制调用(如果在编译器完成其优化魔术后,即使保留在最终程序中)。
antant

1
@antred,insert必须进行相同的搜索,因此与之没有区别[](因为映射键是唯一的)。
Sz。

很好地说明了打印输出的情况-但是这两段代码实际上在做什么?为什么根本需要3个原始对象副本?
rbennett485

1.必须实现赋值运算符,否则您将在析构函数中得到错误的数字。2.构造对时,请使用std :: move以避免过多的复制构造“ map.insert(std :: make_pair <int,Sample>((1,std: :move(sample)));“
2016年

10

现在在c ++ 11中,我认为在STL映射中插入一对的最佳方法是:

typedef std::map<int, std::string> MyMap;
MyMap map;

auto& result = map.emplace(3,"Hello");

结果将是具有一对:

  • 第一个元素(result.first),指向插入的对,或者如果此键已经存在,则指向具有该键的对。

  • 第二个元素(result.second),如果插入正确或错误,则为true,则出问题。

PS:如果您不关心订单,可以使用std :: unordered_map;)

谢谢!


9

map :: insert()的一个陷阱是,如果密钥已经存在于映射中,它将不会替换值。我看过Java程序员编写的C ++代码,他们期望insert()的行为与Java中替换值的Map.put()相同。


2

请注意,您还可以使用Boost.Assign

using namespace std;
using namespace boost::assign; // bring 'map_list_of()' into scope

void something()
{
    map<int,int> my_map = map_list_of(1,2)(2,3)(3,4)(4,5)(5,6);
}

1

这是另一个示例,显示了如果存在键,则operator[] 覆盖键的值,如果存在,.insert 则不覆盖键的值。

void mapTest()
{
  map<int,float> m;


  for( int i = 0 ; i  <=  2 ; i++ )
  {
    pair<map<int,float>::iterator,bool> result = m.insert( make_pair( 5, (float)i ) ) ;

    if( result.second )
      printf( "%d=>value %f successfully inserted as brand new value\n", result.first->first, result.first->second ) ;
    else
      printf( "! The map already contained %d=>value %f, nothing changed\n", result.first->first, result.first->second ) ;
  }

  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

  /// now watch this.. 
  m[5]=900.f ; //using operator[] OVERWRITES map values
  puts( "All map values:" ) ;
  for( map<int,float>::iterator iter = m.begin() ; iter !=m.end() ; ++iter )
    printf( "%d=>%f\n", iter->first, iter->second ) ;

}

1

这是一个相当有限的案例,但是从我收到的评论来看,我认为这是值得注意的。

我以前看过人们以以下形式使用地图

map< const key, const val> Map;

避免意外值覆盖的情况,但接着继续编写其他一些代码:

const_cast< T >Map[]=val;

我记得他们这样做的原因是因为他们确定在这些特定的代码位中它们不会覆盖映射值。因此,继续采用更具“可读性”的方法[]

我从未真正从这些人编写的代码中遇到任何直接的麻烦,但是直到今天,我坚信直到可以轻易避免的风险(无论风险多么小)都不应被承担。

如果要处理绝对不能覆盖的映射值,请使用insert。不要仅仅为了可读性而设置例外。


有多个不同的人写过那个?当然使用insert(not input),因为const_cast将会导致任何先前的值被覆盖,这是非常不固定的。或者,不要将值类型标记为const。(这类事情通常是的最终结果const_cast,因此几乎总是一个红旗表示其他地方存在错误。)
Potatoswatter 2012年

@Potatoswatter你是对的。我只是看到const_cast []在某些人确定它们不会在某些代码位中替换旧值时与const映射值一起使用;因为[]本身更具可读性。正如我在回答的最后一点中提到的那样,建议insert您在要防止值被覆盖的情况下使用。(只需将其更改inputinsert-谢谢)
dk123

@Potatoswatter如果我没记错的话,人们似乎使用的主要原因const_cast<T>(map[key])是:1. []更具可读性; 2.他们对某些代码有信心,他们不会覆盖值;以及3.他们没有希望其他未知代码覆盖它们的值-因此const value
dk123

2
我从来没有听说过这样做。您在哪里看到的?写作const_cast似乎不能消除的额外“可读性” [],而这种信心几乎足以解雇开发人员。棘手的运行时条件是通过防弹设计而不是直觉来解决的。
Potatoswatter 2012年

@Potatoswatter我记得那是我过去开发教育游戏的工作之一。我永远无法让人们编写代码来改变他们的习惯。您绝对正确,我非常同意您的看法。根据您的评论,我认为这可能比我原来的答案更值得注意,因此,我对其进行了更新以反映这一点。谢谢!
dk123

1

std :: map insert()函数不会覆盖与键关联的值的事实使我们可以编写如下的对象枚举代码:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    dict.insert(make_pair(word, dict.size()));
}

当我们需要将不同的非唯一对象映射到范围为0..N的某些id时,这是一个非常常见的问题。这些ID可以稍后在例如图算法中使用。operator[]在我看来,替代似乎不太可读:

string word;
map<string, size_t> dict;
while(getline(cin, word)) {
    size_t sz = dict.size();
    if (!dict.count(word))
        dict[word] = sz; 
} 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.