c++ 如何高效传递对象,避免不必要的复制

今天在看c11的右值引用特性,遇到个毁三观的问题。在我认知中,函数返回变量会经历两次复制过程,如下例子:
#include <iostream>

class A {
public:

    A() {
        std::cout << " constructor" << std::endl;
    }

    A(const A& orig) {
        std::cout << " copy constructor" << std::endl;
    }

    ~A() {
        std::cout << " destructor" << std::endl;
    }
    
    A& operator=(const A& orig) {
        std::cout << " operator=" << std::endl;
    }

};

A func() {
    return A();
}

int main() {
    A a = func();
    return 0;
}

按我之前的认知,应该输出
 constructor
 copy constructor
 destructor
 copy constructor
 destructor
 destructor
在func中调用复制构造函数来复制return语句中创建的对象,用于返回到main函数,然后析构return语句中创建的对象;func函数返回后,调用a的复制构造函数来复制func返回后的临时对象,然后析构临时对象。最后main函数返回后再析构a。
然而实际在gcc中输出是
 constructor
 destructor
网上查,才知道编译器做了点手脚,它把main中的a直接指向了func中return那句构造的对象,然后func返回时,构造的对象当然不会被析构。这个手脚就是返回值优化(RVO)。不过我们也可以关闭这个优化,只要加上编译选项 -fno-elide-constructors。当加上这个选项后,运行输出
 constructor
 copy constructor
 destructor
 copy constructor
 destructor
 destructor
 

输出时,同时再输出this指针地址,能更直观的看出整个过程:

#include <iostream>

class A {
public:

    A() {
        std::cout << this << " constructor" << std::endl;
    }

    A(const A& orig) {
        std::cout << this << " copy constructor" << std::endl;
    }

    ~A() {
        std::cout << this << " destructor" << std::endl;
    }
    
    A& operator=(const A& orig) {
        std::cout << this << " operator=" << std::endl;
    }
    
    void printAddr() {
        std::cout << this << std::endl;
    }

};

A func() {
    return A();
}

int main() {
    A a = func();
    a.printAddr();
    return 0;
}
加上编译选项-fno-elide-constructors,编译运行输出
0x7ffd892d9d6f constructor
0x7ffd892d9d9f copy constructor
0x7ffd892d9d6f destructor
0x7ffd892d9d9e copy constructor
0x7ffd892d9d9f destructor
0x7ffd892d9d9e
0x7ffd892d9d9e destructor
不加-fno-elide-constructors编译运行输出
0x7ffde009f03f constructor
0x7ffde009f03f
0x7ffde009f03f destructor
另外,若func函数改为返回有名对象:
A func() {
    A local;
    return local;
}
结果是一样的,即有名返回值优化(NRVO)。

若func中传入A对象的引用,再直接返回:

A func(A& r) {
    return r;
}

int main() {
    A f;
    A a = func(f);
    a.printAddr();
    return 0;
}
输出
0x7ffe76589caf constructor
0x7ffe76589cae copy constructor
0x7ffe76589cae
0x7ffe76589cae destructor
0x7ffe76589caf destructor
嗯,知道r到a应该复制一份,合情合理,编译器还挺聪明的嘛。若func中直接传入A对象,再直接返回:
A func(A r) {
    return r;
}

int main() {
    A f;
    A a = func(f);
    a.printAddr();
    return 0;
}
输出
0x7ffead26280e constructor
0x7ffead26280f copy constructor
0x7ffead26280d copy constructor
0x7ffead26280f destructor
0x7ffead26280d
0x7ffead26280d destructor
0x7ffead26280e destructor
除了会多一个从形参到实参的复制,还会多一个形参到a的复制。

接下来讲一个c11右值引用的例子,A中新增一个显式移动构造函数:

#include <iostream>

class A {
public:

    A() {
        std::cout << this << " constructor" << std::endl;
    }

    A(const A& orig) {
        std::cout << this << " copy constructor" << std::endl;
    }

    A(A&& orig) {
        std::cout << this << " move constructor" << std::endl;
    }

    ~A() {
        std::cout << this << " destructor" << std::endl;
    }

    A& operator=(const A& orig) {
        std::cout << this << " operator=" << std::endl;
    }

    void printAddr() {
        std::cout << this << std::endl;
    }

};

A func() {
    return std::move(A());
}

int main() {
    A a = func();

    return 0;
}
因为右值引用是c11新特性,编译时需加上-std=c++0x(或-std=c++11/14/17)。由于RVO,以上代码输出
0x7ffc248d7c8f constructor
0x7ffc248d7c8f destructor
控制变量,我们需要看到新特性右值引用的作用,去掉编译器优化,加上-fno-elide-constructors编译运行输出
0x7ffe158f029f constructor
0x7ffe158f02cf move constructor
0x7ffe158f029f destructor
0x7ffe158f02ce move constructor
0x7ffe158f02cf destructor
0x7ffe158f02ce destructor
从结果能看出,通过std::move()函数,我们可以“移动”对象内存所有权,使得免去逻辑上多余复制的操作,达到资源再利用,提高效率。以上是显式移动,得益于c11新特性,c11从语义上默认支持移动,所以还可以隐式移动,把func改为:
A func() {
//    return std::move(A());
    return A();
}
输出不变。
综上在c11下写返回局部对象的函数,编译器会先自动优化,若某些场景下优化未触发(具体哪些场景可搜索关键字RVO),还会通过移动来避免复制。
此外,还能通过移动来避免右值传参过程中的复制,如下,A不变,新增函数func2:
void func2(A s) {
}

int main() {
    A a;

    std::cout << "copy" << std::endl;
    func2(a);

    std::cout << "\nexplicit move" << std::endl;
    func2(std::move(a));

    std::cout << "\nimplicit move" << std::endl;
    func2(A());

    std::cout << "\ndone" << std::endl;
    return 0;
}
输出
0x7fff2b988efb constructor
copy
0x7fff2b988efc copy constructor
0x7fff2b988efc destructor

explicit move
0x7fff2b988efd move constructor
0x7fff2b988efd destructor

implicit move
0x7fff2b988eff constructor
0x7fff2b988efe move constructor
0x7fff2b988efe destructor
0x7fff2b988eff destructor

done
0x7fff2b988efb destructor
以上std::move(a)和A()都是右值,逻辑上没必要复制到形参,得益于c11新特性,可以通过移动来避免实参到形参的复制。需要注意的:
1. std::move(a)后,a虽没有被立即释放,访问其对象语法上是可以的,但我们清楚其资源已经是别的对象的了,所以访问成员变量(包括析构时析构成员变量)是不允许的,所以在移动构造函数中,形参orig的成员必须被置为nullptr,以防止其访问已不再属于它的资源。
2. 要尽量保证移动构造函数不发生异常(大概因为实参移动到实参或者临时对象返回时抛异常不好处理?),可以通过noexcept关键字,这里可以保证移动构造函数中抛出来的异常会直接调用terminate终止程序。




参考:
https://www.zhihu.com/question/22111546
http://www.cnblogs.com/lengender-12/p/6659833.html
http://book.2cto.com/201306/25367.html
http://blog.csdn.net/immiao/article/details/46876799
http://blog.csdn.net/virtual_func/article/details/48709617

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值