究竟是c++的發展進入了邪路,還是我寫代碼的姿勢不正確?

本人是個c++小菜,因為之前也用其他語言做開發,現在用c++覺得好累,尤其是c++11之後,雖然c++11有很多特性大大簡化了代碼的編寫,比如(auto, lambda表達式,範圍for循環等),但是即使是添加一個小小的函數也要思考很久,我開始懷疑是不是我的水平問題,還是c++的設計問題。以下舉幾個例子:

例子一(傳參到底用值類型,const引用,右值引用,還是其他):

class Example1 {

public:

void init(const string v1, const vector& v2) {

_v1 = v1;

_v2 = v2;

}

void setV1(const string v1) { _v1 = v1; }

private:

string _v1;

vector& _v2;

};

如果是c++98以前,對於設置一個成員變數,比如setV1這樣的函數,我會毫不猶豫的選擇參數為const引用類型。但是現在發現,如果使用const引用,參數必定會被拷貝一次,改成如下,似乎變成了最多拷貝一次:

void setV1(string v1) { _v1 = std::move(v1); }

用戶的調用方式:

string v1 = "test";

Example1 e;

e.setV1(v1); //move一次,拷貝一次

e.setV1(std::move(v1)); // move兩次

進一步思考又發現,move畢竟還是有開銷,不如引用來的快,於是以上版本肯定不是最好的,最好的看起來是提供一個針對右值引用的重載,這樣參數都是通過引用傳遞了,改成如下:

void setV1(const string v1) { _v1 = v1; }

void setV1(string v1) { _v1 = move(v1); }

用戶的調用方式:

string v1 = "test";

Example1 e;

e.setV1(v1); //拷貝一次

e.setV1(std::move(v1)); // move一次

那麼兩個參數的方法如void init(const string v1, const vector& v2)又該怎麼寫呢?參數的傳遞方式排列組合嗎?

設置一個屬性竟然要寫兩個方法,後來我知道有個簡單的方法可以實現這個功能,改成如下:

template&

void setV1(T v1) { _v1 = forward(v1); }

但是這樣丟了類型限定信息,參數多的情況又怎樣呢?(我這裡描述的不準確,我想表達的意思是,從介面上根本看不出來應該傳什麼類型的參數)

如果實現稍微複雜點,比如:

void setV1(const string v1) {if (v1.size() &<= 10) _v1 = v1; }

參數又該怎麼傳比較好?

例子二(關於constexpr):

class Example2 {

public:

constexpr Example2(int v1, int v2): _v1(v1), _v2(v2) {}

constexpr getV1() { return _v1 } const

private:

int _v1;

int _v2;

};

c++終於能有編譯期常量來一定程度的取代enum, 但是這樣每次寫方法都要思考是不是可以constexpr了,而且諸如類模板pair& 是怎麼指定的?編譯器會有選擇的忽略constexpr?

例子三(關於noexcept):

每次寫函數都要看一下該函數所有調用的函數是否會拋出異常再決定noexcept是true or false? 考慮到noexcept肯定生成的代碼性能更優

其他(比如模板。。。),不想舉例

最後,我的問題是, 我只是想添加一個方法, 然後我就要考慮那麼多?參數傳遞方式選哪一種(而且還經常選不出最優的)?考慮是否可以constexpr(這個其實有點底層啊)?查看調用的若干函數是否拋異常決定異常提示符是true or false?

當然我清楚這大部份是因為我對c++的理解太淺的原因,請大家溫柔點


是你的思維誤入歧途了……

你想想,你寫其他語言的時候,會不會考慮參數怎麼傳遞,有沒有開銷?

是你自己在寫C++時考慮了這麼多啊……

要我說你還是別在這種細節上摳了,profile能體現出來嗎?

先不去追求極致效率,根據需求來吧,比如多線程里傳個引用就很值得考慮了……

你第一個例子,傳引用是為了避免複製,如果終究還是要複製,早複製晚複製能有多大差別。要適當的相信編譯器,能給你做出合理的優化。move的開銷還能比複製來得大么……你要是擔心真的會頻繁複制帶來內存頻繁分配的開銷,你就自己寫個字元串的緩存池,搞immutable唄……

第三個例子,noexcept用來輔助編譯器優化,但我想問能優化掉幾條語句,是程序熱點嘛……現在的異常在happy path上都沒多少開銷……

真因為擔心開銷而束手束腳,那何不去寫彙編嘛,省到極致……


腦補優化是可以的,但你現在的水平壓根做不到。你現在不知道:

  • 程序到底性能瓶頸是什麼地方。
  • 編譯器優化了哪裡,又不太能優化哪裡。


http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines 請參考Performance部分前幾條


不要通過copy和move的數量來腦補性能,上Quick C++ Benchmarks

constexpr,如果不寫會編譯報錯,那你再寫,否則不寫

