1. 程式人生 > >c++ const 成員函式 & 臨時變數 & 右值引用 & move

c++ const 成員函式 & 臨時變數 & 右值引用 & move

const 成員函式

我們知道,在C++中,若一個變數宣告為const型別,則試圖修改該變數的值的操作都被視編譯錯誤。例如:

const char blank = 'a';  
blank = 'b';  // 錯誤  

面向物件程式設計中,為了體現封裝性,通常不允許直接修改類物件的資料成員。若要修改類物件,應呼叫公有成員函式來完成。為了保證const物件的常量性,編譯器須區分不安全與安全的成員函式(即區分試圖修改類物件與不修改類物件的函式),例如:

const Screen blankScreen;  
blankScreen.display();   // 物件的讀操作  
blankScreen.set
('*'); // 錯誤:const類物件不允許修改, but but but...這個不允許被修改並不代表 const 成員函式就一定執行緒安全, see Effective Modern c++ Item 16

在C++中,只有被宣告為const的成員函式才能被一個const類物件呼叫。

要宣告一個const型別的類成員函式,只需要在成員函式引數列表後加上關鍵字const,例如:

class Screen {  
public:  
   char get() const;  
};

在類體之外定義const成員函式時,還必須加上const關鍵字,例如:

char Screen::get
() const { return _screen[_cursor]; }

若將成員成員函式宣告為const,則該函式不允許修改類的資料成員。例如:

class Screen {  
public:  
int ok() const {return _cursor; }  
int error(intival) const { _cursor = ival; }  
};  

在上面成員函式的定義中,ok()的定義是合法的,error()的定義則非法。
值得注意的是,把一個成員函式宣告為const可以保證這個成員函式不修改資料成員,但是,如果據成員是指標(注:bitwise

constness和logical constness),則const成員函式並不能保證不修改指標指向的物件,編譯器不會把這種修改檢測為錯誤。例如:

class Name {  
public:  
void setName(const string &s) const;  
private:  
    char *m_sName;  
};  

void setName(const string &s) const {  
    m_sName = s.c_str();      // 錯誤!不能修改m_sName;  

for (int i = 0; i < s.size(); ++i)   
    m_sName[i] = s[i];       // 不好的風格,但不是錯誤的  
}  

雖然m_Name不能被修改,但m_sNamechar *型別,const成員函式可以修改其所指向的字元。

const成員函式可以被具有相同引數列表的const成員函式過載,例如:

class Screen {  
public:  
char get(int x,int y);  
char get(int x,int y) const;  
};  

在這種情況下,類物件的常量性決定呼叫哪個函式。

const Screen cs;  
Screen cc2;  
char ch = cs.get(0, 0);  // 呼叫const成員函式  
ch = cs2.get(0, 0);     // 呼叫非const成員函式  

小結:

  • 1)const成員函式可以訪問非const物件所有資料,也可以訪問const物件內的所有資料成員;
  • 2)非const成員函式可以可以訪問非const物件所有資料,但是不可以訪問const物件內的任何資料成員;
  • 3)作為一種良好的程式設計風格,在宣告一個成員函式時,若該成員函式並不對資料成員進行修改操作,應儘可能將該成員函式宣告為const 成員函式。
  • 4)此外,儘量將不會被修改的函式引數宣告為 const, 因為這樣的函式也可以將 臨時物件 作為引數,否則會編譯不通過。

函式臨時變數:

Question:
When creating a new instance of a MyClass as an argument to a function like so:

class MyClass
{
  MyClass(int a);
};    
myFunction(MyClass(42));

does the standard make any grantees on the timing of the destructor?
Specifically, can I assume that the it is going to be called before the next statement after the call to myFunction() ?

Answer:
Temporary objects are destroyed at the end of the full expression they are part of.

A full expression is an expression that isn’t a sub-expression of some other expression. Usually this means it ends at the ; (or ) for if, while, switch etc.) denoting the end of the statement. In your example, it’s the end of the function call.

Note that you can extend the lifetime of temporaries by binding them to a const reference. Doing so extends their lifetime to the reference’s lifetime:

MyClass getMyClass();

{
  const MyClass& r = getMyClass(); // full expression ends here
  ...
} // object returned by getMyClass() is destroyed here

If you don’t plan to change the returned object, then this is a nice trick to safe a copy constructor call (compared to MyClass obj = getMyClass();) which unfortunately isn’t very well known. (I suppose C++11’s move semantics will render it less useful, though.)

右值引用:

當你函式返回的值是一個 ,而不是一個 引用 的時候,就會出現一個臨時物件了,所以大家都很喜歡傳遞引用(而不是值),以下討論都是基於傳遞引用的情況。
但是在c++11以前你只能通過一個 const 引用去繫結一個臨時物件(或者叫右值),否則就編譯不通過,const 就意味著你不能對該臨時物件進行任何修改了。

假設一種情況,可以修改臨時物件,例如:在以一個臨時物件為引數構造一個新的物件時,假設那個臨時物件new了一塊記憶體,我們在新的物件中可以直接複製該臨時物件new了記憶體指標,然後將那個臨時物件的指標 修改 為 NULL, 這樣該臨時物件析構的時候就會delete一個NULL指標——記憶體還保留著,而實際分配的記憶體就被轉移到了我們這個新的物件中,這樣是不是就很有效率了呢?

