提问人:fredoverflow 提问时间:11/26/2010 最后编辑:honkfredoverflow 更新时间:9/26/2019 访问量:196780
插入地图的首选/惯用方式是什么?
What is the preferred/idiomatic way to insert into a map?
问:
我已经确定了将元素插入到其中的四种不同方法: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));
以下哪一种是首选/惯用方式?(还有我没有想到的另一种方法吗?
答:
如果要用键 0 覆盖元素
function[0] = 42;
否则:
function.insert(std::make_pair(0, 42));
第一个版本:
function[0] = 42; // version 1
可能会也可能不会将值 42 插入到地图中。如果密钥存在,则它将为该密钥分配 42,覆盖该密钥具有的任何值。否则,它将插入键/值对。0
插入功能:
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
这三个插入功能几乎完全相同。 是 for ,并且显然会产生一个通过模板演绎的魔法。但是,版本 2、3 和 4 的最终结果应相同。std::map<int, int>::value_type
typedef
std::pair<const int, int>
std::make_pair()
std::pair<>
我会使用哪一个?我个人更喜欢版本 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());
首先,成员函数在功能上不等价: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() 函数,如果你想找到元素(按键)并为其分配一些元素 - 使用 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 );
从 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
评论
function.insert({0, 42});
我一直在对上述版本进行一些时间比较:
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 的默认构造函数进行初始化,该版本需要大约 10-15% 的时间。map_W_3[it] = Widget(2.0);
我只是稍微改变一下问题(字符串映射)以显示插入的另一个兴趣:
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)
char
rancking[0]
*
(char)(42)
简而言之,运算符在更新值方面更有效,因为它涉及调用值类型的默认构造函数,然后为其分配一个新值,而添加值更有效。[]
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));
}
}
你可能会决定选择一个无通用编程的版本,但关键是我发现这个范式(区分“添加”和“更新”)非常有用。
从 C++17 开始,std::map
提供了两种新的插入方法:insert_or_assign()
和 try_emplace(),
正如 sp2danny 的评论中提到的。
insert_or_assign()
基本上,是 operator[]
的“改进”版本。与 相反,不要求地图的值类型是默认可构造的。例如,以下代码不编译,因为没有默认构造函数:insert_or_assign()
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()
与上述类似,是 emplace()
的“改进”。与此相反,如果由于映射中已存在键而插入失败,则不会修改其参数。例如,以下代码尝试使用已存储在映射中的键来放置元素(请参阅 *):try_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
请注意:我试图让我的解释尽可能简短,以适应这个答案。为了获得更精确和全面的描述,我建议阅读这篇关于 Fluent C++ 的文章。
评论