<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 10519字,需瀏覽 22分鐘

           ·

          2021-09-03 18:17

          我是極簡(jiǎn)主義者,崇尚簡(jiǎn)潔明快的代碼風(fēng)格,這也可能是我不喜歡Java全家桶的原因……當(dāng)然我說的簡(jiǎn)潔是要建立在不降低可讀性的前提下,即不影響代碼本身的表現(xiàn)力。如果為求代碼精簡(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(12);
          vp.push_back(p);
          vp.push_back(Pointer(34));

          Point p1(1020);
          mp.insert(std::pair<std::string, Point>("key1", p1));
          Point p2(100200);
          mp.insert(std::make_pair("key2", p2));

          C++11之后:

          std::vector<Point> vp;
          std::map<std::string, Point> mp;

          vp.emplace_back(12);
          vp.emplace_back(34);

          Point p1(1020);
          Point p2(100200);
          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(34));

          調(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::stringstd::function<std::vector<std::string>(std::string)>);
              std::function<std::vector<std::string>(std::string)get_func(std::string);
          private:
              std::unordered_map<std::stringstd::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::stringfunc_t);
              func_t get_func(std::string);
          private:
              std::unordered_map<std::stringfunc_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::stringsplit(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::stringsplit(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,開銷不大。

          瀏覽 56
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美美女做爱 | 五月婷婷操逼 | 韩国无码精品久久久 | 影音先锋在线爱爱 | 午夜精品久久久久久久免费APP |