std::unordered_map::operator[] - 为什么有两个签名?

5
在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;
2个回答

7
通常情况下,键只是用于比较目的,因此您可能会想知道为什么需要rvalue语义:const引用已经涵盖了该情况。
但需要注意的是,operator[]确实可以创建新的键/值对:如果键尚不存在于映射中。
在这种情况下,如果使用了第二个重载,则映射可以安全地将提供的键值移动到映射中(同时默认初始化值)。在我看来,这是一个非常罕见且可忽略的优化,但当您是C++标准库时,即使它只发生一次,也不应该节省任何努力来节约时间!
至于第二个问题,我可能错了,但应该将第二个重载视为最佳重载。
编辑: 还有一个有效的观点是,即使这是一个值得争议的决定,它也可能允许您使用仅能移动的对象作为键值。

2
我不确定这是为了优化而完成的,你可能忽略了一些类型是可移动但不可复制的事实。在这种情况下,如果没有这个重载,您将无法将此类类型用作键。 - Slava
@Slava,你说得很有道理,我甚至会在我的答案中加入它,但是我不明白在实践中为什么你想要一个只能移动的类型作为映射的键类型。 你不会把互斥锁、线程或唯一指针作为键。你应该只使用行为类似值类型的类型。 - KABoissonneault

1

这是出于性能考虑。例如,如果键是rvalue,则在插入新元素时移动键而不是复制。

因此,您可以避免对象/键的额外复制。您可以在以下示例中看到:

#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;这个案例中所看到的,rvalue对象被移动了,与语句fmap[a] = 100;不同,后者调用了一个复制构造函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接