插入地图的首选/惯用方式是什么?


111

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

哪一种是首选/惯用方式?(还有我没想到的另一种方式吗?)


26
您的地图应称为“答案”,而不是“功能”
Vincent Robert 2010年

2
@Vincent:嗯?函数基本上是两个集合之间的映射。
fredoverflow

7
@FredOverflow:似乎文森特的评论有点像是关于某些书的笑话……
Victor Sorokin 2010年

1
这似乎与原作背道而驰-42不能同时回答(a)生命,宇宙和万物,以及(b)一无所有。但是,然后您如何将生活,宇宙和一切表达为一个整数?
Stuart Golodetz,2010年

19
@sgolodetz您可以使用足够大的int表示所有内容。
Yakov Galka 2010年

Answers:


90

首先,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,构造a value_type可能是可行的方法。


在make_pair()中从Key到const Key的转换是否真的要求另一个函数调用?似乎隐式强制转换就足以使哪个编译器应该乐于这样做。
galactica

99

从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方法通常不是那样使用的。



11

第一个版本:

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_typetypedeffor std::pair<const int, int>,并且std::make_pair()显然产生了std::pair<>via模板推论魔术。但是,最终结果应该与版本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

由于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 pMyObj.m_i = 2

关于Coliru的代码

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


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%的时间。


2

简而言之,[]operator对于更新值更有效,因为它涉及到调用值类型的默认构造函数,然后为其分配一个新值,而insert()在添加值时效率更高。

有效STL中引用的摘录 Scott Meyers 改进标准模板库使用的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”)非常有用。


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

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)
基思M

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.