noexcept,同上

模板,越簡單越好,如果某個東西(noexcept,constexpr,forward ref,SFINAE)不寫會報錯,那你再寫


今天跟題主想到同一個問題了,google了一下:

Is it better in C++ to pass by value or pass by constant reference?stackoverflow.com圖標

這個回答里高贊答案推薦的做法基本上就是:

1. 如果你要在函數里修改參數,就傳

2. 如果你不修改但是要在函數里弄一份拷貝,就傳值,然後用move把它吃掉

3. 如果你不修改也不拷貝,就傳const (int, double和小class之類的直接傳值)

4. 還有就是你提到的move兩次把外面的object吃掉,就傳

大部分函數你不用上面每個類型都寫一次啊,而且只要你和調用方約定好,對方看到你的API就知道這個函數想要怎麼用他的參數,意圖很清晰。


想省事的話,就每一個類都去設計一個完美的右值引用構造函數,然後全部傳值。這樣不管參數是左值還是右值,函數裡面要直接用還是複製走,都可以不會真的去做多餘的複製工作。這個你可以自己分析一下,以前寫過太多次懶得再寫了。因為4個參數就重載16個函數顯然是不可接受的。

如果你的類型的右值引用構造函數沒辦法做到特別簡單的話,那你可以考慮永遠使用const T來傳值。對於只有幾個位元組的值類型(如struct裡面兩個int),那用值類型就好了。


一般的 get set 之類的操作可以這樣寫:

class Example{
public:
std::string v1(){return v1_;}
const std::string v1()const{return v1_;}
private:
std::string v1_;
};

size_t foo(){
Example ex;
ex.v1()="some"; /// set value
return ex.v1().size(); /// get value
}

成員函數反正會優化掉的。

C++ 標準庫的容器和 std::string 基本是這種寫法,比如

string::back - C++ Referencewww.cplusplus.com

vector::data - C++ Referencewww.cplusplus.com


不要過早優化,優化要等到跑完VTune之後再說。你實際使用的時候,可能傳右值和傳引用只用到其中一個。永遠先讓代碼跑起來,跑對了,發現有必要再摳這些細節。


剛學C++的時候,面試的人問我const有什麼用?我會說const就是常量呀,裡面的值不能改呀。這幾年看了不少書也寫了不少C++的代碼,我覺得const的修飾性作用要遠遠大於它的功能性作用,換句話說,與其說const是給編譯器看的,不如說是給人看的,比如一個變數加了const,就表明該變數不可讀,一個成員函數加了const,表明它對於類而言是「只讀」,所以什麼const_cast這種東西我是大大不推薦使用的,因為用了這玩意代碼的可讀性一下子就下來了,本來以為某個A是只讀的,代碼里不可能回對它做修改,結果某個同學在某個不為人知的陰暗角落裡對它做了個const_cast,然後值又被改變了,想想是有點邪門。但是C++就是這樣,作為一個好的C++程序員,能認識到這些問題很不錯,給題主點個贊。


是你,以及很多人的心理問題。

先別考慮性能,先設計好介面和類吧。然後再根據benchmark做相應調整即可。

https://www.zhihu.com/question/51284083/answer/125091030

C++就怕想太多。

想的多,當然可能開發慢(光想就花時間了,還別說C++程序員大多關注了輪帶逛)(或者說寫些多餘的東西)

Java不讓你想那麼多。(但我卻因為想太多,所以不用Java以及Go)。


setter 函數的參數永遠要選 const 形式,否則setter實現裡面把參數對象給改寫了有你哭的時候。

介面設計的首要目的不是性能而是安全性,可讀性,可維護性。


新特性的引入,是為了讓代碼所表達的語義儘可能體現你的思路,減少語言本身帶來的委曲求全。所以究竟如何寫,還是要看你要解決什麼樣的問題。

比如前兩個例子,你之所以糾結,是因為你想要找到銀彈,一招鮮吃遍天,這不現實。

而第三個例子其實也是一樣,你需要先搞清楚你想解決什麼問題,確定你要寫的函數是幹什麼的、能不能出錯,就可以確定會不會拋異常了,而不是反過來根據實現來確定介面。


趕緊Effective Mordern C++,答疑解惑

裡面有幾個條款和你的疑惑有關,都是業界最佳實踐的答案吧


@靈劍 大大說了,不要過早優化。

比如按你的example1 string的stl的實現底層有好幾種優化,可能會按你的size選擇不同的處理方法。就算string複製了又怎樣?你要相信該優化的地方庫作者會幫你優化的。另外所謂完美轉發就是不會丟失類型限定。所以你要用現在cxx的話就一律用通用引用好了,不需要考慮那麼多有的沒的。

另外constexpr這種,搞不清楚就別加,除非你要拿來當數組的size用。你要相信編譯優化階段,需要變常量的自然會變成常量。

