#include <iostream> #include <vector> #include <string> struct President { std::string name; std::string country; int year; President(std::string p_name, std::string p_country, int p_year) : name(std::move(p_name)), country(std::move(p_country)), year(p_year) { std::cout << "I am being constructed.\n"; } President(const President& other) : name(other.name), country(other.country), year(other.year) { std::cout << "I am being copy-constructed.\n"; } President(President&& other) noexcept : name(std::move(other.name)), country(std::move(other.country)), year(other.year) { std::cout << "I am being move-constructed.\n"; } }; int main() { std::vector<President> elections; std::cout << "Using push_back:\n"; elections.push_back(President("Nelson Mandela", "South Africa", 1994)); }上述代码的输出会是:
Using push_back: I am being constructed. I am being move-constructed.这里发生了什么?
template<class... Args> void emplace_back(Args&&... args);这里的 class... Args就是一个模板参数包,Args&&... args则是一个函数参数包。这使得 emplace_back可以接收用于构造 President对象的所有参数,例如 emplace_back("Nelson Mandela", "South Africa", 1994)。完美转发 (Perfect Forwarding)仅仅能接收参数还不够,emplace还需要以“原汁原味”的方式将这些参数传递给元素的构造函数。这意味着参数的值类别(左值或右值)必须被保留。如果传递的是一个右值(如临时字符串),它应该被作为右值转发,以便触发移动构造;如果传递的是一个左值(如一个变量),它就应该被作为左值转发。
template<typename T, typename Allocator = std::allocator<T>> class vector { // ... public: template<class... Args> void emplace_back(Args&&... args) { // 1. 检查是否需要扩容 if (size_ == capacity_) { reallocate(); // 如果需要,重新分配内存并移动现有元素 } // 2. 在预留的内存位置上直接构造对象 // 使用 placement new 在指定内存地址 (m_data + size_) 上调用构造函数 // std::forward 确保参数以原始的值类别被传递 new (m_data + size_) T(std::forward<Args>(args)...); // 堆代码 duidaima.com // 3. 增加容器的大小 ++size_; } // ... private: T* m_data; size_t size_; size_t capacity_; };关键点: new (m_data + size_) T(...)是一个 placement new表达式。它不会分配新内存,而是在一个已经存在的、由 m_data + size_指向的内存地址上调用类型 T的构造函数。
// ... (President class with counters) std::vector<President> presidents; std::cout << "--- emplace_back ---\n"; presidents.emplace_back("Ronald Reagan", "USA", 1981); // 只需要传入构造函数的参数 std::cout << "\n--- push_back ---\n"; presidents.push_back(President("Margaret Thatcher", "UK", 1979)); // 需要创建一个临时对象分析结果:
std::map<std::string, President> presidents_map; std::cout << "\n--- map::emplace ---\n"; presidents_map.emplace("USA", President("Ronald Reagan", "USA", 1981)); std::cout << "\n--- map::insert ---\n"; presidents_map.insert(std::make_pair("UK", President("Margaret Thatcher", "UK", 1979)));emplace可以更高效地直接构造 std::pair的两个部分,特别是当键和值都可以就地构造时。C++17 引入的 try_emplace在这方面做得更好(详见“避坑指南”)。
在这两种情况下,emplace_back和 push_back都会调用 President的拷贝构造函数。因为传递给它们的是一个左值 reagan,emplace_back只是将这个左值转发给了 President的构造函数,最终匹配到的还是拷贝构造函数。
#include <iostream> #include <vector> #include <deque> #include <list> #include <map> #include <set> #include <unordered_map> #include <string> /** * @class Widget * @brief A simple class to demonstrate construction. */ struct Widget { int id; std::string name; /** * @brief Construct a new Widget object * @param i The id of the widget. * @param n The name of the widget. */ Widget(int i, std::string n) : id(i), name(std::move(n)) { std::cout << " Constructing Widget " << id << " with name " << name << "\n"; } }; int main() { // --- 序列容器 --- std::cout << "1. std::vector::emplace_back\n"; std::vector<Widget> v; v.emplace_back(1, "VectorWidget"); std::cout << "\n2. std::deque::emplace_front\n"; std::deque<Widget> d; d.emplace_front(2, "DequeWidget"); std::cout << "\n3. std::list::emplace\n"; std::list<Widget> l; l.emplace(l.begin(), 3, "ListWidget"); // --- 关联容器 --- std::cout << "\n4. std::map::emplace\n"; std::map<int, std::string> m; // emplace 构造 std::pair<const int, std::string> m.emplace(10, "MapValue"); // 使用 std::piecewise_construct 进行更复杂的构造 std::map<std::string, Widget> widget_map; widget_map.emplace(std::piecewise_construct, std::forward_as_tuple("key1"), std::forward_as_tuple(4, "WidgetInMap")); std::cout << "\n5. std::set::emplace\n"; std::set<Widget> s; // 注意:set 的比较操作符需要被定义 // 这里为了简化,我们假设Widget可以通过id比较(实际需要提供比较器) // s.emplace(5, "SetWidget"); // 这行需要比较器才能编译 // --- 无序关联容器 --- std::cout << "\n6. std::unordered_map::emplace\n"; std::unordered_map<int, std::string> um; um.emplace(20, "UnorderedMapValue"); return 0; }5. 避坑指南:常见陷阱与最佳实践
std::vector<std::string> vs; // 我们期望调用 std::string(size_t count, char ch) 构造函数 // 创建一个包含10个'c'的字符串 "cccccccccc" vs.emplace_back(10, 'c'); // 实际行为是正确的 // 但是考虑这种情况: class MyString { public: // 接受C风格字符串的构造函数 MyString(const char* s) { std::cout << "MyString(const char*)\n"; } // 接受布尔值的构造函数 MyString(bool b) { std::cout << "MyString(bool)\n"; } }; std::vector<MyString> vm; vm.emplace_back(nullptr); // 你期望调用哪个?nullptr可以被隐式转换为 const char*,也可以被隐式转换为 bool(值为 false)。这会导致编译错误或调用非预期的构造函数。相比之下,push_back(MyString(nullptr))的意图是明确的,因为它会先构造 MyString对象,此时编译器会完成正确的重载决议。
std::map<int, std::unique_ptr<Widget>> my_map; auto ptr = std::make_unique<Widget>(1, "MyWidget"); my_map.emplace(1, std::move(ptr)); // 第一次插入,OK // ptr 现在是空的 auto ptr2 = std::make_unique<Widget>(2, "AnotherWidget"); // 尝试用一个已存在的键再次 emplace my_map.emplace(1, std::move(ptr2));emplace的签名是 emplace(Args&&... args)。为了判断键是否存在,它必须先利用 args...**构造出 value_type**(即 std::pair<const int, std::unique_ptr<Widget>>)。在这个过程中,ptr2的所有权被转移给了这个临时的 pair。然后 emplace发现键 1已存在,插入失败。但此时 ptr2已经被移走,变成了空指针!
// C++17 auto ptr3 = std::make_unique<Widget>(3, "ThirdWidget"); my_map.try_emplace(1, std::move(ptr3)); try_emplace(Key&& k, Args&&... args)会首先检查键 k是否存在。只有当键不存在时,它才会使用 args...去构造 mapped_type (即 std::unique_ptr<Widget>)。因此,如果键已存在,ptr3的所有权不会被转移。陷阱3:冗余的移动操作