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

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

我只是问了他,我们都不记得为什么插入更好,但我相信这不仅仅是一个风格偏好,而是有一个技术原因,如效率。SGI STL引用简单地说:“严格地说,这个成员函数是不必要的:它只是为了方便而存在。”

有人能告诉我原因吗,还是我只是在做梦?

164885 次浏览

当涉及到映射中已经存在的键时,两者具有不同的语义。所以它们没有直接的可比性。

但是操作符[]版本需要默认构造值,然后赋值,所以如果这比复制构造更昂贵,那么它也会更昂贵。有时默认结构没有意义,那么就不可能使用操作符[]版本。

如果默认构造函数的性能影响不是问题,看在上帝的份上,请使用可读性更好的版本。

:)

当你写作的时候

map[key] = value;

没有办法知道你是为key 取代 value,还是用value 创建一个新的key

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

map::insert()的一个问题是,如果键已经存在于映射中,它不会替换某个值。我见过Java程序员编写的c++代码,他们希望insert()的行为与Java中的Map.put()相同,其中值被替换。

注意,你也可以使用提振。分配:

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);
}

关于std::map需要注意的另一件事:

myMap[nonExistingKey];将在映射中创建一个新条目,键值为nonExistingKey,初始化为默认值。

当我第一次看到它时(当我的头撞到一个讨厌的遗留bug时),这把我吓坏了。没想到会这样。对我来说,这看起来像一个get操作,我没有预料到“副作用”。当从你的地图获取时,更倾向于map.find()

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

我以前见过有人用地图的形式

map< const key, const val> Map;

为了避免意外重写值的情况,然后继续写一些其他的代码:

const_cast< T >Map[]=val;

我记得他们这么做的原因是因为他们确信在这些特定的代码中他们不会覆盖map值;因此,继续使用更“可读”的方法[]

实际上,我从来没有因为这些人写的代码而遇到过任何直接的麻烦,但直到今天,我仍然强烈地感觉到,当风险可以轻松避免时,不应该冒任何风险——无论风险有多小。

当你处理的映射值绝对不能被覆盖时,使用insert。不要仅仅为了可读性而破例。

下面是另一个例子,显示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 ) ;


}

如果你的应用程序是速度关键,我会建议使用[]操作符,因为它创建了原始对象的总共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()时输出 Output when [] operator is used

.

现在在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;)

谢谢!

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;
}

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

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

  1. map[key] -创建一个默认值的map元素。
  2. = value -将值复制到该元素中。

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

insert操作提供了强有力的保证,意味着它没有副作用(https://en.wikipedia.org/wiki/Exception_safety)。insert要么完全完成,要么使映射处于未修改状态。

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

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

insert()operator[]之间的区别已经在其他答案中得到了很好的解释。然而,c++ 11c++ 17分别引入了新的std::map插入方法:

让我简单总结一下“新”;插入方法:

  • emplace():如果正确使用,此方法可以通过构造要插入的元素来避免不必要的复制或移动操作。与insert()类似,只有在容器中没有具有相同键值的元素时,才会插入一个元素。
  • insert_or_assign():此方法是“改进的”;operator[]的版本。与operator[]不同,insert_or_assign()不要求映射的值类型为默认可构造的。这克服了例如在Greg Rogers的回答中提到的缺点。
  • try_emplace():此方法是“改进的”;emplace()的版本。与emplace()不同,如果由于映射中已经存在键而导致插入失败,try_emplace()不会修改其参数(由于移动操作)。

有关insert_or_assign()try_emplace()的更多详细信息,请参阅我的回答在这里

Coliru上的简单示例代码