将 std::map::emplace 与返回 shared_ptr 的函数一起使用是否正确?

Is it correct to use std::map::emplace with a function returning a shared_ptr?

如果我有一些 returns 和 std::shared_ptr<T> 的函数,我应该如何在 std::map<U, std::shared_ptr<T>> 中插入该函数调用的结果:使用 insertmake_pair 或者只是 emplace?

假设我已经知道没有重复键,并且我是单线程的。

std::shared_ptr<Foo> bar() {
   return std::make_shared<Foo>();
}

std::map<std::string, std::shared_ptr<Foo>> my_map;

// Which makes more sense?
my_map.emplace("key", bar());
// or
my_map.insert(make_pair("key", bar()));

RVO 是否适用于 std::map::emplace

我认为是正确的。


RVO

我认为只要在您的编译器中实现 RVO 就可以正常工作,因为 bar() returns 时间对象和编译器知道什么对象将从 bar() 返回。 此外,在 C++17 及更高版本中,RVO 在这种情况下得到保证。

Is RVO (Return Value Optimization) applicable for all objects?

What are copy elision and return value optimization?


放置与插入 (DEMO)

std::map::emplace

my_map.emplace("key", bar());

,RVO 将 bar() 替换为 std::make_shared<Foo>() 并创建了 std::shared_ptr<Foo>。 然后转发 std::shared_ptr<Foo>move-ctor 在 [=26] 的新元素的就地构造中调用 一次 =].

std::map::insert with std::make_pair

my_map.insert(make_pair("key", bar()));

,RVO 再次将 bar() 替换为 std::make_shared<Foo>()

接下来,从N3337的20.3.2开始,C++11修正了一些小错误,std::pair的class模板是

namespace std{
  template<class T1, class T2>
  struct pair
  {
    typedef T1 first_type;
    typedef T2 second_type;
    T1 first;
    T2 second;
    ...
    template<class U, class V> pair(U&& x, V&& y);
    ...
  }
}

其中模板构造器是

template<class U, class V> pair(U&& x, V&& y);

Effects: The constructor initializes first with std::forward<U>(x) and second with std::forward<V>(y).

此外,从 20.3.3

template<class T1, class T2>
pair<V1, V2> make_pair(T1&& x, T2&& y);

Returns: pair<V1, V2>(std::forward<T1>(x), std::forward<T2>(y));...

因此,此定义的 std::make_pair 的典型实现被认为是这样的:

namespace std{
  template<class T1, class T2>
  inline pair<typename decay<T1>::type, typename decay<T2>::type>
  make_pair(T1&& x, T2&& y)
  {
    return pair<typename decay<T1>::type,
                typename decay<T2>::type>
                (std::forward<T1>(x), std::forward<T2>(y));
  }
}

并且 RVO 将再次为 std::make_pair 工作。 事实上,如果我们用 C++14 编译 DEMO 和禁用 g++ 的 RVO 的编译器选项“-fno-elide-constructors”,那么 move-ctor 调用的数量只会增加一个。

因此在 std::make_pair("key", bar()) 中,我预计会出现以下情况:

  • 1) RVO 将 bar() 替换为 std::make_shared<Foo>(),
  • 2) RVO 也将 std::make_pair 替换为 std::pair::pair,
  • 3)创建std::shared_prtr<Foo>,然后转发到std::pair::pair
  • 4) std::shared_prtr<Foo>move-ctor.
  • 创建为元素 second

最后,std::map::insert 也为 r 值重载:

// since C++11
template< class P >
std::pair<iterator,bool> insert( P&& value );

又来了

  • 5) 调用 std::shared_prtr<Foo>move-ctor

综上所述,我预计

my_map.insert(make_pair("key", bar()));

调用 std::shared_ptr<Foo> 的 move-ctor 两次

我的回答

由于在这两种情况下都没有完成 std::shared_ptr<Foo> 的副本并且调用了几乎相同数量的移动操作者,因此它们的性能几乎相同。

Moving an object into a map

insert vs emplace vs operator[] in c++ map


注1,通用参考

在C++11及以上版本中,std::make_pair的左参数和右参数都是Scott Meyers所说的universal reference,即它们可以接受左值和右值:

// until C++11
template< class T1, class T2 >
std::pair<T1,T2> make_pair( T1 t, T2 u );

// since C++11
template< class T1, class T2 >
std::pair<V1,V2> make_pair( T1&& t, T2&& u );

因此,如果我们将左值传递给 std::make_pair,那么 std::shared_ptr<Foo> 的复制函数将作为通用引用的结果被调用。


注2,std::map::try_emplace

std::map::emplace 总是构造 my_map 的新元素,即使键已经存在。 如果密钥已经存在,则这些新实例将被销毁。 但是从 C++17 开始,我们得到

template <class... Args>
pair<iterator, bool> try_emplace(key_type&& k, Args&&... args);

如果键 k 已存在于容器中,则此函数不会构造 args。 虽然我们已经知道本题不存在重复键,但如果不知道是否存在重复键,则优先选择std::map::try_emplace