插入映射的首选/惯用方法是什么?

我已经确定了在 std::map中插入元素的四种不同方法:

std::map<int, int> function;


function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

这两种方法中哪一种是首选的/惯用的方法? (还有没有其他我没有想到的方法?)

168581 次浏览

如果要用键0覆盖元素,请使用

function[0] = 42;

否则:

function.insert(std::make_pair(0, 42));

第一个版本:

function[0] = 42; // version 1

可能会也可能不会将值42插入到地图中。如果键 0存在,那么它将为该键分配42,覆盖该键的任何值。否则它将插入键/值对。

插入函数:

function.insert(std::map<int, int>::value_type(0, 42));  // version 2
function.insert(std::pair<int, int>(0, 42));             // version 3
function.insert(std::make_pair(0, 42));                  // version 4

另一方面,如果键 0已经存在于映射中,则不要执行任何操作。如果键不存在,则插入键/值对。

这三个插入函数几乎是相同的。std::map<int, int>::value_typestd::pair<const int, int>typedef,而 std::make_pair()显然通过模板演绎产生了 std::pair<>。但是,对于版本2、3和4,最终结果应该是相同的。

我该用哪个?我个人更喜欢第一版,它简洁而“自然”。当然,如果不需要它的覆盖行为,那么我更喜欢版本4,因为它比版本2和版本3需要更少的输入。我不知道是否有一种将键/值对插入到 std::map的单一 事实上方法。

通过映射的一个构造函数将值插入映射的另一种方法:

std::map<int, int> quadratic_func;


quadratic_func[0] = 0;
quadratic_func[1] = 1;
quadratic_func[2] = 4;
quadratic_func[3] = 9;


std::map<int, int> my_func(quadratic_func.begin(), quadratic_func.end());

首先,operator[]insert成员函数在功能上是不等价的:

  • operator[]搜索作为键,如果没有找到,则插入一个 默认构造值,并返回一个您为其分配值的引用。显然,如果可以直接初始化 mapped_type而不是默认构造和分配,那么这种方法可能效率低下。此方法还使得无法确定是否确实进行了插入,或者是否仅覆盖了以前插入的键的值
  • 如果键已经出现在映射中,那么 insert成员函数将不会起作用,并且,尽管它经常被遗忘,但返回一个可能有意义的 std::pair<iterator, bool>(最显著的是确定是否已经实际插入)。

从所有列出的调用 insert的可能性来看,这三种方法都是 差不多等效的。作为提醒,让我们看看 insert签名在标准中的表现:

typedef pair<const Key, T> value_type;


/* ... */


pair<iterator, bool> insert(const value_type& x);

这三个电话有什么不同?

  • std::make_pair依赖于模板参数推导,并且可以(在这种情况下是 威尔)产生与映射的实际 value_type不同的类型,这将需要额外调用 std::pair模板构造函数以转换为 value_type(即: 将 const添加到 first_type)
  • std::pair<int, int>还需要额外调用 std::pair的模板构造函数,以便将参数转换为 value_type(即: 将 const添加到 first_type)
  • std::map<int, int>::value_type绝对不容置疑,因为它直接是 insert成员函数所期望的参数类型。

最后,当目标是插入时,我会避免使用 operator[],除非在缺省构造和分配 mapped_type时没有额外的成本,而且我也不关心是否有效地插入了一个新键。当使用 insert时,构造一个 value_type可能是最好的方法。

如果要在 std: : map 中插入元素,请使用 insert ()函数,如果要查找元素(按键)并为其指定一些元素,请使用操作符[]。

为了简化插入操作,可以像下面这样:

using namespace boost::assign;


// For inserting one element:


insert( function )( 0, 41 );


// For inserting several elements:


insert( function )( 0, 41 )( 0, 42 )( 0, 43 );

从 C + + 11开始,你有两个主要的附加选项。首先,你可以使用带有列表初始化语法的 insert():

