C++ 在c+中插入vs emplace vs operator[]+;地图
我第一次使用贴图,我意识到有很多方法可以插入元素。您可以使用C++ 在c+中插入vs emplace vs operator[]+;地图,c++,dictionary,insert,operators,emplace,C++,Dictionary,Insert,Operators,Emplace,我第一次使用贴图,我意识到有很多方法可以插入元素。您可以使用emplace()、operator[]或insert(),以及使用value\u type或make\u pair等变体。虽然有很多关于所有这些案例的信息和关于特定案例的问题,但我仍然无法理解全局。 因此,我的两个问题是: 它们各自的优势是什么 是否需要在标准中添加emplace?没有它之前有什么是不可能的吗 Emplace:利用右值引用使用已创建的实际对象。这意味着不调用复制或移动构造函数,这对大型对象很好!O(log(N))时间
emplace()
、operator[]
或insert()
,以及使用value\u type
或make\u pair等变体。虽然有很多关于所有这些案例的信息和关于特定案例的问题,但我仍然无法理解全局。
因此,我的两个问题是:
它们各自的优势是什么
是否需要在标准中添加emplace?没有它之前有什么是不可能的吗
Emplace:利用右值引用使用已创建的实际对象。这意味着不调用复制或移动构造函数,这对大型对象很好!O(log(N))时间
插入:具有标准左值引用和右值引用的重载,以及要插入的元素列表的迭代器,以及元素所属位置的“提示”。使用“提示”迭代器可以将插入所花费的时间缩短到contant time,否则就是O(log(N))时间
运算符[]:检查对象是否存在,如果存在,则修改对此对象的引用,否则使用提供的键和值在两个对象上调用make_pair,然后执行与insert函数相同的工作。这是O(log(N))时间
结对:只不过是结对而已
没有“需要”在标准中添加emplace。在c++11中,我相信添加了&&类型的引用。这消除了移动语义的必要性,并允许优化某些特定类型的内存管理。特别是右值引用。重载的insert(value_type&&)运算符没有利用就地语义,因此效率要低得多。虽然它提供了处理右值引用的能力,但它忽略了它们的关键用途,即对象的就地构造。除了优化机会和更简单的语法之外,插入和定位之间的一个重要区别是后者允许显式转换。(这涉及整个标准库,而不仅仅是地图。)
下面是一个示例来演示:
#include <vector>
struct foo
{
explicit foo(int);
};
int main()
{
std::vector<foo> v;
v.emplace(v.end(), 10); // Works
//v.insert(v.end(), 10); // Error, not explicit
v.insert(v.end(), foo(10)); // Also works
}
#包括
结构foo
{
显式foo(int);
};
int main()
{
std::向量v;
v、 安放(v.end(),10);//工作
//v、 insert(v.end(),10);//错误,不明确
v、 insert(v.end(),foo(10));//也可以
}
诚然,这是一个非常具体的细节,但在处理用户定义的转换链时,需要记住这一点。在地图的特定情况下,旧的选项只有两个:操作符[]
和插入
(插入
的不同风格)。所以我将开始解释这些
运算符[]
是一个查找或添加运算符。它将尝试在映射内找到具有给定键的元素,如果该元素存在,它将返回对存储值的引用。如果没有,它将使用默认初始化创建一个插入到位的新元素,并返回对它的引用
insert
函数(在单元素风格中)采用value\u type
(std::pair
),它使用键(first
成员)并尝试插入它。因为std::map
不允许重复,如果存在现有元素,它将不会插入任何内容
两者之间的第一个区别是操作符[]
需要能够构造默认初始化值,因此对于无法默认初始化的值类型,它是不可用的。二者之间的第二个区别是,当已经存在具有给定键的元素时会发生什么。insert
函数不会修改映射的状态,而是向元素返回一个迭代器(以及一个false
表示未插入该元素)
但事实并非如此。。。[1] 和[2]实际上是等价的。在这两种情况下,代码都会创建一个相同类型的临时对象(std::pair
),并将其传递给insert
函数。insert
函数将在二进制搜索树中创建适当的节点,然后将value\u type
部分从参数复制到节点。使用value\u-type
的优点是,value\u-type
总是匹配value\u-type
,您不能错误键入std::pair
参数的类型
区别在于[3]。函数std::make_pair
是一个模板函数,它将创建一个std::pair
。签名为:
template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );
但这仍然容易出错,就像在案例[1]中显式键入类型一样
到目前为止,我们有不同的调用insert
的方法,需要在外部创建value\u type
,并将该对象的副本复制到容器中。或者,如果类型是默认的可构造和可赋值类型(有意只关注于m[k]=v
),并且它需要一个对象的默认初始化和值到该对象中的副本,则可以使用operator[]
在C++11中,通过可变模板和完美转发,有了一种通过放置(就地创建)将元素添加到容器中的新方法。不同容器中的emplace
函数的作用基本相同:该函数不获取复制到容器中的源,而是获取将转发到容器中存储的对象的构造函数的参数
m.emplace(t,u); // 5
在[5]中,不会创建std::pair
并将其传递给emplace
,而是将对t
和u
对象的引用传递给emplace
template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );
m.insert( std::make_pair<const K,V>(t,u) ); // 4
m.emplace(t,u); // 5
#include <iostream>
#include <unordered_map>
#include <utility>
//Foo simply outputs what constructor is called with what value.
struct Foo {
static int foo_counter; //Track how many Foo objects have been created.
int val; //This Foo object was the val-th Foo object to be created.
Foo() { val = foo_counter++;
std::cout << "Foo() with val: " << val << '\n';
}
Foo(int value) : val(value) { foo_counter++;
std::cout << "Foo(int) with val: " << val << '\n';
}
Foo(Foo& f2) { val = foo_counter++;
std::cout << "Foo(Foo &) with val: " << val
<< " \tcreated from: \t" << f2.val << '\n';
}
Foo(const Foo& f2) { val = foo_counter++;
std::cout << "Foo(const Foo &) with val: " << val
<< " \tcreated from: \t" << f2.val << '\n';
}
Foo(Foo&& f2) { val = foo_counter++;
std::cout << "Foo(Foo&&) moving: " << f2.val
<< " \tand changing it to:\t" << val << '\n';
}
~Foo() { std::cout << "~Foo() destroying: " << val << '\n'; }
Foo& operator=(const Foo& rhs) {
std::cout << "Foo& operator=(const Foo& rhs) with rhs.val: " << rhs.val
<< " \tcalled with lhs.val = \t" << val
<< " \tChanging lhs.val to: \t" << rhs.val << '\n';
val = rhs.val;
return *this;
}
bool operator==(const Foo &rhs) const { return val == rhs.val; }
bool operator<(const Foo &rhs) const { return val < rhs.val; }
};
int Foo::foo_counter = 0;
//Create a hash function for Foo in order to use Foo with unordered_map
namespace std {
template<> struct hash<Foo> {
std::size_t operator()(const Foo &f) const {
return std::hash<int>{}(f.val);
}
};
}
int main()
{
std::unordered_map<Foo, int> umap;
Foo foo0, foo1, foo2, foo3;
int d; //Some int that will be umap's value. It is not important.
//Print the statement to be executed and then execute it.
std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
umap.insert(std::pair<Foo, int>(foo0, d));
//Side note: equiv. to: umap.insert(std::make_pair(foo0, d));
std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
umap.insert(std::move(std::pair<Foo, int>(foo1, d)));
//Side note: equiv. to: umap.insert(std::make_pair(foo1, d));
std::cout << "\nstd::pair<Foo, int> pair(foo2, d)\n";
std::pair<Foo, int> pair(foo2, d);
std::cout << "\numap.insert(pair)\n";
umap.insert(pair);
std::cout << "\numap.emplace(foo3, d)\n";
umap.emplace(foo3, d);
std::cout << "\numap.emplace(11, d)\n";
umap.emplace(11, d);
std::cout << "\numap.insert({12, d})\n";
umap.insert({12, d});
std::cout.flush();
}
Foo() with val: 0
Foo() with val: 1
Foo() with val: 2
Foo() with val: 3
umap.insert(std::pair<Foo, int>(foo0, d))
Foo(Foo &) with val: 4 created from: 0
Foo(Foo&&) moving: 4 and changing it to: 5
~Foo() destroying: 4
umap.insert(std::move(std::pair<Foo, int>(foo1, d)))
Foo(Foo &) with val: 6 created from: 1
Foo(Foo&&) moving: 6 and changing it to: 7
~Foo() destroying: 6
std::pair<Foo, int> pair(foo2, d)
Foo(Foo &) with val: 8 created from: 2
umap.insert(pair)
Foo(const Foo &) with val: 9 created from: 8
umap.emplace(foo3, d)
Foo(Foo &) with val: 10 created from: 3
umap.emplace(11, d)
Foo(int) with val: 11
umap.insert({12, d})
Foo(int) with val: 12
Foo(const Foo &) with val: 13 created from: 12
~Foo() destroying: 12
~Foo() destroying: 8
~Foo() destroying: 3
~Foo() destroying: 2
~Foo() destroying: 1
~Foo() destroying: 0
~Foo() destroying: 13
~Foo() destroying: 11
~Foo() destroying: 5
~Foo() destroying: 10
~Foo() destroying: 7
~Foo() destroying: 9
std::cout << "\numap.insert({{" << Foo::foo_counter << ", d}})\n";
umap.insert({{Foo::foo_counter, d}});
//but umap.emplace({{Foo::foo_counter, d}}); results in a compile error!
std::cout << "\numap.insert(std::pair<const Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<const Foo, int>({Foo::foo_counter, d}));
//The above uses Foo(int) and then Foo(const Foo &), as expected. but the
// below call uses Foo(int) and the move constructor Foo(Foo&&).
//Do you see why?
std::cout << "\numap.insert(std::pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<Foo, int>({Foo::foo_counter, d}));
//Not only that, but even more interesting is how the call below uses all
// three of Foo(int) and the Foo(Foo&&) move and Foo(const Foo &) copy
// constructors, despite the below call's only difference from the call above
// being the additional { }.
std::cout << "\numap.insert({std::pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({std::pair<Foo, int>({Foo::foo_counter, d})});
//Pay close attention to the subtle difference in the effects of the next
// two calls.
int cur_foo_counter = Foo::foo_counter;
std::cout << "\numap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}}) where "
<< "cur_foo_counter = " << cur_foo_counter << "\n";
umap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}});
std::cout << "\numap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}}) where "
<< "Foo::foo_counter = " << Foo::foo_counter << "\n";
umap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}});
//umap.insert(std::initializer_list<std::pair<Foo, int>>({{Foo::foo_counter, d}}));
//The call below works fine, but the commented out line above gives a
// compiler error. It's instructive to find out why. The two calls
// differ by a "const".
std::cout << "\numap.insert(std::initializer_list<std::pair<const Foo, int>>({{" << Foo::foo_counter << ", d}}))\n";
umap.insert(std::initializer_list<std::pair<const Foo, int>>({{Foo::foo_counter, d}}));