std::unordered_map::operator[] - 为什么有两个签名?
std::unordered_map::operator[] - why there are two signatures?
在C++11中,std::unordered_map::operator[]有两个版本,即:
mapped_type& operator[] ( const key_type& k ); //1
mapped_type& operator[] ( key_type&& k ); //2
有两个问题:
1) 为什么第二个是必要的 - 第一个允许将常量传递给函数,因为第一个包含关键字 const
2) 例如,在这种情况下将调用哪个版本,1 或 2:
std::unordered_map<std::string, int> testmap;
testmap["test"] = 1;
这是出于性能原因。例如,如果键是右值,则在插入新元素时移动而不是复制键。
因此,您可以避免 object/key 的额外副本。您可以在以下示例中看到这一点:
#include <iostream>
#include <unordered_map>
struct Foo {
Foo() { std::cout << "Foo() called" << std::endl; }
Foo(Foo const &other) { std::cout << "Foo(Foo const &other) called" << std::endl; }
Foo(Foo &&other) { std::cout << "Foo(Foo &&other) called" << std::endl; }
int i = 0;
};
bool operator==(Foo const &lhs, Foo const &rhs) {
return lhs.i == rhs.i;
}
void hash_combine(std::size_t& seed, const Foo& v) {
std::hash<int> hasher;
seed ^= hasher(v.i) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}
struct CustomHash {
std::size_t operator()(Foo const& v) const {
std::size_t res = 0;
hash_combine(res, v);
return res;
}
};
int main() {
std::unordered_map<Foo, int, CustomHash> fmap;
Foo a;
a.i = 100;
fmap[a] = 100;
fmap[Foo()] = 1;
}
输出:
Foo() called
Foo(Foo const &other) called
Foo() called
Foo(Foo &&other) called
正如在案例 fmap[Foo()] = 1;
中看到的,右值对象被移动,与调用复制构造函数的语句 fmap[a] = 100;
形成对比。
通常,键仅用于比较目的,因此您可能想知道为什么需要右值语义:const 引用应该已经涵盖了这种情况。
但需要注意的是,operator[] 确实可以创建一个新的 key/value 对:如果映射中不存在键。
在这种情况下,如果使用了第二个重载,则地图可以安全地移动地图中提供的键值(同时默认初始化该值)。在我看来,这是一个非常罕见且可以忽略不计的优化,但是当你是 C++ 标准库时,你不应该不遗余力地为某人节省一个周期,即使它只发生一次!
关于第二个问题,我可能是错的,但应该认为第二次重载是最好的重载。
编辑:
还有一个有效的观点是它可能允许您使用仅移动对象作为键值,即使这是一个值得商榷的决定
在C++11中,std::unordered_map::operator[]有两个版本,即:
mapped_type& operator[] ( const key_type& k ); //1
mapped_type& operator[] ( key_type&& k ); //2
有两个问题:
1) 为什么第二个是必要的 - 第一个允许将常量传递给函数,因为第一个包含关键字 const
2) 例如,在这种情况下将调用哪个版本,1 或 2:
std::unordered_map<std::string, int> testmap;
testmap["test"] = 1;
这是出于性能原因。例如,如果键是右值,则在插入新元素时移动而不是复制键。
因此,您可以避免 object/key 的额外副本。您可以在以下示例中看到这一点:
#include <iostream>
#include <unordered_map>
struct Foo {
Foo() { std::cout << "Foo() called" << std::endl; }
Foo(Foo const &other) { std::cout << "Foo(Foo const &other) called" << std::endl; }
Foo(Foo &&other) { std::cout << "Foo(Foo &&other) called" << std::endl; }
int i = 0;
};
bool operator==(Foo const &lhs, Foo const &rhs) {
return lhs.i == rhs.i;
}
void hash_combine(std::size_t& seed, const Foo& v) {
std::hash<int> hasher;
seed ^= hasher(v.i) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}
struct CustomHash {
std::size_t operator()(Foo const& v) const {
std::size_t res = 0;
hash_combine(res, v);
return res;
}
};
int main() {
std::unordered_map<Foo, int, CustomHash> fmap;
Foo a;
a.i = 100;
fmap[a] = 100;
fmap[Foo()] = 1;
}
输出:
Foo() called
Foo(Foo const &other) called
Foo() called
Foo(Foo &&other) called
正如在案例 fmap[Foo()] = 1;
中看到的,右值对象被移动,与调用复制构造函数的语句 fmap[a] = 100;
形成对比。
通常,键仅用于比较目的,因此您可能想知道为什么需要右值语义:const 引用应该已经涵盖了这种情况。
但需要注意的是,operator[] 确实可以创建一个新的 key/value 对:如果映射中不存在键。
在这种情况下,如果使用了第二个重载,则地图可以安全地移动地图中提供的键值(同时默认初始化该值)。在我看来,这是一个非常罕见且可以忽略不计的优化,但是当你是 C++ 标准库时,你不应该不遗余力地为某人节省一个周期,即使它只发生一次!
关于第二个问题,我可能是错的,但应该认为第二次重载是最好的重载。
编辑: 还有一个有效的观点是它可能允许您使用仅移动对象作为键值,即使这是一个值得商榷的决定