窥视C++细节-为资源管理类提供swap函数的原因


在有资源的类中,使用swap函数交换两个对象时,如果没有正确的使用swap函数,则会造成资源的拷贝和释放,导致性能降低。

环境

在运行测试代码时,使用了如下环境:

linux使用的是ubuntu 18,在ubuntu上使用的g++版本如下:

root@learner:~# g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

定义一个用于测试的类

class A
{
public:
    A(const char *s = nullptr){
        cout << "constuctor :" << this << endl;
        if (s == nullptr){
            str_ = new char[1];
            *str_ = '\0';
        }else{
            int len = strlen(s);
            str_ = new char[len + 1];
            strcpy(str_, s);
        }
    }

    A(const A &another){
        cout << this << " cpy constructor from " << &another << endl;

        int len = strlen(another.str_);
        str_ = new char[len + 1];
        strcpy(str_, another.str_);
    }

    A &operator=(const A &another){
        cout << this << " operator= from " << &another << endl;

        if (this != &another){
            delete[] this->str_;

            int len = strlen(another.str_);
            this->str_ = new char[len + 1];
            strcpy(this->str_, another.str_);
        }

        return *this;
    }

    A(A &&another){
        cout << this << " move constructor from " << &another << endl;

        this->str_ = another.str_;

        another.str_ = nullptr;
    }

    A &operator=(A &&another){
        cout << this << " move operator= from" << &another << endl;
        if (this != &another){
            delete[] this->str_;

            this->str_ = another.str_;

            another.str_ = nullptr;
        }

        return *this;
    }

    ~A(){
        cout << "deconstrutor :" << this << endl;
        if (str_){
            delete[] str_;
        }
    }

    char * c_str(){
        return str_;
    }

private:
    char *str_;
};

使用std::swap函数

使用标准库中的std::swap函数交换两个对象时,需要分为两种情况:一种是类内有移动构造和移动赋值运算符;另一类是没有移动构造和移动赋值运算符。

类内有移动构造和移动赋值运算符
int main()
{
    A a("hello");
    A b("world");

    cout << "&a = " << &a << endl;
    cout << "&b = " << &b << endl;

    cout << "===========" << endl;
    std::swap(a, b);
    cout << "===========" << endl;

    cout<<"a = "<<a.c_str()<<endl;
    cout<<"b = "<<b.c_str()<<endl;

    return 0;
}
constuctor :0x7ffd1dfe3758
constuctor :0x7ffd1dfe3760
&a = 0x7ffd1dfe3758
&b = 0x7ffd1dfe3760
===========
0x7ffd1dfe3720 move constructor from 0x7ffd1dfe3758
0x7ffd1dfe3758 move operator= from0x7ffd1dfe3760
0x7ffd1dfe3760 move operator= from0x7ffd1dfe3720
deconstrutor :0x7ffd1dfe3720
===========
a = world
b = hello
deconstrutor :0x7ffd1dfe3760
deconstrutor :0x7ffd1dfe3758

类提供了移动构造和移动赋值运算符时,交换两个对象发生了一次移动构造、两次移动赋值运算和一次析构。

在这里插入图片描述

调用std::swap函数之前的状态。
在这里插入图片描述

首先使用a对象,通过移动构造,构造出tmp对象。tmp对象构造结束后,a对象变成了将亡值,此时a对象内部的资源已被掏空。
在这里插入图片描述

通过移动赋值运算,将b对象内部的资源转移至对象a中,此时b对象变成了将亡值。
在这里插入图片描述

再次通过移动赋值运算,将tmp对象内部的资源转移值对象b中,tmp对象变成了将亡值。
在这里插入图片描述

当std::swap()函数接收时,tmp对象离开其作用域,调用析构器将其析构掉。

可以看出,在类提供了移动构造和移动赋值时,此时使用std::swap()交换对象时,仅仅是对象内部资源的搬移,并没有产生资源的拷贝,所以性能很高。

int main()
{
    A a("hello");
    A b("world");

    cout << "&a = " << &a << endl;
    cout << "&b = " << &b << endl;

    cout << "===========" << endl;
    //std::swap(a, b);
    {
        A tmp = std::move(a);
        a = std::move(b);
        b = std::move(tmp);
    }
    
    cout << "===========" << endl;

    cout<<"a = "<<a.c_str()<<endl;
    cout<<"b = "<<b.c_str()<<endl;

    return 0;
}
constuctor :0x7ffc0df0daf0
constuctor :0x7ffc0df0daf8
&a = 0x7ffc0df0daf0
&b = 0x7ffc0df0daf8
===========
0x7ffc0df0db00 move constructor from 0x7ffc0df0daf0
0x7ffc0df0daf0 move operator= from0x7ffc0df0daf8
0x7ffc0df0daf8 move operator= from0x7ffc0df0db00
deconstrutor :0x7ffc0df0db00
===========
a = world
b = hello
deconstrutor :0x7ffc0df0daf8
deconstrutor :0x7ffc0df0daf0

