07-困惑多年的難題,是否返回對象

前言

寫了這麼十來篇博客,我發現我的寫作風格都極其相似。好像還停留在高考那種應試要求的“八股文”一樣。前言,提出問題,解決問題,發散思維,總結 可能這種方式更加符合的我思考方式吧,如果有什麼更好的推薦方式,可以評論區留言。謝謝大家!!在《effective C++》的條款20中,推薦使用const-by-reference-to-const替代pass-by-value,很多人如果沒有看到條款就會在函數返回的時候,返回對象的引用。因爲這樣可以減少拷貝複製,以及析構帶來的效率降低風險。在條款21中,提出如果我們必須返回對象時,千萬不要返回引用。這是爲什麼呢? 如果返回這個對象是在函數內部定義的,那麼這個對象的作用域位於函數內部,但是如果返回了這個對象的引用,那麼就將這個函數的內存泄漏出去了。

既然不能返回對象的引用,那我們就只能返回對象的值了。這一點C++標準化委員會心裏也清楚,所以就對這部分進行了相應的優化。至於如何優化我們就來看下面的內容吧。

舊式解決辦法

在C++11之前爲了解決這問題,我們通常是由調用者分配好內存空間,然後將這部分空間以引用的形式調用接口。比如下面這種方式:

MyObject mobj;
auto ec = initialize(&mobj);

MyObject initialize(MyObject mobj) {
	return mobj;
}

這樣是與C兼容的,但是這樣就很麻煩,需要調用者做額外的工作。當然是最簡單的直接返回對象才最符合調用者的期待啊!

auto ec = initialize();

MyObject initialize() {
	MyObject mobj;
	return mobj;
}

返回對象

通常我們可以返回的對象,是滿足可移動構造/賦值的,一般也是滿足可拷貝構造/賦值的,如果這個對象同時又滿足可默認構造,那麼我們就稱這個對象是半正則對象。

根據我們前一個文章所講,六個特殊函數之間存在相互制約關係。所以我們如果要滿足這個半正則對象的要求,我們必須要在這個對象一般構造函數的基礎上加上默認構造函數,拷貝構造函數,移動構造函數,拷貝賦值運算符,移動賦值運算符。

在左值右值那一部分裏面我說過,函數的返回值是一個純右值元素。所以在調用臨時變量的構造函數的時候優先調用右值重載的移動構造函數。

#include<iostream>

using namespace std;

class A {
public:
    A() { cout << "create A()" << endl; }
    ~A() { cout << "delete A()" << endl; }
    A(const A&) { cout << "copy A()" << endl; }
    A(A&&) { cout << "move A()" << endl; }
};

template<typename T>
A return_A(T&& a)
{
    return forward<T>(a);
}

A makeA() {
    return A();
}

int main() {
    A a1;
    auto a2 = return_A(move(a1));
    /*
    create A()
	create A()
	delete A()
	delete A()
    */
    auto a2 = makeA();
    /*
    create A()
	move A()
	delete A()
	delete A()
    */
}

在MSVC當中這個優化不是很強大,但是在GCC或者其他的編譯器中這的結果是不一樣的。

總結

到了這個地方,其實我們需要掌握的內容不是爲什麼這不同的編譯器帶來的效果不一致。而是需要知道什麼時候需要返回對象,在F.20中提出了幾個例外的情況

  • 返回對象可能是存在多態特徵的,比如通過返回父類對象的值來返回子類對象,這會導致對象切割;
  • 移動對象成本很高的情況,這種情況下我們可以考慮把這個對象分配到堆上,然後返回一個指針對象即可;

除了上述這兩種情況外,其它的情況大多可以直接返回對象即可。《effective modern C++》中的條款41,提到針對可複製的形參,在移動成本低且一定會被複制的前提下,考慮將其按值傳遞。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章