前段时间,我和一位同事讨论了如何在 STL映射中插入值。我更喜欢map[key] = value;
它,因为它感觉自然并且读起来很清楚,而他更喜欢map.insert(std::make_pair(key, value))
。
我刚问过他,我们都不记得为什么 insert 更好,但我相信这不仅仅是一种风格偏好,而是效率等技术原因。SGI STL 参考简单地说: “严格来说,这个成员函数是不必要的:它只是为了方便而存在。”
谁能告诉我这个原因,还是我只是梦想有一个?
前段时间,我和一位同事讨论了如何在 STL映射中插入值。我更喜欢map[key] = value;
它,因为它感觉自然并且读起来很清楚,而他更喜欢map.insert(std::make_pair(key, value))
。
我刚问过他,我们都不记得为什么 insert 更好,但我相信这不仅仅是一种风格偏好,而是效率等技术原因。SGI STL 参考简单地说: “严格来说,这个成员函数是不必要的:它只是为了方便而存在。”
谁能告诉我这个原因,还是我只是梦想有一个?
当你写
map[key] = value;
无法判断您是替换了value
for key
,还是创建了一个新key
的value
.
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
.
当涉及到地图中已经存在的键时,两者具有不同的语义。所以它们并不能直接比较。
但是 operator[] 版本需要默认构造值,然后分配,所以如果这比复制构造更昂贵,那么它会更昂贵。有时默认构造没有意义,然后就不可能使用 operator[] 版本。
还有一点需要注意std::map
:
myMap[nonExistingKey];
将在映射中创建一个新条目,键控nonExistingKey
初始化为默认值。
当我第一次看到它时,这把我吓坏了(当我的头撞到一个令人讨厌的遗留错误时)。没想到。对我来说,这看起来像是一个 get 操作,我没想到会有“副作用”。map.find()
从您的地图上获取时更喜欢。
如果默认构造函数的性能影响不是问题,请看在上帝的份上,使用更易读的版本。
:)
insert
从异常安全的角度来看更好。
表达式map[key] = value
实际上是两个操作:
map[key]
- 创建具有默认值的地图元素。= value
- 将值复制到该元素中。第二步可能会出现异常。结果,操作将仅部分完成(一个新元素被添加到地图中,但该元素没有用 初始化value
)。操作未完成,但系统状态被修改的情况,称为有“副作用”的操作。
insert
操作提供了强有力的保证,意味着它没有副作用(https://en.wikipedia.org/wiki/Exception_safety)。insert
要么完全完成,要么使地图处于未修改状态。
http://www.cplusplus.com/reference/map/map/insert/:
如果要插入单个元素,则容器中不会发生异常情况下的更改(强保证)。
如果您的应用程序对速度要求很高,我建议您使用 [] 运算符,因为它会创建原始对象的 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;
}
现在在 c++11 中,我认为在 STL 映射中插入一对的最佳方法是:
typedef std::map<int, std::string> MyMap;
MyMap map;
auto& result = map.emplace(3,"Hello");
结果将是一对:
第一个元素(result.first),指向插入的对,如果键已经存在,则指向具有此键的对。
第二个元素(result.second),如果插入正确,则为 true,否则为 false,表示出现问题。
PS:如果您不关心订单,您可以使用 std::unordered_map ;)
谢谢!
map::insert() 的一个问题是,如果键已经存在于映射中,它不会替换值。我见过 Java 程序员编写的 C++ 代码,他们期望 insert() 的行为方式与 Java 中的 Map.put() 相同,其中值被替换。
需要注意的是,您也可以使用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);
}
这是一个相当有限的案例,但从我收到的评论来看,我认为值得注意。
我过去见过人们以以下形式使用地图
map< const key, const val> Map;
避免意外值覆盖的情况,然后继续编写其他一些代码:
const_cast< T >Map[]=val;
我记得他们这样做的原因是因为他们确信在这些特定的代码位中他们不会覆盖映射值。因此,继续使用更“可读”的方法[]
。
这些人编写的代码实际上从来没有给我带来任何直接的麻烦,但直到今天我都强烈地感觉到,当可以轻松避免风险时,不应该冒险——无论风险多么小。
如果您正在处理绝对不能被覆盖的地图值,请使用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 ) ;
}
insert()
std::map函数不会覆盖与键关联的值这一事实允许我们编写对象枚举代码,如下所示:
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()
和之间的区别operator[]
已经在其他答案中得到了很好的解释。但是, C++11和C++17std::map
分别引入了新的插入方法:
emplace()
中也提到了C++11 提供。insert_or_assign()
和try_emplace()
.让我简单总结一下“新”插入方法:
emplace()
:如果使用得当,该方法可以通过构造要插入的元素就地避免不必要的复制或移动操作。与 类似insert()
,只有在容器中没有具有相同键的元素时才会插入元素。insert_or_assign()
:此方法是operator[]
. 与 不同operator[]
,insert_or_assign()
不需要地图的值类型是默认可构造的。这克服了Greg Rogers 的回答中提到的缺点。try_emplace()
:此方法是emplace()
. 与emplace()
,不同,try_emplace()
如果由于映射中已经存在的键而导致插入失败,则不会修改其参数(由于移动操作)。有关详细信息insert_or_assign()
,请在此处try_emplace()
查看我的回答。