闽公网安备 35020302035485号
#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:冗余的移动操作