這裡有兩個問題: 第一個就是如何判斷該物件是臨時物件,第二個就是該臨時物件不能是const型別的。

c++11中的右值引用(rvalue reference)解決了這兩個問題!

// 如果func引數是引用(避免複製),則必須為 const 引用
func(std::string("abc"));  // 傳入的是個臨時物件(rvalue), 該臨時物件的生命週期 see above

std::string str("abc");
func(str)  // 傳入的不是臨時變數(lvalue)

Prior to C++11, if you had a temporary object, you could use a “regular” or “lvalue reference” to bind it, but only if it was const:

const string& name = getName(); // ok
string& name = getName(); // NOT ok

The intuition here is that you cannot use a “mutable” reference because, if you did, you’d be able to modify some object that is about to disappear, and that would be dangerous. Notice, by the way, that holding on to a const reference to a temporary object ensures that the temporary object isn’t immediately destructed. This is a nice guarantee of C++, but it is still a temporary object, so you don’t want to modify it.

In C++11, however, there’s a new kind of reference, an “rvalue reference”, that will let you bind a mutable reference to an rvalue, but not an lvalue. In other words, rvalue references are perfect for detecting if a value is temporary object or not. Rvalue references use the && syntax instead of just &, and can be const and non-const, just like lvalue references, although you’ll rarely see a const rvalue reference (as we’ll see, mutable references are kind of the point):

const string&& name = getName(); // ok
string&& name = getName(); // also ok - praise be!

你編寫的 move 函式能夠被成功呼叫還要有兩個條件: 1, 必須傳入一個右值 2, 該右值能夠被修改(即不是const)
std::move就是保證這兩個條件能夠滿足,(std::move 的作用就是將一個變數強制型別轉換右值引用 型別)
例如:

std::vector<int> a = {1,2,3,4,5};
std::vector<int> b = {3,2,1};
b = a;  // 呼叫operator= 複製各個元素
b = std::move(a); // 呼叫 move operator 移動各個元素,高效!所有的容器元素都是通過 new 出來的,所以move用在這裡再好不過了。不過此時 a 就變成了空vector了.

std::string mystring("hello world");  // string 底層也是通過 new 出來的
std::vector<std::string> myvector;

myvector.emplace_back(mystring);  // 這是複製構造,相當於 .push_back(mystring)
myvector.push_back(std::move(mystring));  // 這是 move,高效

但是注意,如果被move的物件是一個flat type資料結構(如 int,double 等),不包含指標成員指向new出來的記憶體空間,則 std::move 並沒有任何效率提高。

如下圖(ref. effective modern c++),move 一個 vector 相當於移動一個指標(vw1 變為 null):
這裡寫圖片描述
還有一個注意點: 在多層右值函式呼叫的時候,每一層函式引數都需要 顯示 強制型別轉換(用std::move函式)為 右值引用 (因為在函式內部,該引數只是一個引用——是一個左值,不是右值),否則下層函式呼叫將會過載引數為 const 引用左值 的版本,而不是右值版本。

Put a final way: both lvalue and rvalue references are lvalue expressions. The difference is that an lvalue reference must be const to hold a reference to an rvalue, whereas an rvalue reference can always hold a reference to an rvalue. It’s like the difference between a pointer, and what is pointed to. The thing pointed-to came from an rvalue, but when we use rvalue reference itself, it results in an lvalue.

不過, rvalue references cannot magically keep an object alive for you, 所以這樣是不可行的( 與返回 lvalue 引用是類似的,不能返回臨時物件的引用 ):

int && getRvalueInt ()
{
    int x = 123;
    return std::move( x );
}

總結下來, 左值引用 和 右值引用 一樣,都只是個引用(是個右值),不同點是,左值引用只能繫結到左值右值引用可以繫結到右值,並且右值常量引用也可以繫結到左值

所以說,右值引用 和 std::move 對那種包含一大塊記憶體的 臨時物件 具有很大的利用率。尤其是對於需要在內部new一些記憶體的物件(如vector, 很長的 string等等)。對於這些物件,自己要寫 move 建構函式 和 move operator(對於自己編寫的類,如果實現了copy operations, move operations, or destructors 這裡頭的任何一個函式,編譯器就不會預設自動生成這些函數了,需要自己挨個實現——or 顯示寫=default)。 而且要考慮自己寫的哪些函式會影響編譯自動生成的函式(預設構造,預設複製,預設賦值 等等等)。
c++11所有的 stl 容器都已經實現了 move constructor 和 move assignment operator, 在使用標準容器的時候可以可以考慮優化一下程式碼。

函式返回值不需要被move

另外需要注意的是,在函式返回值時,不要使用 std::move,因為編譯器會做一些 NRVO 優化工作:

The compiler tries to elide copies, invokes a move constructor if it can’t remove copies, calls a copy constructor if it can’t move, and fails to compile if it can’t copy.

// explicit move
SerialBuffer read( size_t size ) const
{
    SerialBuffer buffer( size );  // 假設 SerialBuffer 支援move
    read( begin( buffer ), end( buffer ) );
    return std::move( buffer );  // 沒必要,編譯器會進行合適優化,包括使用move語意。直接寫成 return buffer 即可。
}