然後就是noexcept,又不是說以前的cxx就沒有異常開銷,他只是提供個統一的方式指明為無異常而已。然後調用的函數優先使用無異常版本。其實你現在這個階段也沒必要死扣這個無異常,反正你用的第三方庫十有八九也不是noexcept的。

lz你要相信cxx編譯器這麼多年的迭代,編譯優化水平絕對是最牛x的沒有之一。

最後最後,等到你真分析出哪部分是瓶頸了,再去針對性優化那一塊吧。那個時候你再來扣這部分的細節不遲,你信不信90%是改個用法就能解決問題。真到你需要扣編譯優化的地步,什麼語言都跑不了,而且有些語言你想扣都沒發扣。我就喜歡把介面改成向前不兼容,然後修復新介面的所有編譯錯誤就完事兒了。


因為寫 C++ 容易想太多,總想著這裡可以避免一次拷貝,那裡可以少一次 new,所以到最後就寫不下去了。

所以說,乾脆shared_ptr一把梭算了(逃


如果你覺得 C++ 走上邪路,那我明確告訴你, C++ 早在 C++98 就走上邪路了。因為現在添加的很多特性都是在填 C++98 的坑。

1) 傳值就好了,反正都要複製。如果看著傳值不爽,就用轉發的寫法,類型限定的東西總能弄回來的。

2) constexpr 實際上是自動依賴的。某個模板就算標了 constexpr ,假如形參類型不滿足要求,特化後也不是 constexpr 。

假如你覺得哪個函數結果需要弄到模板參數里,或者哪個類需要保證靜態初始化(如 unique_ptr 這種),就加上 constexpr 吧。

3) 有些庫會依賴 noexcept 的狀況選擇所用的具體操作,這時適配一下就行。雖然移動構造/移動賦值/移動轉換/交換等函數最好都是 noexcept 。


void setV1(string v1) { _v1 = std::move(v1); }

e.setV1(std::move(v1)); // move一次

傳值這種方式最簡單,開銷大多數時候可以不計,而且很多時候會被編譯器優化掉。

要麼你就用完美轉發

template&
void setV1(T v1) {
_v1 = std::forward&(v1);
}

要限定類型的話只能用 SFINAE

template&
typename std::enable_if&< std::is_trivially_constructible&::value, void&>::type
setV1(T v1) {
_v1 = std::forward&(v1);
}

不然你就要各種情況分別考慮,很麻煩的。

還是直接傳值吧,只要你寫成 inline 函數(直接寫在類裡面),多半都會被優化掉的。


你沒錯,錯的是 C++。

C++早就失控了,本身語言的特性隨著新的標準不停的膨脹。再加上不同平台,不同領域的各種專有寫法,一個項目用上C++是非常危險的。比如遊戲服務端架構設計的不好,在運行一定時間出現內存泄漏,在有限時間下,需要層層排查,會讓技術人員面臨過大的壓力。解決方法,要不就是用QT,或者像寫C#一樣來寫C++。設計一套力求簡單表現邏輯,高可讀性的規範。在規範下進行書寫C++,把複雜度降到最低,才能避免混亂。

在.netCore出來之後,我早就放棄C++了。.netCore支持JIT編譯,也支持AOT編譯。可以直接將C#直接編譯成機器碼。也可以將C#代碼編譯成C++代碼,然後調用對應平台的C++編譯器優化編譯成機器碼。加上微軟重新設計的.netAPI,解決起問題來簡潔而又優雅。

在這些基礎上,然後再定一套C#的可讀性規範,屏蔽一些複雜用法。寫代碼的時候,腦海裡面都是直接解決問題的邏輯,寫出來的代碼非常簡短,可讀性非常高。出了bug的時候,任何人都能很輕鬆閱讀,並直接定位過去修改,簡直不能再爽。


std::move最大的意義在寫庫,而不是業務代碼。

對於業務代碼來說,只要能堅持使用stl容器,那就已經自動享受了大部分好處了,不用管什麼移動,拷貝,交給編譯器自動管就行了。

實在要寫不能用容器的大段代碼,比如需要自己分配管理內存,那參考樓上各位大神的精髓 - 不要過早優化


實際項目的感覺是,右值引用,lambda,auto確實增加了編碼或閱讀負擔,開發效率相對98標準也沒有顯著提升,尤其是seastar那種回調風格的lambda,代碼沒有任何可維護性,再立的c++項目,準備嚴格限制新特性的使用。


推薦閱讀:

哪些公司的哪些團隊有嚴格的 code review?
軟體開發行業相關的介紹?小白成長起來需要多少東西?
如何知道自己是否適合做軟體開發?
大齡門外漢如何進入軟體開發行業?

TAG:軟體開發 | 編程語言 | C | CC |