不同场景下的构造函数调用

本文为对不同场景下的构造函数调用进行跟踪。

构造函数

默认情况下,在 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

posted on 2024-05-21 15:00  文一路挖坑侠  阅读(4)  评论(0编辑  收藏  举报

导航