什么是向映射表中插入元素的首选/惯用方式?

173

我已经确定了向 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));

其中哪种方式是首选/惯用的?(还有我没有考虑到的其他方式吗?)


34
你的地图应该被称为“answers”,而不是“function”。 - Vincent Robert
2
@Vincent:嗯?函数基本上是两个集合之间的映射。 - fredoverflow
11
@FredOverflow: 似乎Vincent的评论有点开玩笑,涉及某本书... - Victor Sorokin
2
似乎与原始含义相矛盾——42不能同时成为生命、宇宙和一切的答案,也不能成为“无”的答案。但是,如何将生命、宇宙和一切表示为整数呢? - Stuart Golodetz
25
您可以使用足够大的整数来表示任何内容。 - Yakov Galka
显示剩余5条评论
9个回答

150

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

function.insert({0, 42});

这在功能上等同于

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

但更加简洁易读。正如其他答案所指出的,这种方法比其他形式有几个优点:

  • operator[]方法要求映射类型是可赋值的,但并非总是这样。
  • operator[]方法可能会覆盖现有元素,并且没有办法告诉你是否发生了这种情况。
  • 您列出的其他insert表单涉及隐式类型转换,这可能会使您的代码变慢。

主要缺点是,这种形式曾经需要键和值是可复制的,因此它无法与例如具有unique_ptr值的映射一起使用。这已在标准中修复,但修复可能尚未到达您的标准库实现。

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

function.emplace(0, 42);

这比任何一种insert()的形式都更为简洁,并且可以很好地与像unique_ptr这样的移动类型配合使用,理论上可能会更高效(尽管一个良好的编译器应该会优化掉差异)。唯一的主要缺点是它可能会让你的读者感到有些惊讶,因为emplace方法通常不会被用在这种方式上。


13
还有两个新函数insert_or_assigntry_emplace - sp2danny
function.insert({0, 42}); 不是一个最令人烦恼的解析案例吗? - Alex
2
不,最令人烦恼的解析(Most Vexing Parse)是当你本意为变量声明的东西被解析为函数声明。这个不能被解析为函数声明,而且也不打算是变量声明,所以MVP不是问题。 - Geoff Romer

113

首先,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(即添加 constfirst_type)。
  • std::pair<int, int> 还需要调用 std::pair 的模板构造函数以将参数转换为 value_type(即添加 constfirst_type)。
  • std::map<int, int>::value_type 没有任何疑问,它直接是 insert 成员函数期望的参数类型。

最后,如果目标是插入,我会避免使用 operator[],除非默认构造和分配 mapped_type 没有额外的成本,并且我不关心确定是否实际插入了新键。使用 insert,构造一个 value_type 可能是更好的选择。


将make_pair()中的Key转换为const Key是否真的需要另一个函数调用?似乎隐式转换就足够了,编译器应该会很高兴地这样做。 - galactica

17
自从C++17std::map提供了两种新的插入方法:insert_or_assign()try_emplace(),这也在comment by 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 pMyObj.m_i = 2

Coliru上的代码

请注意: 为了适应这个答案,我尽可能保持我的解释简短和简单。对于更精确和全面的描述,我建议阅读Fluent C++上的这篇文章


13

第一个版本:

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_type 是 std :: pair <const int,int> 的 typedef ,而 std :: make_pair()显然通过模板推导产生 std :: pair <> 。但无论如何,版本2、3和4的最终结果应该相同。
我会使用哪一个?我个人更喜欢版本1;它简洁而“自然”。当然,如果不需要其覆盖行为,则我会更喜欢版本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());

5

如果您想覆盖键为0的元素

function[0] = 42;

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

3

我一直在比较上述版本之间的时间差异:

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毫秒

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

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


3
简而言之,[] 运算符更适用于更新值,因为它涉及调用值类型的默认构造函数,然后将其赋予新值,而 insert() 更适用于添加值。
引自 Scott Meyers 的《Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library》第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));
    }
}

你可以选择使用不涉及泛型编程的版本,但是我的观点是,我认为这种范式(区分“添加”和“更新”)非常有用。

1

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

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;" 上没有显示错误,这一事实可能会带来毁灭性的影响!

编译器不会为前者显示错误,因为std::string::operator=(char)存在,但是对于后者,它们会显示错误,因为构造函数std::string::string(char)不存在。它不应该产生错误,因为C++始终自由地将任何整数样式文字解释为char,所以这不是编译器错误,而是程序员错误。基本上,我只是说无论是否在您的代码中引入了错误,这都是您必须自己注意的事情。顺便说一下,您可以打印rancking[0],使用ASCII的编译器将输出*,即(char)(42) - Keith M
请参阅:http://www.cplusplus.com/reference/string/string/operator=/ 和 http://www.cplusplus.com/reference/string/string/string/。 - Keith M

1
如果您想在std::map中插入元素,请使用insert()函数,如果您想查找元素(按键)并将其分配给某个元素,请使用operator[]。
为了简化插入操作,请使用boost::assign库,如下所示:
using namespace boost::assign;

// For inserting one element:

insert( function )( 0, 41 );

// For inserting several elements:

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

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