礦坑系列 ── 四個你不該用 "const" 的時機
礦坑系列首頁:首頁
hackmd 版首頁:首頁
四個你不該用 “const” 的時機 source:C++ Weekly - Ep 75 - Why You Cannot Move From Const
source:C++ Weekly - Ep 322 - Top 4 Places To Never Use “const”
主要原因是因為 const
會破壞移動,看看這個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <iostream> struct S { S () { puts ("S()" ); } S (const S &) { puts ("S(const S &)" );} S (S&&) noexcept { puts ("S(S&&)" ); } ~S () { puts ("~S()" ); } S &operator =(const S &) { puts ("operator=(const S &)" ); return *this ; } S &operator =(S &&) noexcept { puts ("operator=(S &&)" ); return *this ; } }; int main () { S s1; S t1 = std::move (s1); const S s2; S t2 = std::move (s2); S s3; S &&r1 = std::move (s3); const S s4; const S &&r2 = std::move (s4); }
由於 std::move()
基本上是在做轉型,這會保留 const 的修飾,因此若原物件有 const 修飾,就需要使用 const rvalue reference 來做繫結,也因此他會無法去呼叫移動建構子,破壞了移動。
由此可知 const
也會破壞隱式移動(implicit move),有關隱式移動,可以去看之前寫得值類別篇。這邊 Jason Turner 給了四個要注意不該使用 const
的例子:
當 function 回傳 non-reference type 時,return type 不應該用 const 修飾 寫在 return type 的 const
大部分的時候都會被忽略,有時候甚至會破壞效能,如這邊提到的它會破壞隱式移動,看看這段 code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> struct S { S () { puts ("S()" ); } S (const S &) { puts ("S(const S &)" );} S (S&&) noexcept { puts ("S(S&&)" ); } ~S () { puts ("~S()" ); } S &operator =(const S &) { puts ("operator=(const S &)" ); return *this ; } S &operator =(S &&) noexcept { puts ("operator=(S &&)" ); return *this ; } }; const S make_value () { return S{}; } S make_value2 () { return S{}; } int main () { puts ("s" ); S s; s = make_value (); puts ("\ns2" ); S s2; s2 = make_value2 (); puts ("\nend" ); }
輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 s S() S() operator=(const S &) ~S() s2 S() S() operator=(S &&) ~S() end ~S() ~S()
這邊有兩個 function,上面那個在 return type 上有 const
修飾,下面的沒有。你可以看見上面那個使用的是 copy,而下面那個使用的是 move。
原因就如前面所述,一旦加上了 const
,reference 要連結時就變成要使用 const rvalue reference,但通常 const rvalue reference 並不會有用處(幾乎沒意義,很多餘),因此通常我們函式不會實作 const rvalue reference 的版本,也因此會去呼叫複製,破壞了隱式移動。
當然,const rvalue reference 偶爾會有用就是了,真的很偶爾。
額外閱讀:Do rvalue references to const have any use?
需要隱式移動時,回傳的變數不該用 const 修飾 原因一樣是因為會破壞隱式移動,看看這個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> struct S { S () { puts ("S()" ); } S (const S &) { puts ("S(const S &)" );} S (S&&) noexcept { puts ("S(S&&)" ); } ~S () { puts ("~S()" ); } S &operator =(const S &) { puts ("operator=(const S &)" ); return *this ; } S &operator =(S &&) noexcept { puts ("operator=(S &&)" ); return *this ; } }; S make_value (bool flag) { if (flag) { const S s; return s; } else { const S s2; return s2; } } int main (int argc, const char *[]) { S s = make_value (argc == 1 ); }
輸出:
1 2 3 4 S() S(const S &) ~S() ~S()
function 內回傳的是有名物件,因此套用的是 NRVO,但因為有 branch,因此編譯器會嘗試去做隱式移動,然而一樣由於無法利用 rvalue reference 去做連結,因此無法套用移動,導致去呼叫了 copy。
在你可能需要直接回傳的 non-trivial 參數上,不應該用 const 修飾 原因跟前面都一樣,因為會破壞隱式移動,看這個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> struct S { S () { puts ("S()" ); } S (const S &) { puts ("S(const S &)" );} S (S&&) noexcept { puts ("S(S&&)" ); } ~S () { puts ("~S()" ); } S &operator =(const S &) { puts ("operator=(const S &)" ); return *this ; } S &operator =(S &&) noexcept { puts ("operator=(S &&)" ); return *this ; } }; S make_value (const S s) { return s; } int main (int argc, const char *[]) { S s = make_value (S{}); }
輸出:
1 2 3 4 S() S(const S &) ~S() ~S()
當然我們可能會使用 const reference,那就沒關係,但這邊討論的是一般的 pass by value 的狀況。
額外閱讀:What is a non-trivial constructor in C++?
Data member 不應該用 const 修飾 這點就比較有趣了,如果你的 data member 有 const
修飾,那這不只會破壞掉隱式移動,也有可能會破壞掉 assignment 的語意,看看這個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> struct S { S () { puts ("S()" ); } S (const S &) { puts ("S(const S &)" );} S (S&&) noexcept { puts ("S(S&&)" ); } ~S () { puts ("~S()" ); } S &operator =(const S &) { puts ("operator=(const S &)" ); return *this ; } S &operator =(S &&) noexcept { puts ("operator=(S &&)" ); return *this ; } }; struct Data { const S s; }; int main (int argc, const char *[]) { Data d; Data d2 = std::move (d); }
輸出:
1 2 3 4 S() S(const S &) ~S() ~S()
上例中你可以看見 s 使用的並不是 move 而是 copy,這跟我們大部分時候預期的不太一樣,除非你有特殊用途。
這在我們要使用 STL 容器時會有些問題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream> #include <vector> struct S { S () { puts ("S()" ); } S (const S &) { puts ("S(const S &)" );} S (S&&) noexcept { puts ("S(S&&)" ); } ~S () { puts ("~S()" ); } S &operator =(const S &) { puts ("operator=(const S &)" ); return *this ; } S &operator =(S &&) noexcept { puts ("operator=(S &&)" ); return *this ; } }; struct Data { const S s; }; int main (int argc, const char *[]) { std::vector<Data> data; data.emplace_back (); data.emplace_back (); data.emplace_back (); }
輸出:
1 2 3 4 5 6 7 8 9 10 11 12 S() S() S(const S &) ~S() S() S(const S &) S(const S &) ~S() ~S() ~S() ~S() ~S()
因為我們把隱式移動給破壞掉了,導致在 resize 容器時會需要整個複製,如果 s
沒有加上 const,輸出會是:
1 2 3 4 5 6 7 8 9 10 11 12 S() S() S(S&&) ~S() S() S(S&&) ~S() S(S&&) ~S() ~S() ~S() ~S()