C++代碼簡(jiǎn)化之道

本文會(huì)介紹10個(gè)條款,后續(xù)還會(huì)陸續(xù)更新相關(guān)的內(nèi)容,請(qǐng)大家持續(xù)關(guān)注!
1. 善用emplace
C++11開始STL容器出現(xiàn)了emplace(置入)的語義。比如 vector、map、unordered_map,甚至 stack和 queue都有。
emplace方便之處在于,可以用函數(shù)參數(shù)自動(dòng)構(gòu)造對(duì)象,而不是向vector的push_back,map的insert那樣傳入一個(gè)構(gòu)造好的對(duì)象。
舉個(gè)例子,比如有這么一個(gè)對(duì)象。
class Point {
public:
Point(int x, int y):_x(x),_y(y){}
private:
int _x;
int _y;
};
C++11之前。大概的寫法
std::vector<Point> vp;
std::map<std::string, Point> mp;
Point p(1, 2);
vp.push_back(p);
vp.push_back(Pointer(3, 4));
Point p1(10, 20);
mp.insert(std::pair<std::string, Point>("key1", p1));
Point p2(100, 200);
mp.insert(std::make_pair("key2", p2));
C++11之后:
std::vector<Point> vp;
std::map<std::string, Point> mp;
vp.emplace_back(1, 2);
vp.emplace_back(3, 4);
Point p1(10, 20);
Point p2(100, 200);
mp.emplace("key1", p1);
mp.emplace("key2", p2);
注意,其實(shí)也不需要無腦使用emplace_back。比如,當(dāng)你的使用場(chǎng)景中,已經(jīng)確切存在了一個(gè)Point的對(duì)象,你需要把它放進(jìn)vector:
// 彼時(shí),你已經(jīng)有了一個(gè)Point的對(duì)象p。不需要自己憑空構(gòu)造。
vp.push_back(p);
vp.emplace_back(p);
這種情況下,兩種寫法的表現(xiàn)幾乎無差別(push_back反而短……當(dāng)然可能也沒必要追求這個(gè))。見過一些老項(xiàng)目升級(jí)C++11之后,無腦給push_back全替換成emplace_back的。雖然也沒啥問題,但其實(shí)有時(shí)候沒必要。
當(dāng)然,當(dāng)需要從參數(shù)來構(gòu)造出對(duì)象的時(shí)候。那么 emplace_back明顯會(huì)簡(jiǎn)潔許多。但此時(shí)push_back其實(shí)除了代碼冗長(zhǎng)外,其性能開銷也沒有比emplace_back高太多,因?yàn)?/p>
vp.push_back(Pointer(3, 4));
調(diào)用的是:
void push_back (value_type&& val);
有較真的網(wǎng)友提到 emplace的置入功能,還是要比這種push_back (value_type&& val)稍勝一籌,anyway。兩個(gè)函數(shù)實(shí)現(xiàn)邏輯不同,肯定無法做到性能完全一致,但是也沒到足以影響自己編碼習(xí)慣的地步??偠灾?dāng)要放入 vector的對(duì)象不存在的時(shí)候,直接用 emplace_back來構(gòu)造,在已存在的時(shí)候用 emplace_back或 push_back都可以。
2. 在不影響可讀性的情況下使用auto,區(qū)分auto& 、auto&&
auto不多解釋了。
很多C++程序員被問『熟悉C++11嗎?說一說』
答一個(gè)『auto』
沒啦
auto就是用來簡(jiǎn)化長(zhǎng)類型的(比如命名空間嵌套曾經(jīng)很深)。另外auto&和auto&&(萬能引用)也不多解釋了。
當(dāng)然濫用auto也會(huì)造成代碼可讀性變差。在我等不用IDE,用vim開發(fā)C++的程序員面前,auto濫用猶如噩夢(mèng)。沒有類型提示啊。
3. lambda表達(dá)式替換手寫函數(shù)和函數(shù)對(duì)象
lambda表達(dá)式(或者說lamba對(duì)象)可能是C++程序員在回答『熟悉C++11嗎?』這個(gè)問題,答完auto之后,說出的第二個(gè)新語法。
有了lambda,STL的algorithm里的函數(shù),用起來更簡(jiǎn)潔了。
另外lambda除了替代了定義普通函數(shù)、函數(shù)對(duì)象(重載operator())之外,還有其他便利。那就是閉包的特性。說閉包可能一時(shí)難以理解。你就可以理解成是lambda的引用捕獲功能。
在lambda的參數(shù)之外,獲取到了其他的參數(shù)。并且是可跨越lambda生命周期的。
唯一需要注意的是:引用捕獲可能在后續(xù)lambda對(duì)象被實(shí)際調(diào)用的時(shí)候,出現(xiàn)引用懸空(類似空指針),從而出現(xiàn)core dump。
4. 給冗長(zhǎng)的類型建立別名,尤其是std::function類型
看一段冗長(zhǎng)的代碼。
class FuncFactory {
public:
void put_func(std::string, std::function<std::vector<std::string>(std::string)>);
std::function<std::vector<std::string>(std::string)> get_func(std::string);
private:
std::unordered_map<std::string, std::function<std::vector<std::string>(std::string)>> _func_map;
};
用using簡(jiǎn)化掉:
using func_t = std::function<std::vector<std::string>(std::string)>;
class FuncFactory {
public:
void put_func(std::string, func_t);
func_t get_func(std::string);
private:
std::unordered_map<std::string, func_t> _func_map;
};
5. 頭文件中使用#pragma once替換老破舊#ifndef #define #endif
從上個(gè)世紀(jì)70年代C語言誕生之始,頭文件都在使用#ifndef #define #endif來避免重復(fù)包含。
#ifndef HEADER_FILE
#define HEADER_FILE
...
#endif
C++也繼承了這種寫法。然而時(shí)至今日還可以這樣寫:
#pragma once
...
這個(gè)語法很久之前就有,但并非是C++標(biāo)準(zhǔn)的一部分。但在很多編譯器廠商的實(shí)現(xiàn)中,早早地支持了這種語法。C++11中這個(gè)語法依舊沒有轉(zhuǎn)正,但是由于被編譯器廣泛支持,幾乎可以放心使用了。在Google和Facebook的C++開源項(xiàng)目中都有大量使用。#ifndef #define #endif終于壽終正寢。
當(dāng)然在個(gè)別情況下,這個(gè)語法也存在坑:
不同于頭文件防護(hù),這條語用使得錯(cuò)誤地在多個(gè)文件中使用相同的宏名變得不可能。另一方面,因?yàn)閹?pragma once的文件是基于其文件系統(tǒng)層次的身份所排除的,所以若頭文件在項(xiàng)目中有多個(gè)位置,則這不能防止包含它兩次。
可以參考:https://zh.cppreference.com/w/cpp/preprocessor/impl
簡(jiǎn)而言之,pragma是基于頭文件的文件路徑來保持唯一的。而宏可以做到跨多個(gè)文件來保持include的唯一性。比如當(dāng)你一個(gè)代碼庫中存在一個(gè)頭文件的多個(gè)版本……
一般情況下,我們可能很少在一個(gè)項(xiàng)目中需要用到一個(gè)頭文件的多個(gè)版本,反正我是沒這種需求。
6. 善用for range遍歷容器,也可以針對(duì)PB的repeated字段(甚至mutable)
還在用下標(biāo)遍歷容器嗎?
for (int i = 0; i < v.size(); ++i) {
cout<<v[i]<<endl;
v[i] *= 10;
}
java和其他語言早有不借助下標(biāo)的for - range循環(huán),C++11也有了:
for (auto& e: v) {
cout<<e<<endl;
e *= 10;
}
最好用引用&來遍歷,否則如果容器中存儲(chǔ)的是對(duì)象,會(huì)出現(xiàn)拷貝。當(dāng)然如果你不想修改容器內(nèi)元素的話,也可以用const auto& 遍歷。
C++工程項(xiàng)目中,protobuf肯定是會(huì)大量使用的。for range也可以遍歷pb的repeated字段
syntax = "proto3";
message Student {
string name = 1;
int32 score = 2;
}
message Report {
repeated Student student = 1;
}
代碼中:
// report 是一個(gè)Report類型的對(duì)象
for (auto& student: report.student()) {
cout<< student.name << "'s score:" << student.score << endl;
}
工作中看多很多遍歷pb repeated字段代碼大多可以做到上面那樣。但是當(dāng)遍歷pb repeated字段并修改其中變量的時(shí)候(mutable返回的是指針,不能直接for range),很多人還是選擇了用傳統(tǒng)的for+下標(biāo)的形式來遍歷。其實(shí)不用,依舊可以for range
for (int i = 0; i < report.student_size(); ++i) {
report.mutable_student(i)->set_score(60); // 60分萬歲!
}
啰嗦?。。。。。。。。。?!可以這樣寫:
for (auot& student: *report.mutable_student()) {
student.set_score(60); // 60分萬歲!
}
7. 用do while或IIFE跳過部分連續(xù)邏輯,但不結(jié)束函數(shù)
你有沒有這種體驗(yàn):在函數(shù)中一段平鋪的邏輯中,依次經(jīng)歷1,2,3三個(gè)步驟,然后是其他邏輯(比如 4,5)。其中1,如果失敗就不執(zhí)行2,2如果失敗不執(zhí)行3。就是邏輯中斷之后直接跳到4和5。容易想到的實(shí)現(xiàn)思路有三:
其一:把步驟1,2,3抽象成函數(shù)。每次判斷函數(shù)的返回值,成功才調(diào)用下一個(gè)函數(shù)。OK。這樣沒問題。但是如果順序邏輯太多。那么要抽成很多個(gè)函數(shù),而且每個(gè)函數(shù)內(nèi)只有寥寥幾行代碼。反而啰嗦。
其二:使用異常。如果是Java語言應(yīng)該很習(xí)慣用異常來實(shí)現(xiàn)這個(gè)邏輯,把順序邏輯封在 try catch塊里。每個(gè)步驟失敗直接throw異常。OK,C++也可以寫類似的代碼。然而C++用異常隱患很多,不如Java安全,很多工程規(guī)范都竭力避免拋異常。另外就是拋異常也不是無開銷的,而且這里只是邏輯中斷,邏輯上也不算『異?!?,通過throw異常和catch異常的方式未免更加影響表現(xiàn)力……
其三:goto。看過一些代碼確實(shí)在這種場(chǎng)合使用過goto。當(dāng)然我們要嚴(yán)厲禁止goto。這個(gè)方案直接略過。
其實(shí)還有第4種方案:do while(0)
do {
// 步驟1
...
if (步驟1失敗) {
break;
}
// 步驟2
...
if (步驟2失敗) {
break;
}
// 步驟3
...
if (步驟3失敗) {
break;
}
} while(0);
// 步驟4
...
// 步驟5
...
這個(gè)其實(shí)也適用于其他有do while的語言,不止C++。另外由于C++11中l(wèi)ambda函數(shù)的出現(xiàn),你還可以這樣寫:
[]() {
// 步驟1
...
if (步驟1失敗) {
return;
}
// 步驟2
...
if (步驟2失敗) {
return;
}
// 步驟3
...
if (步驟3失敗) {
return;
}
}();
// 步驟4
...
// 步驟5
...
這個(gè)是在普通 lambda表達(dá)式的末尾加上了一個(gè)括號(hào),也就是讓定義的lambda可以立即執(zhí)行。
這一特性也被人稱為IIFE(Immediately Invoked Function Expression),即立即調(diào)用函數(shù)表達(dá)式。這是一個(gè)出自 Javascript的術(shù)語,可能不是C++中的正統(tǒng)稱呼……
8. 某些情況下用struct替代class,避免把C++類寫成JavaBean
因?yàn)榉N種原因,從Java轉(zhuǎn)C++的程序員,喜歡把C++的類寫成JavaBean。動(dòng)不動(dòng)就set()、get()
當(dāng)然這種封裝也沒問題,數(shù)據(jù)成員設(shè)置成private,所有的訪問都通過接口函數(shù)。只是太教條的話,反而啰嗦。C++中,我喜歡把純數(shù)據(jù)類型(只含數(shù)據(jù))的類,直接用struct來表示。不包含任何成員函數(shù)。也不需要要用class,然后設(shè)置一個(gè)public。就用struct更直觀!
【當(dāng)然,這條可能有爭(zhēng)議~】
9. 函數(shù)直接返回STL容器或?qū)ο蟆2灰祷刂羔槪膊恍枰o函數(shù)加出參
C++11之前。如果要返回一個(gè)STL容器(或其他復(fù)雜類型)的對(duì)象怎么辦?
第一種:
void split(std::string str, std:string del, std::vector<std::string>& str_list) {
// 解析字符串str,按del分隔符分割,拆成小字符串存入str_list中
...
}
// 調(diào)用方:
std::vector<std::string> str_list;
split("a:b:c:d", ":", str_list);
這種用的時(shí)候不太方便。如果不是split,而且其他例子。我可能想一行連續(xù)點(diǎn)點(diǎn)點(diǎn)調(diào)用返回值的成員變量(foo().bar().xxx())。無疑,上面這種會(huì)中斷我的一行語句寫法。
第二種:
std::shard_ptr<std::vector<string>> split(std::string str, std:string del) {
std::shard_ptr<std::vector<string>> p_str_list = std::make_shared<std::vector<std::string>>();
// 解析字符串str,按del分隔符分割,拆成小字符串存入p_str_list中
...
return p_str_list;
}
或者最原始版本:
std::vector<std::string>* split(std::string str, std:string del) {
std::vector<std::string>* p_str_list = new std::vector<std::string>;
// 解析字符串str,按del分隔符分割,拆成小字符串存入p_str_list中
...
return p_str_list;
}
需要小心的處理返回值,自己控制delete掉指針,避免內(nèi)存泄露。
都太啰嗦。但無一例外。熟悉C++98的老前輩們都不會(huì)建議你用函數(shù)直接返回STL容器。然而事情從C++11開始起了變化。那些不熟悉C++98的新手程序員們反而寫出來最優(yōu)解:
std::vector<std::string> split(std::string str, std:string del) {
std::vector<std::string> str_list;
// ...
return str_list;
}
相信我,沒問題。
這個(gè)變化,其實(shí)也在工作中造成一些尷尬。有時(shí)候我寫這種代碼,在給老同事過core review的時(shí)候,生怕被批一頓代碼寫的爛。如果被批一頓,我自然尷尬,然后我解釋一番這種寫法在C++11里面沒問題,那么老同事就尷尬了。
為避免這種尷尬我總會(huì)在代碼附近加個(gè)注釋:
// it's ok in C++11
std::vector<std::string> split(std::string str, std:string del);
其實(shí)C++11之前也有這么用的。因?yàn)榫幾g器自己做的RVO,NRVO優(yōu)化,這當(dāng)然是非標(biāo)的。改一下編譯選項(xiàng)可能就沒啦。雖然gcc不顯式關(guān)閉RVO的話,默認(rèn)就開始的。但曾經(jīng)我在C++98的環(huán)境下工作時(shí),還是很少見到這種直接返回對(duì)象的寫法。其實(shí)不是所有返回對(duì)象函數(shù)定義都能觸發(fā)RVO,如果不清楚,C++98的程序員還是謹(jǐn)慎使用。
但是C++11開始,你不用擔(dān)心了。
10. 利用unordered_map/map的[]運(yùn)算符的默認(rèn)行為
比如我們程序中有一個(gè)計(jì)數(shù)邏輯,使用了一個(gè) unordered_map<string, int>(或map<string, int>)來對(duì)某個(gè) string類型的tag進(jìn)行計(jì)數(shù)。之前看到有同事這樣寫:
// freq_map 是一個(gè) unordered_map<string, int> 類型。
// 通過某個(gè)計(jì)算獲取到了一個(gè)string類型的變量tag,下面進(jìn)行計(jì)數(shù)
if (freq_map.find(tag) == freq_map.end()) {
frea_map.emplace(tag, 1);
} else {
freq_map[tag] += 1;
}
// 或者這種
if (freq_map.find(tag) == freq_map.end()) {
frea_map.emplace(tag, 0);
}
freq_map[tag] += 1;
其實(shí)通通不用,上述兩種大概是python中用dict來計(jì)數(shù)的寫法(當(dāng)年我寫MapReduce任務(wù)的時(shí)候也有類似的寫法)但是C++不用,因?yàn)?。C++的map在使用 [] 運(yùn)算符的時(shí)候會(huì)在key不存在的時(shí)候默認(rèn)創(chuàng)建出一個(gè)值!如果value是基本數(shù)據(jù)類型,那么就是0。
所以可以直接寫:
frep_map[tag]++;
// 或
freq_map[tag] += 1;
當(dāng)然也正因?yàn)?[] 運(yùn)算符的這個(gè)默認(rèn)性質(zhì)所以 Effective C++里面才有一條說要用m.insert()來插入key,value(C++11之后用emplace)而不要用m[key] = value的寫法,因?yàn)楹笳邥?huì)先構(gòu)造一個(gè)空對(duì)象,再覆蓋掉它。當(dāng)然具體到我這里提到這個(gè)計(jì)數(shù)場(chǎng)景,不需要考慮這個(gè)。因?yàn)楸緛砭托枰趉ey不存在的時(shí)候初始化一個(gè),而且value是基本數(shù)據(jù)類型,初始化成0,然后覆蓋成1,開銷不大。