通过这段代码测试可以看出,std::swap(a,b);和如下代码的效果是一致的。

A tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
类内没有移动构造和移动赋值运算符
int main()
{
    A a("hello");
    A b("world");

    cout << "&a = " << &a << endl;
    cout << "&b = " << &b << endl;

    cout << "===========" << endl;
    std::swap(a, b);
    cout << "===========" << endl;

    cout<<"a = "<<a.c_str()<<endl;
    cout<<"b = "<<b.c_str()<<endl;

    return 0;
}
constuctor :0x7ffef856f578
constuctor :0x7ffef856f580
&a = 0x7ffef856f578
&b = 0x7ffef856f580
===========
0x7ffef856f540 cpy constructor from 0x7ffef856f578
0x7ffef856f578 operator= from 0x7ffef856f580
0x7ffef856f580 operator= from 0x7ffef856f540
deconstrutor :0x7ffef856f540
===========
a = world
b = hello
deconstrutor :0x7ffef856f580
deconstrutor :0x7ffef856f578

类没有提供了移动构造和移动赋值运算符时,交换两个对象发生了一次拷贝构造、两次拷贝赋值和一次析构。
在这里插入图片描述

调用std::swap函数之前的状态。

在这里插入图片描述

通过a对象拷贝构造出tmp对象,此时a对象内部的资源发生了一次拷贝。

在这里插入图片描述

将b对象赋值给a对象,先是将a对象内部的资源释放掉,之后拷贝一份b对象内部的资源。

在这里插入图片描述

将tmp对象赋值给b对象,此时也会发生资源的释放和拷贝。
在这里插入图片描述

std::swap函数调用结束后,tmp调用析构器将资源销毁。

此时std::swap(a,b)等同于如下代码:

A tmp = a;
a = b;
b = tmp;

通过上面的分析可知,当类没有提供移动构造和移动赋值时,使用std::swap函数交换两个对象时,会导致多次资源的拷贝和释放。如果资源很大,会消耗大量性能。

提供自己的swap函数

在没有移动构造和移动赋值时,可以提供自己的swap函数,同样也可以避免资源的拷贝和释放。

class A
{
public:
    ...
    void swap(A & another)
    {
        std::swap(str_,another.str_);
    }
    ...
};
int main()
{
    A a("hello");
    A b("world");

    cout << "&a = " << &a << endl;
    cout << "&b = " << &b << endl;

    cout << "===========" << endl;
    a.swap(b);
    cout << "===========" << endl;

    cout<<"a = "<<a.c_str()<<endl;
    cout<<"b = "<<b.c_str()<<endl;

    return 0;
}
constuctor :0x7ffcf371c3c8
constuctor :0x7ffcf371c3d0
&a = 0x7ffcf371c3c8
&b = 0x7ffcf371c3d0
===========
===========
a = world
b = hello
deconstrutor :0x7ffcf371c3d0
deconstrutor :0x7ffcf371c3c8

在调用类内部成员函数的时候,直接将a对象和b对象内部资源的指针交换。在交换两个对象时,交换的仅仅是两个指针,效率很高。

class A
{
public:
    ...
    void swap(A & another)
    {
        std::swap(str_,another.str_);
    }
    ...
};

inline void swap(A & x, A &y)
{
    x.swap(y);
}

提供一个全局的swap函数,和std::swap函数实现重载。当在调用swap的参数是A类对象时,会使用我们提供的全局函数,效率更高。

int main()
{
    A a("hello");
    A b("world");

    cout << "&a = " << &a << endl;
    cout << "&b = " << &b << endl;

    cout << "===========" << endl;
    swap(a,b);
    cout << "===========" << endl;

    cout<<"a = "<<a.c_str()<<endl;
    cout<<"b = "<<b.c_str()<<endl;

    return 0;
}
constuctor :0x7ffffd5c6c28
constuctor :0x7ffffd5c6c30
&a = 0x7ffffd5c6c28
&b = 0x7ffffd5c6c30
===========
===========
a = world
b = hello
deconstrutor :0x7ffffd5c6c30
deconstrutor :0x7ffffd5c6c28

在SGI版本的STL中,也是使用上面这种方式为容器提供交换函数。例如vector的交换函数源码如下:

//vector内部函数成员
void swap(vector<T, Alloc>& x) {
    std::swap(start, x.start);                       
    std::swap(finish, x.finish);
    std::swap(end_of_storage, x.end_of_storage);
}

//全局函数
template <class T, class Alloc>
inline void swap(vector<T, Alloc>& x, vector<T, Alloc>& y) {
  x.swap(y);
}

vector内部的swap函数,在交换两个vector容器时,仅仅交换了vector内部的三个指针。

总结

在有资源的类中,如果没有移动构造和移动赋值,此时调用标准库中的swap函数,会发生资源的拷贝和释放,降低程序的性能。我们可以为其提供一个全局重载的swap函数,在这个函数中,交换指向资源的指针就可以实现对象的交换,交换两个指针的效率比交换资源的效率高太多了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值