function.insert({0, 42});

这在功能上等同于

function.insert(std::map<int, int>::value_type(0, 42));

正如其他答案所指出的,这种格式比其他格式有几个优点:

  • operator[]方法要求映射的类型是可赋值的,但情况并非总是如此。
  • operator[]方法可以覆盖现有的元素,并且无法判断是否发生了这种情况。
  • 您列出的其他形式的 insert涉及隐式类型转换,这可能会降低代码的速度。

主要的缺点是这个表单过去要求键和值是可复制的,所以它不能用于例如带有 unique_ptr值的 map。标准中已经修复了这个问题,但是这个修复可能还没有达到您的标准库实现。

其次,可以使用 emplace()方法:

function.emplace(0, 42);

这比任何形式的 insert()都更简洁,适用于像 unique_ptr这样的仅移动类型,并且理论上可能稍微高效一些(尽管一个像样的编译器应该优化掉这种差异)。唯一的主要缺点是它可能会让读者有点吃惊,因为 emplace方法通常不是这样使用的。

我对上述版本进行了一些时间比较:

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

结果表明,插入版本之间的时间差异很小。

#include <map>
#include <vector>
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::posix_time;
class Widget {
public:
Widget() {
m_vec.resize(100);
for(unsigned long it = 0; it < 100;it++) {
m_vec[it] = 1.0;
}
}
Widget(double el)   {
m_vec.resize(100);
for(unsigned long it = 0; it < 100;it++) {
m_vec[it] = el;
}
}
private:
std::vector<double> m_vec;
};




int main(int argc, char* argv[]) {






std::map<int,Widget> map_W;
ptime t1 = boost::posix_time::microsec_clock::local_time();
for(int it = 0; it < 10000;it++) {
map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));
}
ptime t2 = boost::posix_time::microsec_clock::local_time();
time_duration diff = t2 - t1;
std::cout << diff.total_milliseconds() << std::endl;


std::map<int,Widget> map_W_2;
ptime t1_2 = boost::posix_time::microsec_clock::local_time();
for(int it = 0; it < 10000;it++) {
map_W_2.insert(std::make_pair(it,Widget(2.0)));
}
ptime t2_2 = boost::posix_time::microsec_clock::local_time();
time_duration diff_2 = t2_2 - t1_2;
std::cout << diff_2.total_milliseconds() << std::endl;


std::map<int,Widget> map_W_3;
ptime t1_3 = boost::posix_time::microsec_clock::local_time();
for(int it = 0; it < 10000;it++) {
map_W_3[it] = Widget(2.0);
}
ptime t2_3 = boost::posix_time::microsec_clock::local_time();
time_duration diff_3 = t2_3 - t1_3;
std::cout << diff_3.total_milliseconds() << std::endl;


std::map<int,Widget> map_W_0;
ptime t1_0 = boost::posix_time::microsec_clock::local_time();
for(int it = 0; it < 10000;it++) {
map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));
}
ptime t2_0 = boost::posix_time::microsec_clock::local_time();
time_duration diff_0 = t2_0 - t1_0;
std::cout << diff_0.total_milliseconds() << std::endl;


system("pause");
}

这分别给出了版本(我运行文件3次,因此每个版本有3个连续的时间差) :

map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));

2198毫秒,2078毫秒,2072毫秒

map_W_2.insert(std::make_pair(it,Widget(2.0)));

2290毫秒,2037毫秒,2046毫秒

 map_W_3[it] = Widget(2.0);

2592毫秒,2278毫秒,2296毫秒

 map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));

2234毫秒,2031毫秒,2027毫秒

因此,可以忽略不同插入版本之间的结果(但没有执行假设检验) !

由于使用了 Widget 的缺省构造函数进行初始化,这个例子中的 map_W_3[it] = Widget(2.0);版本需要多花费10-15% 的时间。

我只是稍微修改了一下问题(字符串映射) ,以显示插入的另一个兴趣:

