[C++] 不同场景下的构造函数调用
本文为对不同场景下的构造函数调用进行跟踪。
构造函数
默认情况下,在 C++ 之后至少存在六个函数 默认构造/析构函数,复制构造/复制赋值,移动构造/移动赋值。以下代码观测发生调用的场景
#include <iostream>
struct Foo {
Foo() : fd(0) { std::cout << "Foo::Foo() this=" << this << " fd=" << fd << std::endl; }
Foo(int d) : fd(d) { std::cout << "Foo::Foo(int) this=" << this << " fd=" << d << std::endl; }
~Foo() { std::cout << "Foo::~Foo() this=" << this << " fd=" << fd << std::endl; }
Foo(const Foo &other) {
fd = other.fd;
std::cout << "Foo::Foo(const Foo &) this=" << this << " fd=" << fd << std::endl;
}
Foo &operator=(const Foo &other) {
fd = other.fd;
std::cout << "Foo::Foo &operator=(const Foo &) this=" << this << " fd=" << fd << std::endl;
return *this;
}
#if __cplusplus >= 201103L
Foo(Foo &&other) {
fd = other.fd;
other.fd = -1;
std::cout << "Foo::Foo(Foo &&) this=" << this << " fd=" << fd << std::endl;
}
Foo &operator=(Foo &&other) {
fd = other.fd;
other.fd = -1;
std::cout << "Foo::Foo &operator=(Foo &&) this=" << this << " fd=" << fd << std::endl;
return *this;
}
#endif
int fd;
};
void test_constructor() {
std::cout << std::endl << "// Foo f1(1);" << std::endl;
Foo f1(1);
std::cout << std::endl << "// Foo f2(f1);" << std::endl;
Foo f2(f1);
std::cout << std::endl << "// Foo f3 = f1;" << std::endl;
Foo f3 = f1;
std::cout << std::endl << "// f3 = f1;" << std::endl;
f3 = f1;
#if __cplusplus >= 201103L
std::cout << std::endl << "// Foo f4(std::move(f1));" << std::endl;
Foo f4(std::move(f1));
std::cout << std::endl << "// f4 = std::move(f1);" << std::endl;
f4 = std::move(f1);
#endif
std::cout << "\n";
}
int main() {
std::cout << "TEST Constructor\n";
test_constructor();
std::cout << "\n";
}
输出如下:移动构造/移动赋值 函数为 C++11 增加特性,需要使用 std::move
来调用移动构造函数。
// Foo f1(1);
Foo::Foo(int) this=0x7ffd3fc2420c fd=1
// Foo f2(f1);
Foo::Foo(const Foo &) this=0x7ffd3fc24208 fd=1
// Foo f3 = f1;
Foo::Foo(const Foo &) this=0x7ffd3fc24204 fd=1
// f3 = f1;
Foo::Foo &operator=(const Foo &) this=0x7ffd3fc24204 fd=1
// Foo f4(std::move(f1));
Foo::Foo(Foo &&) this=0x7ffd3fc24200 fd=1
// f4 = std::move(f1);
Foo::Foo &operator=(Foo &&) this=0x7ffd3fc24200 fd=-1
Foo::~Foo() this=0x7ffd3fc24200 fd=-1
Foo::~Foo() this=0x7ffd3fc24204 fd=1
Foo::~Foo() this=0x7ffd3fc24208 fd=1
Foo::~Foo() this=0x7ffd3fc2420c fd=-1
布置 new
基于 布置new
表达式可以实现STL中各种容器的 emplace 操作,借助 clangd 看到其默认实现为 operator new
💡 对于非 布置new表达式 则为调用 operator new(std::size_t)(在 libstdc++ 中为调用 malloc),然后再调用构造函数
function operator new
→ void *
Parameters:
std::size_t (aka unsigned long)
void * __p
Default placement versions of operator new.
inline void *operator new(std::size_t, void *__p) noexcept
// 实现就是返回 __p
_GLIBCXX_NODISCARD inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
用一个极简的例子来观察过程。众所周知构造函数的第一个参数为 this 指针,也就是类所在内存的起始地址。
所以对于实现,基于这个 this 指针调用构造函数即可,完成所谓的原位构造。
#include <new>
struct Foo {
Foo(int d) : fd(d) {}
~Foo() {}
};
int main() {
Foo *x = static_cast<Foo *>(::operator new(sizeof(Foo)));
::new ((Foo *)x) Foo(1111);
}
从汇编的角度看一下,在 x86 上 *di/*si
分别作为函数的第一/第二个参数,*ax
为函数的返回值。
movl $4, %edi ; sizeof(Foo)
call _Znwm@PLT ; ::operator new
movq %rax, -8(%rbp) ; 申请的内存首地址
movq -8(%rbp), %rax
movq %rax, %rsi ; operator new 的第二个参数
movl $4, %edi ; operator new 的第一个参数
call _ZnwmPv ; 调用 operator new
movl $1111, %esi ; Foo 构造函数的第二个参数
movq %rax, %rdi ; Foo 构造函数的第一个参数,也就是 operator new 的返回值
call _ZN3FooC1Ei ; 调用构造函数
std::move/std::forward
两者皆为 C++11 引入了右值引用特性的产物,实现如下
template <typename _Tp>
constexpr _Tp &&forward(typename std::remove_reference<_Tp>::type & __t) noexcept {
return static_cast<_Tp &&>(__t);
}
template <typename _Tp>
constexpr _Tp &&forward(typename std::remove_reference<_Tp>::type && __t) noexcept {
static_assert(!std::is_lvalue_reference<_Tp>::value,
"std::forward must not be used to convert an rvalue to an lvalue");
return static_cast<_Tp &&>(__t);
}
template <typename _Tp>
constexpr typename std::remove_reference<_Tp>::type &&move(_Tp && __t) noexcept {
return static_cast<typename std::remove_reference<_Tp>::type &&>(__t);
}
引入右值引用产生第一个问题:如何主动触发移动构造函数?
std::move 的实现为 强行转换右值引用类型,以此触发移动构造函数,举个例子
Foo f(1); // Foo::Foo(int) this=0x7ffe1fbe82e8 fd=1
Foo f1 = f; // Foo::Foo(const Foo &) this=0x7ffe6b948054 fd=1
Foo f2 = std::move(f); // Foo::Foo(Foo &&) this=0x7ffe1fbe82e0 fd=1
std::forward 用来解决另外一个问题:如何用一个函数来同时接收左值引用和右值引用类型的参数?
配合 引用折叠 规则:使用模版函数通过返回万能引用(格式为 T&&),对入参类型进行引用折叠,简单来说就是 T&& + && = T&&, T&& + & = T&&。这样就保持了原有的类型,完美转发。
可以通过一个构造函数的例子来观察,引用的匹配情况
template <typename T>
void test_ref(T &&v) {
typename std::remove_reference<T>::type vv(std::forward<T>(v));
}
int main() {
Foo f(11);
test_ref(f);
test_ref(std::move(f));
test_ref(Foo(22));
}
输出如下,std::move(f) 将类型转换为右值引用,std::forward 保持了类型,调用了移动构造函数。
Foo::Foo(int) this=0x7ffcd93aa0c8 fd=11
Foo::Foo(const Foo &) this=0x7ffcd93aa0ac fd=11
Foo::~Foo() this=0x7ffcd93aa0ac fd=11
Foo::Foo(Foo &&) this=0x7ffcd93aa0ac fd=11
Foo::~Foo() this=0x7ffcd93aa0ac fd=11
Foo::Foo(int) this=0x7ffcd93aa0cc fd=22
Foo::Foo(Foo &&) this=0x7ffcd93aa0ac fd=22
Foo::~Foo() this=0x7ffcd93aa0ac fd=22
Foo::~Foo() this=0x7ffcd93aa0cc fd=-1
Foo::~Foo() this=0x7ffcd93aa0c8 fd=-1
在 STL 中的应用
STL 版本为 gcc version 12.2.0
在 std::vector 中的应用场景
增加以下代码来观察构造函数的调用情况
#include <vector>
void test_constructor_with_vector() {
std::vector<Foo> foo_vec;
std::cout << std::endl << "// foo_vec.push_back(Foo(20));" << std::endl;
foo_vec.push_back(Foo(20));
std::cout << std::endl << "// foo_vec.push_back(Foo(21));" << std::endl;
foo_vec.push_back(Foo(21));
std::cout << std::endl << "// foo_vec[1] = Foo(22);" << std::endl;
foo_vec[1] = Foo(22);
#if __cplusplus >= 201103L
std::cout << std::endl << "// foo_vec.emplace_back(30);" << std::endl;
foo_vec.emplace_back(30);
std::cout << std::endl << "// foo_vec.emplace_back(31);" << std::endl;
foo_vec.emplace_back(31);
#endif
std::cout << "\n";
}
int main() {
std::cout << "TEST Constructor with std::vector\n";
test_constructor_with_vector();
std::cout << "\n";
}
在没有扩容的情况下,每次 push_back 至少需要调用两次构造函数
普通构造函数
Foo(1) 调用,必不可少
复制构造函数
push_back 只提供了参数为左指引用的版本,在没有扩容及使用默认分配器的情况下,使用到以下函数进行构造
void push_back(const value_type& __x) {
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish, __x);
}
template <typename _Tp>
static void construct(_Alloc &__a, pointer __p, const _Tp &__arg) {
__a.construct(__p, __arg);
}
void construct(pointer __p, const _Tp &__val) { ::new ((void *)__p) _Tp(__val); }
输出为
TEST Constructor with std::vector
// foo_vec.push_back(Foo(20));
Foo::Foo(int) this=0x7ffde54a85a4 fd=20
Foo::Foo(const Foo &) this=0x5630096842c0 fd=20
Foo::~Foo() this=0x7ffde54a85a4 fd=20
// foo_vec.push_back(Foo(21));
Foo::Foo(int) this=0x7ffde54a85a8 fd=21
Foo::Foo(const Foo &) this=0x5630096842e4 fd=21
Foo::Foo(const Foo &) this=0x5630096842e0 fd=20
Foo::~Foo() this=0x5630096842c0 fd=20
Foo::~Foo() this=0x7ffde54a85a8 fd=21
// foo_vec[1] = Foo(22);
Foo::Foo(int) this=0x7ffde54a85ac fd=22
Foo::Foo &operator=(const Foo &) this=0x5630096842e4 fd=22
Foo::~Foo() this=0x7ffde54a85ac fd=22
Foo::~Foo() this=0x5630096842e0 fd=20
Foo::~Foo() this=0x5630096842e4 fd=22
移动构造函数(C++11 起)
从 C++11 起,引入了右值引用的概念,对于 push_back 增加了参数右值引用的版本,内部实现为调用 emplace_back。实现如下(默认分配器)
void push_back(value_type &&__x) { emplace_back(std::move(__x)); }
void vector<_Tp, _Alloc>::emplace_back(_Args && ...__args) {
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
std::forward<_Args>(__args)...);
++this->_M_impl._M_finish;
}
template <typename _Up, typename... _Args>
static void
construct(allocator_type &__a __attribute__((__unused__)), _Up *__p,
_Args &&...__args) noexcept(std::is_nothrow_constructible<_Up, _Args...>::value) {
__a.construct(__p, std::forward<_Args>(__args)...);
}
template <typename _Up, typename... _Args>
void
construct(_Up *__p,
_Args &&...__args) noexcept(std::is_nothrow_constructible<_Up, _Args...>::value) {
::new ((void *)__p) _Up(std::forward<_Args>(__args)...);
}
在代码保持不变的情况下,增加 -std=c++11 后,从复制构造函数变成了移动构造函数调用,输出如下
TEST Constructor with std::vector
// foo_vec.push_back(Foo(20));
Foo::Foo(int) this=0x7fff7a4a612c fd=20
Foo::Foo(Foo &&) this=0x561fc61b22c0 fd=20
Foo::~Foo() this=0x7fff7a4a612c fd=-1
// foo_vec.push_back(Foo(21));
Foo::Foo(int) this=0x7fff7a4a6130 fd=21
Foo::Foo(Foo &&) this=0x561fc61b22e4 fd=21
Foo::Foo(const Foo &) this=0x561fc61b22e0 fd=20
Foo::~Foo() this=0x561fc61b22c0 fd=20
Foo::~Foo() this=0x7fff7a4a6130 fd=-1
// foo_vec[1] = Foo(22);
Foo::Foo(int) this=0x7fff7a4a6134 fd=22
Foo::Foo &operator=(Foo &&) this=0x561fc61b22e4 fd=22
Foo::~Foo() this=0x7fff7a4a6134 fd=-1
// foo_vec.emplace_back(30);
Foo::Foo(int) this=0x561fc61b22c8 fd=30
Foo::Foo(const Foo &) this=0x561fc61b22c0 fd=20
Foo::Foo(const Foo &) this=0x561fc61b22c4 fd=22
Foo::~Foo() this=0x561fc61b22e0 fd=20
Foo::~Foo() this=0x561fc61b22e4 fd=22
// foo_vec.emplace_back(31);
Foo::Foo(int) this=0x561fc61b22cc fd=31
Foo::~Foo() this=0x561fc61b22c0 fd=20
Foo::~Foo() this=0x561fc61b22c4 fd=22
Foo::~Foo() this=0x561fc61b22c8 fd=30
Foo::~Foo() this=0x561fc61b22cc fd=31
扩容使用的构造函数
观察以上的输出,扩容都是使用的复制构造函数,理论上使用移动构造也没有问题,其实现为最终调用 __do_uninit_copy 在构造的时候第二个参数为 *__first,解指针引用为左指引用,所以调用复制构造函数。
template <typename _InputIterator, typename _ForwardIterator>
_ForwardIterator __do_uninit_copy(_InputIterator __first, _InputIterator __last,
_ForwardIterator __result) {
_ForwardIterator __cur = __result;
try {
for (; __first != __last; ++__first, (void)++__cur)
std::_Construct(std::__addressof(*__cur), *__first);
return __cur;
} catch (...) {
std::_Destroy(__result, __cur);
throw;
}
}
在 std::map 中的应用场景
常见的两种插入值的方式,增加以下代码进行测试
#include <map>
void test_constructor_with_map() {
std::map<int, Foo> foo_map;
std::cout << std::endl << "// foo_map[1] = Foo(1);" << std::endl;
foo_map[1] = Foo(1);
std::cout << std::endl << "// foo_map.insert(std::pair<int, Foo>(2, Foo(2)));" << std::endl;
foo_map.insert(std::pair<int, Foo>(2, Foo(2)));
#if __cplusplus >= 201103L
std::cout << std::endl << "// foo_map.emplace(3, Foo(3));" << std::endl;
foo_map.emplace(3, Foo(3));
#endif
std::cout << "\n";
}
普通构造函数
Foo(1) 调用
默认构造函数
使用 operator [] 会出现未匹配到 key 的情况,所以对于 map 的 Value 而言,必须存在默认构造函数。默认构造函数调用在 mapped_type()
处。
mapped_type &operator[](const key_type &__k) {
iterator __i = lower_bound(__k);
if (__i == end() || key_comp()(__k, (*__i).first))
__i = insert(__i, value_type(__k, mapped_type()));
return (*__i).second;
}
复制构造函数
在 C++11 之前,使用 operator [] 会调用两次 Foo 的复制构造函数,分别在
1.构造 std::pair 也就是 operator[] 中的 value_type(__k, mapped_type())
处
mapped_type &operator[](const key_type &__k) {
iterator __i = lower_bound(__k);
if (__i == end() || key_comp()(__k, (*__i).first))
__i = insert(__i, value_type(__k, mapped_type()));
return (*__i).second;
}
2.插入的过程 __node_gen((__v))
处
template <typename _NodeGen>
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_insert_(_Base_ptr __x, _Base_ptr __p,
const _Val &__v,
_NodeGen &__node_gen) {
bool __insert_left =
(__x != 0 || __p == _M_end() || _M_impl._M_key_compare(_KeyOfValue()(__v), _S_key(__p)));
_Link_type __z = __node_gen((__v));
_Rb_tree_insert_and_rebalance(__insert_left, __z, __p, this->_M_impl._M_header);
++_M_impl._M_node_count;
return iterator(__z);
}
template <typename _Arg>
_Link_type operator()(const _Arg &__arg) const {
return _M_t._M_create_node((__arg));
}
_Link_type _M_create_node(const value_type &__x) {
_Link_type __tmp = _M_get_node();
_M_construct_node(__tmp, __x);
return __tmp;
}
void _M_construct_node(_Link_type __node, const value_type &__x) {
get_allocator().construct(__node->_M_valptr(), __x);
}
在 C++11 的之前输出如下:
TEST Constructor with std::map
// foo_map[1] = Foo(1);
Foo::Foo(int) this=0x7ffcbc0c6fb0 fd=1
Foo::Foo() this=0x7ffcbc0c6f5c fd=0
Foo::Foo(const Foo &) this=0x7ffcbc0c6f58 fd=0
Foo::Foo(const Foo &) this=0x56401a9412e4 fd=0
Foo::~Foo() this=0x7ffcbc0c6f58 fd=0
Foo::~Foo() this=0x7ffcbc0c6f5c fd=0
Foo::Foo &operator=(const Foo &) this=0x56401a9412e4 fd=1
Foo::~Foo() this=0x7ffcbc0c6fb0 fd=1
// foo_map.insert(std::pair<int, Foo>(2, Foo(2)));
Foo::Foo(int) this=0x7ffcbc0c6fc8 fd=2
Foo::Foo(const Foo &) this=0x7ffcbc0c6fc4 fd=2
Foo::Foo(const Foo &) this=0x7ffcbc0c6fbc fd=2
Foo::Foo(const Foo &) this=0x56401a941314 fd=2
Foo::~Foo() this=0x7ffcbc0c6fbc fd=2
Foo::~Foo() this=0x7ffcbc0c6fc4 fd=2
Foo::~Foo() this=0x7ffcbc0c6fc8 fd=2
Foo::~Foo() this=0x56401a941314 fd=2
Foo::~Foo() this=0x56401a9412e4 fd=1
移动构造函数
C++11 起,std::pair 增加了在参数为右值引用的版本,使用 std::pair<int, Foo>(2, Foo(2)) 的时候就调用了 Foo 的移动构造函数,emplace 的出现也简化了使用方法,可以直接 emplace(2, Foo(2))
。
emplace 下借助 std::forward 调用了 Foo 移动构造函数的实现。
template <typename... _Args>
std::pair<iterator, bool> emplace(_Args &&...__args) {
return _M_t._M_emplace_unique(std::forward<_Args>(__args)...);
}
template <typename _Key, typename _Val, typename _KeyOfValue, typename _Compare,
typename _Alloc>
template <typename... _Args>
auto _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::_M_emplace_unique(_Args && ...__args)
->pair<iterator, bool> {
_Auto_node __z(*this, std::forward<_Args>(__args)...);
auto __res = _M_get_insert_unique_pos(__z._M_key());
if (__res.second)
return {__z._M_insert(__res), true};
return {iterator(__res.first), false};
}
struct _Auto_node {
template <typename... _Args>
_Auto_node(_Rb_tree &__t, _Args &&...__args)
: _M_t(__t), _M_node(__t._M_create_node(std::forward<_Args>(__args)...)) {}
}
template <typename... _Args>
_Link_type _M_create_node(_Args &&...__args) {
_Link_type __tmp = _M_get_node();
_M_construct_node(__tmp, std::forward<_Args>(__args)...);
return __tmp;
}
template <typename... _Args>
void _M_construct_node(_Link_type __node, _Args &&...__args) {
::new (__node) _Rb_tree_node<_Val>;
_Alloc_traits::construct(_M_get_Node_allocator(), __node->_M_valptr(),
std::forward<_Args>(__args)...);
}
template <typename _U1, typename _U2,
typename enable_if<
_PCCFP<_U1, _U2>::template _MoveConstructiblePair<_U1, _U2>() &&
_PCCFP<_U1, _U2>::template _ImplicitlyMoveConvertiblePair<_U1, _U2>(),
bool>::type = true>
constexpr pair(pair<_U1, _U2> &&__p)
: first(std::forward<_U1>(__p.first)), second(std::forward<_U2>(__p.second)) {}
C++11 版本的构造函数调用输出如下,对比更早的C++版本,消除了两个复制构造函数的调用。
TEST Constructor with std::map
// foo_map[1] = Foo(1);
Foo::Foo(int) this=0x7ffd143ea270 fd=1
Foo::Foo() this=0x55f65bd5c2e4 fd=0
Foo::Foo &operator=(Foo &&) this=0x55f65bd5c2e4 fd=1
Foo::~Foo() this=0x7ffd143ea270 fd=-1
// foo_map.insert(std::pair<int, Foo>(2, Foo(2)));
Foo::Foo(int) this=0x7ffd143ea280 fd=2
Foo::Foo(Foo &&) this=0x7ffd143ea27c fd=2
Foo::Foo(Foo &&) this=0x55f65bd5c314 fd=2
Foo::~Foo() this=0x7ffd143ea27c fd=-1
Foo::~Foo() this=0x7ffd143ea280 fd=-1
// foo_map.emplace(3, Foo(3));
Foo::Foo(int) this=0x7ffd143ea288 fd=3
Foo::Foo(Foo &&) this=0x55f65bd5c344 fd=3
Foo::~Foo() this=0x7ffd143ea288 fd=-1
Foo::~Foo() this=0x55f65bd5c344 fd=3
Foo::~Foo() this=0x55f65bd5c314 fd=2
Foo::~Foo() this=0x55f65bd5c2e4 fd=1
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 智能桌面机器人:用.NET IoT库控制舵机并多方法播放表情
· Linux glibc自带哈希表的用例及性能测试
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· DeepSeek火爆全网,官网宕机?本地部署一个随便玩「LLM探索」
· 开发者新选择:用DeepSeek实现Cursor级智能编程的免费方案
· 【译】.NET 升级助手现在支持升级到集中式包管理
· 独立开发经验谈:如何通过 Docker 让潜在客户快速体验你的系统
· Tinyfox 发生重大改版