插入地图的首选/惯用方式

时间:2010-11-26 15:48:29

标签: c++ stl insert stdmap std-pair

我已经确定了四种不同的方式插入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));

其中哪些是首选/惯用方式? (还有另一种我没想过的方法吗?)

9 个答案:

答案 0 :(得分:80)

从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方法通常不会以这种方式使用。

答案 1 :(得分:76)

首先,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可能是最佳选择。

答案 2 :(得分:7)

第一个版本:

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_typetypedef的{​​{1}},而std::pair<const int, int>显然会通过模板扣除魔法产生std::make_pair()。但是,对于版本2,3和4,最终结果应该相同。

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

通过其构造函数之一将值插入到地图中的另一种方法:

std::map

答案 3 :(得分: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 ms,2078 ms,2072 ms

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

2290 ms,2037 ms,2046 ms

 map_W_3[it] = Widget(2.0);

2592 ms,2278 ms,2296 ms

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

2234 ms,2031 ms,2027 ms

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

由于使用Widget的默认构造函数进行初始化,map_W_3[it] = Widget(2.0);版本对此示例的时间大约多10-15%。

答案 4 :(得分:2)

由于C++17 std::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

Code on Coliru

请注意:我试图使我的解释尽可能简短,以使它们适合此答案。有关更精确,更全面的描述,我建议阅读 Fluent C ++ 上的this article

答案 5 :(得分:1)

如果要用键0覆盖元素

function[0] = 42;

否则:

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

答案 6 :(得分: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 );

答案 7 :(得分: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;”上没有显示错误这一事实会产生毁灭性的影响!

答案 8 :(得分:1)

简而言之,[]运算符更有效地更新值,因为它涉及调用值类型的默认构造函数,然后为其分配新值,而insert()更有效地添加值。

来自有效STL的引用片段:提高您使用标准模板库的50种具体方法作者:Scott Meyers,第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));
    }
}

你可能决定选择一个免费的通用编程版本,重点是我发现这个范例(区分&#39;添加&#39;更新&#39;)非常有用。