std::map<int, std::string> rancking;


rancking[0] = 42;  // << some compilers [gcc] show no error


rancking.insert(std::pair<int, std::string>(0, 42));// always a compile error

编译器在“ rancking [1] = 42;”上没有显示错误的事实可能会产生毁灭性的影响!

简而言之,[]操作符更有效地更新值,因为它涉及调用值类型的缺省构造函数,然后为其分配一个新值,而 insert()操作符更有效地添加值。

引自 Scott Meyers 的 有效的 STL: 改善标准模板库使用的50个具体方法第24项的片段可能会有所帮助。

template<typename MapType, typename KeyArgType, typename ValueArgType>
typename MapType::iterator
insertKeyAndValue(MapType& m, const KeyArgType&k, const ValueArgType& v)
{
typename MapType::iterator lb = m.lower_bound(k);


if (lb != m.end() && !(m.key_comp()(k, lb->first))) {
lb->second = v;
return lb;
} else {
typedef typename MapType::value_type MVT;
return m.insert(lb, MVT(k, v));
}
}

您可能决定选择一个没有通用编程的版本,但关键是我发现这个范例(区分‘ add’和‘ update’)非常有用。

由于 C + + 17 std::map提供了两种新的插入方法: insert_or_assign()try_emplace(),也在 评论来自 sp2danny中提到。

insert_or_assign()

基本上,insert_or_assign()operator[]的“改进”版本。与 operator[]不同,insert_or_assign()不要求映射的值类型是默认可构造的。例如,下面的代码不能编译,因为 MyClass没有缺省构造函数:

class MyClass {
public:
MyClass(int i) : m_i(i) {};
int m_i;
};


int main() {
std::map<int, MyClass> myMap;


// VS2017: "C2512: 'MyClass::MyClass' : no appropriate default constructor available"
// Coliru: "error: no matching function for call to 'MyClass::MyClass()"
myMap[0] = MyClass(1);


return 0;
}

但是,如果用以下代码行替换 myMap[0] = MyClass(1);,那么代码就会编译,插入就会按预期进行:

myMap.insert_or_assign(0, MyClass(1));

而且,与 insert()类似,insert_or_assign()返回一个 pair<iterator, bool>。如果发生插入,布尔值为 true; 如果完成赋值,布尔值为 false。迭代器指向插入或更新的元素。

try_emplace()

与上述相似,try_emplace()emplace()的一个“改进”。与 emplace()不同,如果由于映射中已经存在一个键而导致插入失败,try_emplace()不会修改其参数。例如,下面的代码尝试将一个元素与一个已经存储在映射中的键放在一起(参见 *) :

int main() {
std::map<int, std::unique_ptr<MyClass>> myMap2;
myMap2.emplace(0, std::make_unique<MyClass>(1));


auto pMyObj = std::make_unique<MyClass>(2);
auto [it, b] = myMap2.emplace(0, std::move(pMyObj));  // *


if (!b)
std::cout << "pMyObj was not inserted" << std::endl;


if (pMyObj == nullptr)
std::cout << "pMyObj was modified anyway" << std::endl;
else
std::cout << "pMyObj.m_i = " << pMyObj->m_i <<  std::endl;


return 0;
}

输出(至少对于 VS2017和 Coliru) :

未插入 pMyObj
PMyObj 还是被修改了

如您所见,pMyObj不再指向原始对象。但是,如果用以下代码替换 auto [it, b] = myMap2.emplace(0, std::move(pMyObj));,那么输出看起来就不一样了,因为 pMyObj保持不变:

auto [it, b] = myMap2.try_emplace(0, std::move(pMyObj));

产出:

未插入 pMyObj
PMyObj pMyObjec.m _ i = 2

科利鲁密码

请注意: 我尽量使我的解释尽可能简短,以适应这个答案。为了更精确和全面的描述,我建议阅读 流利的 C + + 上的 这篇文章