最近互聯網行業一片哀嘆,這是受到三年影響的后遺癥,許多的公司也未能挺過寒冬,一些外資也開始撤出市場,因此許多的IT從業人員加入失業的行列,而且由于公司較少導致許多人求職進度緩慢,很不幸本人也是其中之一,自從參加作業以來,一直都是忙忙碌碌,開始總認為作業只是為了更好的生活,但是一旦作業停下來后自己就覺得失去了一點什么,所以很少有像最近這兩個月左右空閑的時光,人一旦空閑下來就未免有些空虛,空虛的原因大部分還是因為對未來的焦躁不安,比如要擔心口袋余糧問題、擔心兢兢業業的別人會投以不屑的目光,于是在這期間我也嘗試應聘一些崗位,許多面試官問到plain framework是如何實作的時候,說實話一時間我沒法準確的把他們想知道的內容表述清楚,一方面是自己的口才欠缺,一方面確實是因為時隔太久對許多的東西產生了一定的遺忘,大家不要奇怪為何自己會忘了自己寫的東西,拿一件事來說就是我相信大多數人在作業之后對于自己曾經思慮再三覺得十分念念不忘的優美作文或者做題解法都說不出個所以然,人的記憶力是有限的,不然也就不會有那句好記性不如爛筆頭的名言了,重構plain的計劃實際上是從2019年C++準備發布C++20標準的時候產生的,其module模塊概念的引入讓我對于庫開發產生了一種十分美好的期盼,特別是去年年初我就一直在關注C++23,因此在去年就決定在今年內準備完成對plain的重構,其版本號直接提升到了plain2.0,其原因是相比現在的plain1.1rc產生了比較多的變化,我想說的是重構這個框架的目的之一是重拾自己的記憶,完成既定目標的一部分,同時也是增強自己在modern C++方面的技術,希望在這里與朋友們一起學習和探討設計中遇到的一些問題,同時也順便提一句,大環境雖然是不好但是心中還是應當存有希望,寒冬過后一定會有春天,絕境之地也自然會有生機,盲目自怨自艾不如放松心情轉換思維提升自己(當然這里不是說卷),我相信未來總是美好的,祝愿那些失業或者正要失業的朋友們有個好的作業和生活,也祝福正在作業或者無需作業的活得快樂,同時也希望這系列文章(可能會寫幾篇),給大家能夠帶來一定的識訓,順道說一句,這一些列文章不是C++學習的基礎,可能需要大家對該語言有一定程度的了解,該系列文章主要分享的是我在開發和設計中的一些心得體會,如果有不正確的地方希望大家在評論區留言,或者加入群聯系我,我以前沒有詳細地寫過這方面的文章,主要還是因為太懶,而現在寫這些是因為人到了一定歲數是想要總結什么的時候了,再說如果中年危機得不到緩解,互聯網或許不再是我的棲身之處,也希望在這里留下一些有用的知識能夠讓大家對網路編程有一定的理解,(這篇文章幾憾訓了一天時間,看來是因為自己反應力衰退了,如果大家認為有所幫助希望可以關注一下,后續我將繼續分享)
原因
plain1.1rc的不足
1、命名空間問題
如果看過或者接觸過plain的朋友,不難發現命名空間都是以pf_*開頭,說起這個的時候,還是要從plain的前身plainserver(https://github.com/viticm/plainserver)說起,我看了看時間,emm...,時間大概是九年前,那個時候我還深處在南方的沿海城市,從名字上來說,讓人很容易聯想到服務器,這個專案有著非常強的針對性,是實作了一個服務器的常用模型,一個多行程多執行緒模式下的游戲服務器的實作,這套模型是許多現今仍舊有許多老的廠商(搜狐、金山等)可能仍在應用的模式,其應用的原因大致是這些公司成立時間早,在這方面應用該模式已經得心應手(成熟的相對是可靠的,企業不愿意冒風險去嘗試,畢竟嘗試是需要代價的),有興趣的朋友們不妨可以看一下其中的實作,我在以前的文章里提到過該構架并且用圖畫簡單的畫出了其模型,
大家不妨看看plainserver的common目錄,這個目錄下的結構,再來對比下plain下的那些目錄,是不是感覺到有很多相似,沒有錯,plain就是在common上面衍生出來的,一個更精細、抽象和共用的庫,本來common就是作為客戶端和服務器的公用庫而存在,然后重點問題來了,打開檔案看看比如common/src/common/base/log.cc這個檔案,看看其命名空間,不難發現namespace ps_common_base {,沒有錯這就是之前的命名規則,這是我看了google C++編碼規范之后,自己想的一個命名規則,不難看出這里的規則是ps(專案名稱)_common(父目錄)_base(子目錄),
從上面的命名空間的規則,不難發現plain現在的命名空間實際上是在此基礎上做了一點略微調整,自從接觸命名空間后,我就盡量想要讓函式或者物件被包裹起來,這樣是為了減少跨模塊之間的沖突,但是如果以純粹用命名空間和目錄結構來劃分的話,那么目錄的層次結構必然會嵌套很深,于是在plain的時候規定目錄的嵌套最好少于三個層次,這樣也約束了命名空間的嵌套,這樣就形成了pf_basic、pf_sys等一些列的命名空間,同時也有二級嵌套pf_sys::thread這種,忘了提一點pf其實是plain framework的縮寫,說實話這樣命名本來沒有任何問題,可是在使用中會發現一些命名空間和方法產生了重名,比如檔案(file)模塊,這里面封裝了一些跨平臺的檔案操作介面,如下:
namespace pf_file { namespace api { PF_API int32_t openex(const char *filename, int32_t flag); PF_API int32_t openmode_ex(const char *filename, int32_t flag, int32_t mode); PF_API void closeex(int32_t fd); PF_API uint32_t readex(int32_t fd, void *buffer, uint32_t length); PF_API uint32_t writeex(int32_t fd, const void *buffer, uint32_t length); PF_API int32_t fcntlex(int32_t fd, int32_t cmd); PF_API int32_t fcntlarg_ex(int32_t fd, int32_t cmd, int32_t arg); PF_API bool get_nonblocking_ex(int32_t socketid); PF_API void set_nonblocking_ex(int32_t socketid, bool on); PF_API void ioctlex(int32_t fd, int32_t request, void *argp); PF_API uint32_t availableex(int32_t fd); PF_API int32_t dupex(int32_t fd); PF_API int64_t lseekex(int32_t fd, uint64_t offset, int32_t whence); PF_API int64_t tellex(int32_t fd); PF_API bool truncate(const char *filename); PF_API inline bool exists(const std::string &filename) { auto fp = fopen(filename.c_str(), "r"); if (is_null(fp)) return false; fclose(fp); fp = nullptr; return true; } } //namespace api } //namespace pf_file
總體上來說這個命名規則沒有問題,將內容嵌套起來(將介面包裹的十分好),到去年的時候我仍舊是這樣想的,盡量用命名空間來減少這些沖突,我認為這個出發點是沒有任何問題的,可是在使用的時候,我們會這樣pf_file::api::func這種方式進行呼叫,實際上也沒有多大的問題,可是最近我發現這樣呼叫起來不是十分方便,其實這幾個介面就是對于標準介面的封裝,在后面加上了ex就是防止命名沖突,從C的介面來說這里根本沒有命名空間一說,我總覺得自己對于這樣的包裹確實有點過了頭,我認為目錄應該只是為了更好的分類讓使用者能夠快速定位,也不必非得和命名空間扯上什么關系,在呼叫的時候不需要包裹那么深,我覺得從使用者的角度來講plain::func這種呼叫即可,其實這個規則的改變,是我對于std的一點理解,在標準庫中并不是所有東西都包裹的那么強,因此使用起來比較方便,
2、多余的代碼
由于歷史的原因,因為plain源自于老的專案,其中參考了許多之前的設計,所以我在框架內部保留了許多之前封裝的方法,盡管這些方法可能在框架內部使用不上,但是想到使用者可能會在專案中使用因此還是保留了下來,因此這就造成了許多的冗余,在看了一些開源的專案之后,我發現大多的開源專案中代碼的冗余是十分低的,除了框架需要的實作和外部介面以外任何多余的方法和類都不考慮,減少代碼并不意味著功能的減少,而且減少了自己的維護成本,以及使用者的困惑,畢竟當使用者面對那么一大堆介面的時候,大多時候都表現的手足無措,這個其實和《Effective C++》(這兩月才在看,下面我會推薦一兩本)Item 18: 使介面易于正確使用,而難以錯誤使)的道理一致,我認為介面越少越精致則使用者就越加能夠正確使用,這也大概就是大道至簡的實作方式吧,
對于多余的代碼,plain2.0的辦法就是進行清除和保留,清除一些無關緊要的介面,留下來一些真的有用的東西,因為到了C++23后標準庫里面多了許多有意思的東西,對于其已經實作的東西,比如本來想在C++11的plain1.1中想要使用google的StringPiece進行一定的優化,不過好在標準的提升,C++17里面有了string_view,因此這個就不需要額外封裝了,也是因為這個原因,plain2.0d帶著實驗性的目的大刀闊斧的進行大版本的升級,去除里面多余的代碼,特別是.tcc這種檔案,我準備全部移除,將模板的實作也放到.h中去,
使用標準庫可以讓跨平臺的問題得到更好的解決,因為標準庫幫我們實作了許多介面,這些介面在C++98或者C++03都是沒有的,比如C++11中封裝成的std::thread,以前寫跨平臺的時候你需要熟悉pthread還有windows等相應執行緒API,plain1.0的時候執行緒創建就是呼叫系統的API,這讓自己或者使用者閱讀到相關的原始碼時一頭霧水,時代在進步,標準也在更新,在去年底開始火爆的ChatGTP問世以后,就會發現我們這個時代已經處于技術工業革命快速發展的時期,我覺得有必要跟上時代的步伐,需要嘗試或者去了解一些前沿的新技術,哪怕這項技術不太成熟,但是可以讓自己得到一定的提升,
3、設計模式
還有由于基于老專案的原因,由于plain基于common庫,因此這個庫之前有許多的針對性,所以設計模式上,還是停留在它的思維之上,要從設計模式的缺陷說起的話,不妨看看下面代碼的實作:
#include "pf/engine/config.h" #include "pf/db/config.h" #include "pf/script/config.h" #include "pf/net/connection/manager/config.h" #include "pf/net/protocol/config.h" #include "pf/cache/manager.h" #include "pf/basic/type/variable.h" #include "pf/console/config.h" namespace pf_engine { class PF_API Kernel : public pf_basic::Singleton< Kernel > { public: Kernel(); virtual ~Kernel(); public: static Kernel &getsingleton(); static Kernel *getsingleton_pointer(); public: virtual bool init(); virtual void run(); virtual void stop(); public: pf_net::connection::manager::Basic *get_net() { return net_.get(); }; pf_db::Interface *get_db(); pf_cache::Manager *get_cache() { return cache_.get(); }; pf_script::Interface *get_script(); pf_db::Factory *get_db_factory() { return db_factory_.get(); } pf_script::Factory *get_script_factory() { return script_factory_.get(); } pf_console::Application *get_console() { return console_.get(); } ...
以前在使用虛函式之后,就會發現虛函式有很多函式,因為這是實作C++面向物件之一多型的方式之一,因此在框架之中只要有可能需要的繼承,我就將該類的析構設定為虛函式,可是卻完全沒有考慮過這樣的繼承是否應該存在,我以往想的是使用者可以自定義,其實這里可以使用NVI(None virtual interface)或者std::function的Strategy(策略)以及經典的Pimpl手法,這樣暴露過多的需要繼承的東西其實是增加了使用者的負擔,在設計類時,plain1.1rc中設計模式都是以希望使用者重寫虛函式來實作自定義的實作,甚至在類中充斥了大量protected成員變數,這樣的成員變數會造成封裝的破壞還有很可能造成庫升級程序中ABI(Application binary interface)兼容問題,
為了更好的封裝庫,讓我想到的使用C++20開始支持的module里的export、import實作,至于為何使用C++23 without module,我會在下面專門來講這個問題,這里我就再嘮叨一下在設計模式的應用中,與此似乎不太相關的一個問題,那就是plain1.1rc中設計了script和db相關封裝的介面,以前為了讓plain1.1rc做到能夠除了依賴系統標準庫以外不再有任何依賴,我將腳本和資料庫的封裝單獨羅列出去做成了插件模塊,外部需要使用這些介面的時候只需要將插件以自定義型別的方式進行加載,然后就可以使用plain提供的創建和操作等介面了,這一點上本身沒有什么問題,不過現在看起來單獨為這兩個留一個目錄有點浪費了,因為現在里面的實作最多一兩個檔案,所以我打算將它們移到同一個目錄中作為外部的一些介面標準使用,
改良
plain2.0d的目標
1、簡單
簡單是首要目標,就是介面盡量簡單,暴露給使用者的東西盡量少,因此對于封裝產生了比較高的需求,類可能大部分都需要重新設計,正因為大量的類需要重新設計,加上引入了新的標準,所以才有了大版本的提升,至于C++11的版本,后期可能就會逐漸很少維護了,如果大家希望和我一起學習新的標準,那么就可以切換到開發分支dev進行,(這里將簡單列為首要目標,可能有人說plain以前的理念是安全、快速、簡單,難道安全就不重要了嗎?答案是否定的,因為2.0就是在1.1基礎上的改進,安全仍舊作為底線,雖然可能提供不了太多強烈安全保證,但是基本的安全保證肯定是需要的)
plain的英文翻譯叫做平的/簡單的,也是希望該庫能夠提供一些質樸的介面,讓使用者能夠不用產生太多的困惑,其實要將介面設計的簡單并不是一件容易的事情,畢竟很多時候應用面對的場景是比較復雜的,如何能夠將復雜的問題簡單化始終不是一件容易的事情,在這里我得說說風云的skynet框架,這個框架從一開始設計的目的就是處理訊息包的發送和接收問題,在使用者方面只需要關注skynet.callback和skynet.send等少量介面,但是該框架性能是達標的,他使用的Reactor+執行緒池模式實作了高并發,也就是接下來我要提到的快速其中之一(快速不止是性能),一旦涉及到多執行緒,編程就會面臨復雜的環境,不可避免的就會面臨一些race condition(資源競爭),面對資源競爭,我們就要祭出祖傳大法lock(鎖),下一節我會簡單提及一些鎖的描述,這里舉的例子只是說明看起來簡單的東西實作起來不一定那樣簡單,所以在設計2.0的時候,我還是做了一定的考慮,導致有些時候代碼可能會反復重寫,幸好在dev可以隨便折騰,
2、快速
這里的快速有兩層含義,一是快速上手,二是高性能,快速上手的依賴就是介面的簡單清晰,還有就是提供一些特需的一些應用場景,比如創建一個服務器的監聽,只需要引入頭(未來可能直接import)可能只需要三下五除二幾句代碼就快速實作,
既然作為網路庫的框架,那么它要實作高并發,首先我要在這里申明該網路庫暫時不考慮分布式的那種設計,據我所知分布式有一種訊息實作叫nsq,大家可以在github上搜索下,高并發并不能增加網路的吞吐量,但是可以充分利用CPU的多核優勢增加反應速度,所以突破硬體的限制這種事在plain中根本不作考慮,還有就是plain使用傳統的socket的介面來實作網路部分(協議為TCP),并沒有使用intel的DPDK這種基于網卡的技術(據我所知它是所謂的zero copy),提到TCP/IP,不得不多說一句,我在大學的時候教材里的一本就是TCP/IP 詳解(卷1),現在不知道扔到哪里去了,如果看了這本書的話就會對當今的網路構建有個全面的了解,畢竟互聯網現在幾乎都是基于這種技術,面試的時候可能會問三次握手(SYN、SYN+ACK、ACK)和四次揮手的問題,甚至會問網路OSI的七層模型(物理層、資料鏈路層、網路層、傳輸層、會話層、表示層、應用層),更有甚者有些人還會問IP(網路層)/TCP(傳輸層)協議分別處于哪一層,還有需要你知道IP協議是基于鏈路、TCP是基于端對端相關的問題,實際上這些問題不復雜,可是你一旦不去看,都會遺忘,要不是最近我在看到相關的書籍,問到這些我只能搖頭,看,在這里大家是不是也能夠獲得一些面試的答案,如果對于大家有幫助的話就請點個贊吧(開句玩笑,不點贊也沒關系),
作為高并發核心之一的網路模塊,是plain的核心,其實plain1.1的實作就是基于Reactor模式的,網路框架其實不應該對使用者的消費場景進行過多的考慮和干涉,就是只需要做好自己應該做的事,這樣的話網路模塊的設計復雜度會降低不少,最近看了陳碩的muduo開源庫后,才了解到了一種模式叫做one loop per thread,這種模式考慮了并發性能和執行緒安全,我覺得可以利用該模式來改造網路模塊,而且那個異步日志(AsyncLogger)的實作就幾乎參考了該開源庫的實作,我想后面可以做一些優化,得力于新的標準應該有更多提升性能的方法,
并發編程是C++和其他語言中都會面臨到的難點,避免不了的就是使用鎖,當然也有一種lock free的實作,不過在開發中鎖使用還是相對多一些,提到鎖的話大概有mutex(互斥鎖)、rwlock(讀/寫鎖 共享/獨占鎖)、spinlock(自旋鎖),在muduo這個網路開源框架中陳碩的主張是使用mutex就已經足夠,而且其性能并不差,我也贊同這個觀點,畢竟std::mutex是作為C++標準的一部分而其他的鎖則沒有,陳碩在muduo處理鎖的時候,用到了copy on write(寫的時候拷貝)縮小臨界區,這對提高并發和安全做出了強烈保證,我認為這個思想其實是源自《Effective C++》Item 29: 爭取例外安全(exception-safe)的代碼的思想,Scott Meyers在這里提供了一種強烈例外安全的保證實作,那就是copy and swap(拷貝和交換),有興趣的可以去看看具體的章節,
在實作快速的時候,就是盡量隱藏復雜的細節,也就是不要將復雜蔓延到使用者身上,畢竟使用者只需要關心邏輯計算的處理即可,這也是云風在設計skynet時所說的那樣,使用者只需要處理包到來的處理,然后看情況回傳訊息,
其次是底層需要在實作上加快程式運行的效率,除了多執行緒之外,就是減少運行時的開銷,需要做到運行開銷降低我覺得有兩點比較重要:complie time(某些確定計算盡量放在編譯期)、algorithm(演算法),
complie time:編譯期,如果計算放在編譯器,那么運行期就不必要為了計算產生額外的開銷,這個做法在TMP(Template metaprogramming 模板元編程)以及Generic programming(泛型編程)中運用的比較多,舉個面試中的例子,如何求解1到N所有數字相加之和,不能使用乘除法、for、while、if、else、switch、case以及三元運算子?:,看這里有如此之多的限制,普通的遞回能夠解決這個問題嗎?答案是普通遞回需要用到上面的條件陳述句以及運算子,無法使用,那么這道題其實可以從幾個方面來求解,如利用建構式和類的靜態變數、利用虛函式、利用函式指標,有些人如果看過劍指offer,就知道這些我就是從這本書上看到的,其中還有個比較常見的解法,那就是利用模板,我們來看看這道解法的代碼(因為短我才放到這里的):
template <unsigned int N> class CalcNum { public: enum {n = CalcNum<N - 1>::n + N }; }; template <> class CalcNum<1> { public: enum {n = 1}; };
int main() { std::cout << "n: " << CalcNum<100>().n << std::endl; }
你沒有看錯,n的值并不會在運行期被計算,而是在編譯器具現化模板的時候遞回被計算出來的,也就是該計算發生在編譯期,如果N的數字越大所耗費的時間會增多,這時候體現在編譯的時間變長了,但是運行期時沒有計算的消耗,得力于C++11過后增加的關鍵字constexpr,特別是C++14強化過后,編譯期能夠計算的東西就越來越多,而且標準庫里越來越多的介面都是constexpr,可見制定標準的人和業界都認為編譯期編程是值得考慮的,
algorithm:數學是人類偉大的智慧之一,由數學產生的演算法更是能夠解決生活中許多的問題,常見的有概率學、金融學等等,利用演算法可以解決很多難題,而且往往花費很少的時間,當下AI(人工智能)再次被推向風口浪尖的時候,演算法相應的崗位變得炙手可熱,剛剛走出校門的能夠獲得的報酬也比十年多的程式還要多,這怎能不令人羨慕不已,但是演算法是有門檻的,特別是一些高級演算法,你需要非常扎實的數學功底,很抱歉我在數學方面就屬于有些欠缺的,雖然說我家里有一本C/C++函式演算法速查手冊,但是仍舊對于演算法一知半解,最近接觸演算法也是劍指offer中看到的這些,我覺得那些很有代表性,不只是為了面試,更加有助于自己對于自身技能的一種提高和增強,我覺得有興趣的朋友不妨了解一下,
演算法能夠加快運行期的速度的原因,我舉一兩個例子大家就應該會明白,例如一個經典面試題,這個也是13年的時候我在進行網易電話面試時未能正確回答出來的問題,其原因就是我根本沒有看這方面的知識,既然沒有看自然就只能一問三不知了,這個問題就是如何O(1)時間洗掉單向鏈表中的一個節點,如果沒有接觸相關的知識,除非數學功底好以及對鏈表資料結構十分熟悉,否則這個問題也很難回答上來,其實該題目并不復雜,比起用那些公式才能解決的問題簡單太多了,首先要的是你需要熟悉鏈表的資料結構,單向鏈表就是每個節點都有指向下一個節點的指標,鏈表末尾的節點下一個節點的指標為空,有了對鏈表這種資料結構的了解,加上一點發散思維,洗掉節點就是將這個節點從鏈表中移除,傳統能想到的是遍歷鏈表,直到該節點將它上一個指標的下一個指標指向需要洗掉指標的下一個指標,那么這將花費O(n)的時間,時間效率上是達不到題目需求的,那么利用發散思維,我們不妨使用置換的技巧,交換自己和下一個節點的資料,然后讓待洗掉的節點指向自己下一個節點的下一個節點,那么該操作的時間效率就是O(1),當然為了代碼的robust(魯棒性),你需要考慮該節點位于頭尾節點,代碼如下:
void del_list_node(list_node_t** head, list_node_t* node) { if (!head || !node) return; if (node->next) { auto next = node->next; node->value = https://www.cnblogs.com/lianyue/archive/2023/04/11/node->value; node->next = next->next; delete next; next = nullptr; } else if (*head == node) { *head = nullptr; delete node; node = nullptr; } else { auto temp = *head; while (temp && temp->next) { temp = temp->next; } if (!temp || temp != node) return; temp->next = nullptr; delete node; node = nullptr; } }
關于這個演算法的應用,我在閱讀muduo的原始碼里見到了將list中待洗掉節點和尾節點交換后洗掉尾結點的辦法,該技巧就減少了一次遍歷的消耗,提高了程式運行的效率,
關于演算法的相應代碼,大家可以在這個倉庫找到,該庫里也有C++一些新特性的示例(需等待整理后我才會上傳):https://github.com/viticm/cpp_misc
3、設計
這一部分其實都是為了簡單和快速而做的,就是選擇一兩種設計模式,我準備在plain2.0中使用impl手法以及std::function實作Stratege模式,先看看下面的代碼:
#include "plain/basic/config.h" #include <string_view> namespace plain { class PLAIN_API AsyncLogger : noncopyable { public: AsyncLogger(const std::string &name, std::size_t roll_size, int32_t flush_interval = 3); ~AsyncLogger(); public: void append(const std::string_view& log); void start(); void stop(); private: struct Impl; std::unique_ptr<Impl> impl_; }; } // namespace plain
本來是想用logger.h作為示例,不過那個內容較多,害怕大家看了產生排斥,所以這里選擇了簡單的宣告,這里看起來是不是十分清新爽朗,我個人認為是的,這個介面一目了然,多虧了Impl手法隱藏了實作細節,而對于Strategy的實作,其實plain1.1后面的網路相關都已經在做了,不過做的不夠徹底,還保留了一些多型封裝的十字軍問題,后面具體的實作,我會在后面網路設計方面詳細進行說明,如果有興趣的朋友不妨關注后面的文章,
4、兼容
這里的兼容并不只是升級后ABI的兼容,還有就是跨平臺,
plain是一個跨平臺的網路庫,也是有歷史原因的,因為這套原來的實作的客戶端是在windows上運行的,而在微軟的PC上運行的游戲通常稱為端游,所以在plain1.1rc中跨平臺支持linux和windows,而在plain2.0的時候,我打算加入一個新的平臺,那就是MAC,以前自己沒有MAC的時候也不想去折騰這個平臺,但現在有了之后就干脆將它也考慮在內,因此在跨平臺的宏定義檔案include/plain/basic/macros/platform.h定義了三個宏:OS_UNIX(類LINUX平臺)、OS_WIN(微軟PC)、OS_MAC(蘋果PC),
為了實作更好的跨平臺,我解決的辦法就是盡量使用標準庫,就像陳碩說的將臟活、累活留給別人,而這里的別人正是制定標準的組委會(編譯器的作者表示不服),除非要實作一些標準庫里沒有的功能,比如epoll這種具有平臺屬性的介面時才會干一些平臺性相關的處理,說實話這確實是一件累活,其次一點就是盡量減少依賴,比如我就沒有使用標準指定的委員會那群人(聰明人)弄出來的boost庫,boost庫有很多高性能的東西,還有一些即將進入標準庫的東西,的確是有學習的必要,Scott Meyers在《Effective C++》Item 55: 讓自己熟悉Boost強烈推薦熟悉該庫,我在想如果使用了該庫,我何不直接使用asio而需要大張旗鼓地自己實作網路庫?而且多了一個依賴,使用者必然就會需要安裝它,說實話我不太喜歡boost那種龐大的容量,除非有一種包管理工具,實作golang那種我在代碼中申明依賴了后,初始化可以幫助我自動安裝只需要的模塊,這樣的話我覺得使用它是挺好的,
為了實作ABI的兼容,我將要把大部分介面去掉虛函式的設計,改為使用Pimpl(Pointer implementation)的設計方式,對于用戶不可見的部分,將盡量隱藏,還有對于使用者來說,不需要重寫類,而是只需要設定需要關心的處理方法,這個正是使用std::function來實作的Strategy模式,
5、測驗和示例
在plain1.0的時候單元測驗時沒有的,當版本到了1.1的時候才有,沒有單元測驗當時我就只能依賴與一個簡單的示例來找出代碼中的漏洞,每次除錯說實話有點累人,因此在后面寫的模塊,比如因為見到在PHP一個框架laravel的ORM(Object relational mapping)模塊后,我見到了其中有大量的單元測驗,于是我仿照其實作寫了C++相應的模塊放到了plain中,在db目錄下https://github.com/viticm/plain/tree/master/framework/core/src/db,在2.0中這個模塊將被簡化,ORM的實作我會單獨做成一個工程或者放入插件來使用,說到這里有點稍微跑題,我想說的是單元測驗在這里才開始使用,可以看到里面的測驗都是不全的,plain1.1rc的單元測驗:https://github.com/viticm/plain/tree/master/framework/unit_tests/core_test,由于2.0是真正的大改造,因此就可以實作健全的單元測驗用例,這個可以用來稍微檢測一下程式的健壯性,但是我這里并沒有做那種可以復用的自動回歸的測驗,我覺得先實作一種傳統的測驗即可,后續有需要加上有時間可以再增添,那部分不過只是添磚加瓦而已,
至于示例,由于懶的緣故,plain-simple里只有一個簡單的應用,在2.0中我準備實作以下幾種或者更多的服務器的示例:echo、discard、chat等,這樣可以測驗一下plain作為服務器使用時并發的能力,類似libevent中的性能測驗,當然跑出來的資料可以稍微做一個對比,這個可以作為一個讓自己覺得有成就感的事情來做(手動狗頭,炫耀欺騙一下自己),
標準
C++23(2023)
1、偽命題
c++23真的發布了嗎?
確實C++23似憾訓沒有正式發布,但是組委會通過的那些新特性已經凍結,其實可以認為C++23已經算是發布了,新增的頭檔案和特性已經定型,但是這里存在一個問題,那就是主流的編譯器GCC(目前最新版本12.1),以及VS(Microsoft Visual Studio),其實對于C++23很多特性都還沒有支持,也就是編譯器的作者還正在填坑,不過我覺得C++23其實不算那么著急,先把C++20的std::format功能實作了再說,不然許多人都要面對那充滿議論和詬病的C的printf了,也就是說現在C++標準支持的比較充足的是在C++17,應該是C++17所有的功能都是可以使用的,不過C++20的許多功能也能夠使用,
從上面不難看出,我這里說plain使用C++23是一個偽命題,因為C++23特性沒被支持,怎么能夠寫出基于C++23的庫?我將標準定在這里的原因,就是提醒自己plain使用的是基于最新的C++標準,這確實是一種嘗試,我覺得嘗試是有必要的,一個人沒有必要固步自封,嘗試一些新事物是有用的,在這方面,我認為只有主動擁抱未來的人,才會被未來所擁抱,因此如果想要入C++坑的學生和其他朋友,不妨就以最新的標準開始學習,因為十年之后你會發現這些技術就會真的被普及起來了,
2、modules
C++20引入了module,這個已經被其他語言如python、javascript早已引入的功能,這時候來的確實有點遲了,說實話這也是沒有辦法,因此C++語言的設計者一開始就是以C with object oriented(C和面向物件)來設計的,也就是C++是C的衍生,而C呢頭檔案和實作本來就是分開的,如果早點引入這個的話,你就會發現C++和C區別實在太大了,估計這會造成C的忠實擁堆疊有些不適應(C畢竟還是老大哥,許多的高級語言其實作幾乎都是使用C語言), 可惜的是C++20里編譯器并沒有對標準庫提供可以使用,如陳述句import std;就無法使用,其實C++23標準里就是需要讓標準庫得以支持,那么這個就還是需要編譯器的諸位努力,說實話你們是真的辛苦,為了大家的福祉你們就加把勁吧,還有就是module的使用有個痛點,就是模塊之間的相互參考的編譯順序問題,畢竟需要編譯出*.gcm才能夠使用import,考慮到了復雜度,plain暫時不考慮使用module, 由于module特性的吸引力,個人認為以后module還是應當支持,特別對于庫開發者來說這的確是一大利器,雜項
寫在最后
1、園子
不知不覺我在博客園已經有了十多年,說句話十年之間世界發生了很大的變化,特別是隔壁的博客有許多的限制,如你查看的時候需要登錄、復制也需要登錄,雖然我理解那種做法,是為了吸引更多的用戶,但是并不贊同那樣的做法,說實話那樣讓人有點不舒服,但是園子給我感覺十分清爽,雖然我明顯感覺園子確實變得有點冷清,閱讀量有點變少了,但我覺得作為一個純技術分享的地方,這樣的園子是值得讓人稱贊的,不忘初心終得始終,希望園子能夠堅持下去并且越做越好,在園子里我也在一些文章里獲取了不少知識,因此我十分樂意來分享自己的知識,希望這些知識能夠幫助大家,
在許多年前閉源還十分盛行的時候,人們想不到今天開源會如此的大行其道,人類想要發展的話我認為首先就需要有這種分享的精神,要是不懂得分享的話,就等于閉關鎖國,除非你的技術是需要保密的話可以另說,將自己的技術和想法分享出來也能給自己帶來快樂,我個人是如此感覺的,
2、推薦書籍
《C++編程思想》:可以帶你全面了解C++的語言設計的方方面面
《C++ Primer Plus 第五版》:入門可以選擇這本
《Effective C++ 第三版》:高效的C++編程,看看高手如何使用一些技巧(一共55條)來寫作的
《Effective Modern C++》:同上面那本的作者是Scott Meyers,不過是對于C++11/14上做了一些介紹,還有一些特性的總結,同樣的也是有條款性的建議(42條)
《C++20 高級編程 第五版》:去年底出了中文版,現在我看的是英文版的,沒辦法為了熟悉C++20我只能依靠我憋足的英文來閱讀,看久了就發現似乎英語也不成問題(有種文言文看久了成了古人的錯覺),這本書對C++20的新增特性做了明顯的標注
《UNIX 環境高級編程》:這本書如果開發POSIX的網路,你需要了解一下,這個似乎被稱為UNIX編程的”圣經“,對作業系統有詳細地介紹,目前我還在看第一章(等于沒看)
《TPC/IP詳解 卷一》:許多人都推薦這個,如果你想了解網路編程的方方面面,那么這本書應該讀一讀
《演算法導論》:如果希望對演算法有個深入一點的了解,這本書適合不過,我也正準備看這本書
3、網站
github:https://github.com/ 應該算最大的開源社區了吧
cppreference:https://en.cppreference.com/ 如果想要得知C++23新增的特性,這個網站比較全,你也可以在里面查看標準庫的介面和相應的示例
coding-interview-university:https://github.com/jwasham/coding-interview-university 如果你想進入谷歌、微軟等等頂流大廠,這個上面有一個外國朋友分享的心得,怎樣進行系統的學習和面試的方方面面,本人是不行了,就靠各位后浪的朋友
QQ:348477824(plain交流群),這個群目前就如那個頭像一樣十分死寂,但是不妨礙新的朋友加入過來進行一些技術探討,真誠歡迎各位朋友的加入,當然有人能提供一個好的plain圖示就再好不過了
作者:viticm 出處: http://www.cnblogs.com/lianyue/本文著作權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利,如果覺得還有幫助的話,可以點一下右下角的【推薦】,希望能夠持續的為大家帶來好的技術文章!想跟我一起進步么?那就【關注】我吧,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/549746.html
標籤:其他
