• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            陳碩的Blog

            關于 std::set/std::map 的幾個為什么

            陳碩 (chenshuo.com)

            2013-01-20

            std::set/std::map (以下用 std::map 代表) 是常用的關聯式容器,也是 ADT(抽象數據類型)。也就是說,其接口(不是 OO 意義下的 interface)不僅規定了操作的功能,還規定了操作的復雜度(代價/cost)。例如 set::insert(iterator first, iterator last) 在通常情況下是 O(N log N),N 是區間的長度;但是如果 [first, last) 已經排好序(在 key_compare 意義下),那么復雜度將會是 O(N)。

            盡管 C++ 標準沒有強求 std::map 底層的數據結構,但是根據其規定的時間復雜度,現在所有的 STL 實現都采用平衡二叉樹來實現 std::map,而且用的都是紅黑樹。《算法導論(第 2 版)》第 12、13 章介紹了二叉搜索樹和紅黑樹的原理、性質、偽代碼,侯捷先生的《STL 源碼剖析》第 5 章詳細剖析了 SGI STL 的對應實現。本文對 STL 中紅黑樹(rb_tree)的實現問了幾個稍微深入一點的問題,并給出了我的理解。本文剖析的是 G++ 4.7 自帶的這一份 STL 實現及其特定行為,與《STL 源碼剖析》的版本略有區別。為了便于閱讀,文中的變量名和 class 名都略有改寫(例如 _Rb_tree_node 改為 rb_tree_node)。本文不談紅黑樹的平衡算法,在我看來這屬于“旁枝末節”(見陳碩《談一談網絡編程學習經驗》對此的定義),因此也就不關心節點的具體顏色了。

            數據結構回顧

            先回顧一下數據結構教材上講的二叉搜索樹的結構,節點(Node)一般有三個數據成員(left、right、data),樹(Tree)有一到兩個成員(root 和 node_count)。

            用 Python 表示:
            class Node:
                def __init__(self, data):
                    self.left = None
                    self.right = None
                    self.data = data
            
            class Tree:
                def __init__(self):
                    self.root = None
                    self.node_count = 0
            

            而實際上 STL rb_tree 的結構比這個要略微復雜一些,我整理的代碼見 https://gist.github.com/4574621#file-tree-structure-cc

            節點

            Node 有 5 個成員,除了 left、right、data,還有 color 和 parent。

            C++實現,位于bits/stl_tree.h
            /**
             * Non-template code
             **/
            
            enum rb_tree_color { kRed, kBlack };
            
            struct rb_tree_node_base
            {
              rb_tree_color       color_;
              rb_tree_node_base*  parent_;
              rb_tree_node_base*  left_;
              rb_tree_node_base*  right_;
            };
            
            /**
             * template code
             **/
            
            template<typename Value>
            struct rb_tree_node : public rb_tree_node_base
            {
              Value value_field_;
            };
            

            見下圖。

            node

            color 的存在很好理解,紅黑樹每個節點非紅即黑,需要保存其顏色(顏色只需要 1-bit 數據,一種節省內存的優化措施是把顏色嵌入到某個指針的最高位或最低位,Linux 內核里的 rbtree 是嵌入到 parent 的最低位);parent 的存在使得非遞歸遍歷成為可能,后面還將再談到這一點。

            Tree 有更多的成員,它包含一個完整的 rb_tree_node_base(color/parent/left/right),還有 node_count 和 key_compare 這兩個額外的成員。

            這里省略了一些默認模板參數,如 key_compare 和 allocator。
            template<typename Key, typename Value> // key_compare and allocator
            class rb_tree
            {
             public:
              typedef std::less<Key> key_compare;
              typedef rb_tree_iterator<Value> iterator;
             protected:
            
              struct rb_tree_impl // : public node_allocator
              {
                key_compare       key_compare_;
                rb_tree_node_base header_;
                size_t            node_count_;
              };
              rb_tree_impl impl_;
            };
            
            template<typename Key, typename T> // key_compare and allocator
            class map
            {
             public:
              typedef std::pair<const Key, T> value_type;
             private:
              typedef rb_tree<Key, value_type> rep_type;
              rep_type tree_;
            };
            

            見下圖。這是一顆空樹,其中陰影部分是 padding bytes,因為 key_compare 通常是 empty class。(allocator 在哪里?)

            tree

            rb_tree 中的 header 不是 rb_tree_node 類型,而是 rb_tree_node_base,因此 rb_tree 的 size 是 6 * sizeof(void*),與模板類型參數無關。在 32-bit 上是 24 字節,在 64-bit 上是 48 字節,很容易用代碼驗證這一點。另外容易驗證 std::set 和 std::map 的 sizeof() 是一樣的。

            注意 rb_tree 中的 header 不是 root 節點,其 left 和 right 成員也不是指向左右子節點,而是指向最左邊節點(left_most)和最右邊節點(right_most),后面將會介紹原因,是為了滿足時間復雜度。header.parent 指向 root 節點,root.parent 指向 header,header 固定是紅色,root 固定是黑色。在插入一個節點后,數據結構如下圖。

            tree1

            繼續插入兩個節點,假設分別位于 root 的左右兩側,那么得到的數據結構如下圖所示(parent 指針沒有全畫出來,因為其指向很明顯),注意 header.left 指向最左側節點,header.right 指向最右側節點。

            tree3

            迭代器

            rb_tree 的 iterator 的數據結構很簡單,只包含一個 rb_tree_node_base 指針,但是其++/--操作卻不見得簡單(具體實現函數不在頭文件中,而在 libstdc++ 庫文件中)。

            // defined in library, not in header
            rb_tree_node_base* rb_tree_increment(rb_tree_node_base* node);
            // others: decrement, reblance, etc.
            
            template<typename Value>
            struct rb_tree_node : public rb_tree_node_base
            {
              Value value_field_;
            };
            
            template<typename Value>
            struct rb_tree_iterator
            {
              Value& operator*() const
              {
                return static_cast<rb_tree_node<Value>*>(node_)->value_field_;
              }
            
              rb_tree_iterator& operator++()
              {
                node_ = rb_tree_increment(node_);
                return *this;
              }
            
              rb_tree_node_base* node_;
            };
            

            end() 始終指向 header 節點,begin() 指向第一個節點(如果有的話)。因此對于空樹,begin() 和 end() 都指向 header 節點。對于 1 個元素的樹,迭代器的指向如下。

            tree1i

            對于前面 3 個元素的樹,迭代器的指向如下。

            tree3i

            思考,對 std::set<int>::end() 做 dereference 會得到什么?(按標準,這屬于undefined behaviour,不過但試無妨。)

            rb_tree 的 iterator 的遞增遞減操作并不簡單。考慮下面這棵樹,假設迭代器 iter 指向綠色節點3,那么 ++iter 之后它應該指向灰色節點 4,再 ++iter 之后,它應該指向黃色節點 5,這兩步遞增都各走過了 2 個指針。

            tree7

            對于一顆更大的樹(下圖),假設迭代器 iter 指向綠色節點7,那么 ++iter 之后它應該指向灰色節點 8,再 ++iter 之后,它應該指向黃色節點 9,這兩步遞增都各走過了 3 個指針。

            tree15 

            由此可見,rb_tree 的迭代器的每次遞增或遞減不能保證是常數時間,最壞情況下可能是對數時間(即與樹的深度成正比)。那么用 begin()/end() 迭代遍歷一棵樹還是不是 O(N)?換言之,迭代器的遞增或遞減是否是分攤后的(amortized)常數時間?

            另外注意到,當 iter 指向最右邊節點的時候(7 或 15),++iter 應該指向 header 節點,即 end(),這一步是 O(log N)。同理,對 end() 做--,復雜度也是 O(log N),后面會用到這一事實。

            rb_tree 迭代器的遞增遞減操作的實現也不是那么一目了然。要想從頭到尾遍歷一顆二叉樹(前序、中序、后序),教材上給出的辦法是用遞歸(或者用 stack 模擬遞歸,性質一樣),比如:(https://gist.github.com/4574621#file-tree-traversal-py

            Python:
            def printTree(node):
                if node:
                    printTree(node.left)
                    print node.data
                    printTree(node.right)
            

            如果考慮通用性,可以把函數作為參數進去,然后通過回調的方式來訪問每個節點,代碼如下。Java XML 的 SAX 解析方式也是這樣。

            Python:
            def visit(node, func):
                if node:
                    printTree(node.left)
                    func(node.data)
                    printTree(node.right)
            

            要想使用更方便,在調用方用 for 循環就能從頭到尾遍歷 tree,那似乎就不那么容易了。在 Python 這種支持 coroutine 的語言中,可以用 yield 關鍵字配合遞歸來實現,代碼如下,與前面的實現沒有多大區別。

            在 Python 3.3 中還可以用 yield from,這里用 Python 2.x 的寫法。
            def travel(root):
                if root.left:
                    for x in travel(root.left):
                        yield x
                yield root.data
                if root.right:
                    for y in travel(root.right):
                        yield y
            
            調用方:
                for y in travel(root):
                    print y
            

            但是在 C++ 中,要想做到最后這種 StAX 的遍歷方式,那么迭代器的實現就麻煩多了,見《STL 源碼剖析》第 5.2.4 節的詳細分析。這也是 node 中 parent 指針存在的原因,因為遞增操作時,如果當前節點沒有右子節點,就需要先回到父節點,再接著找。

            空間復雜度

            每個 rb_tree_node 直接包含了 value_type,其大小是 4 * sizeof(void*) + sizeof(value_type)。在實際分配內存的時候還要 round up 到 allocator/malloc 的對齊字節數,通常 32-bit 是 8 字節,64-bit 是 16 字節。因此 set<int>每個節點是 24 字節或 48 字節,100 萬個元素的 set<int> 在 x86-64 上將占用 48M 內存。說明用 set<int> 來排序整數是很不明智的行為,無論從時間上還是空間上。

            考慮 malloc 對齊的影響,set<int64_t> 和 set<int32_t> 占用相同的空間,set<int> 和 map<int, int> 占用相同的空間,無論 32-bit 還是 64-bit 均如此。

            幾個為什么

            對于 rb_tree 的數據結構,我們有幾個問題:

            1. 為什么 rb_tree 沒有包含 allocator 成員?

            2. 為什么 iterator 可以 pass-by-value?

            3. 為什么 header 要有 left 成員,并指向 left most 節點?

            4. 為什么 header 要有 right 成員,并指向 right most 節點?

            5. 為什么 header 要有 color 成員,并且固定是紅色?

            6. 為什么要分為 rb_tree_node 和 rb_tree_node_base 兩層結構,引入 node 基類的目的是什么?

            7. 為什么 iterator 的遞增遞減是分攤(amortized)常數時間?

            8. 為什么 muduo 網絡庫的 Poller 要用 std::map<int, Channel*> 來管理文件描述符?

            我認為,在面試的時候能把上面前 7 個問題答得頭頭是道,足以說明對 STL 的 map/set 掌握得很好。下面一一解答。

            為什么 rb_tree 沒有包含 allocator 成員?

            因為默認的 allocator 是 empty class (沒有數據成員,也沒有虛表指針vptr),為了節約 rb_tree 對象的大小,STL 在實現中用了 empty base class optimization。具體做法是 std::map 以 rb_tree 為成員,rb_tree 以 rb_tree_impl 為成員,而 rb_tree_impl 繼承自 allocator,這樣如果 allocator 是 empty class,那么 rb_tree_impl 的大小就跟沒有基類時一樣。其他 STL 容器也使用了相同的優化措施,因此 std::vector 對象是 3 個 words,std::list 對象是 2 個 words。boost 的 compressed_pair 也使用了相同的優化。

            我認為,對于默認的 key_compare,應該也可以實施同樣的優化,這樣每個 rb_tree 只需要 5 個 words,而不是 6 個。

            為什么 iterator 可以 pass-by-value?

            讀過《Effective C++》的想必記得其中有個條款是 Prefer pass-by-reference-to-const to pass-by-value,即對象盡量以 const reference 方式傳參。這個條款同時指出,對于內置類型、STL 迭代器和 STL 仿函數,pass-by-value 也是可以的,一般沒有性能損失。

            在 x86-64 上,對于 rb_tree iterator 這種只有一個 pointer member 且沒有自定義 copy-ctor 的 class,pass-by-value 是可以通過寄存器進行的(例如函數的頭 4 個參數,by-value 返回對象算一個參數),就像傳遞普通 int 和指針那樣。因此實際上可能比 pass-by-const-reference 略快,因為callee 減少了一次 deference。

            同樣的道理,muduo 中的 Date class 和 Timestamp class 也是明確設計來 pass-by-value 的,它們都只有一個 int/long 成員,按值拷貝不比 pass reference 慢。如果不分青紅皂白一律對 object 使用 pass-by-const-reference,固然算不上什么錯誤,不免稍微顯得知其然不知其所以然罷了。

            為什么 header 要有 left 成員,并指向 left most 節點?

            原因很簡單,讓 begin() 函數是 O(1)。假如 header 中只有 parent 沒有 left,begin() 將會是 O(log N),因為要從 root 出發,走 log N 步,才能到達 left most。現在 begin() 只需要用 header.left 構造 iterator 并返回即可。

            為什么 header 要有 right 成員,并指向 right most 節點?

            這個問題不是那么顯而易見。end() 是 O(1),因為直接用 header 的地址構造 iterator 即可,不必使用 right most 節點。在源碼中有這么一段注釋:

            bits/stl_tree.h
              // Red-black tree class, designed for use in implementing STL
              // associative containers (set, multiset, map, and multimap). The
              // insertion and deletion algorithms are based on those in Cormen,
              // Leiserson, and Rivest, Introduction to Algorithms (MIT Press,
              // 1990), except that
              //
              // (1) the header cell is maintained with links not only to the root
              // but also to the leftmost node of the tree, to enable constant
              // time begin(), and to the rightmost node of the tree, to enable
              // linear time performance when used with the generic set algorithms
              // (set_union, etc.)
              //
              // (2) when a node being deleted has two children its successor node
              // is relinked into its place, rather than copied, so that the only
              // iterators invalidated are those referring to the deleted node.
            

            這句話的意思是說,如果按大小順序插入元素,那么將會是線性時間,而非 O(N log N)。即下面這段代碼的運行時間與 N 成正比:

             // 在 end() 按大小順序插入元素
              std::set<int> s;
              const int N = 1000 * 1000
              for (int i = 0; i < N; ++i)
                  s.insert(s.end(), i);
            

            在 rb_tree 的實現中,insert(value) 一個元素通常的復雜度是 O(log N)。不過,insert(hint, value) 除了可以直接傳 value_type,還可以再傳一個 iterator 作為 hint,如果實際的插入點恰好位于 hint 左右,那么分攤后的復雜度是 O(1)。對于這里的情況,既然每次都在 end() 插入,而且插入的元素又都比 *(end()-1) 大,那么 insert() 是 O(1)。在具體實現中,如果 hint 等于 end(),而且 value 比 right most 元素大,那么直接在 right most 的右子節點插入新元素即可。這里 header.right 的意義在于讓我們能在常數時間取到 right most 節點的元素,從而保證 insert() 的復雜度(而不需要從 root 出發走 log N 步到達 right most)。具體的運行時間測試見 https://gist.github.com/4574621#file-tree-bench-cc ,測試結果如下,縱坐標是每個元素的耗時(微秒),其中最上面的紅線是普通 insert(value),下面的藍線和黑線是 insert(end(), value),確實可以大致看出 O(log N) 和 O(1) 關系。具體的證明見《算法導論(第 2 版》第 17 章中的思考題 17-4。

            stl_tree_bench

            但是,根據測試結果,前面引用的那段注釋其實是錯的,std::inserter() 與 set_union() 配合并不能實現 O(N) 復雜度。原因是 std::inserter_iterator 會在每次插入之后做一次 ++iter,而這時 iter 正好指向 right most 節點,其++操作是 O(log N) 復雜度(前面提到過 end() 的遞減是 O(log N),這里反過來也是一樣)。于是把整個算法拖慢到了 O(N log N)。要想 set_union() 是線性復雜度,我們需要自己寫 inserter,見上面代碼中的 end_inserter 和 at_inserter,此處不再贅言。

            為什么 header 要有 color 成員,并且固定是紅色?

            這是一個實現技巧,對 iterator 做遞減時,如果此刻 iterator 指向 end(),那么應該走到 right most 節點。既然 iterator 只有一個數據成員,要想判斷當前是否指向 end(),就只好判斷 (node_->color_ == kRed && node_->parent_->parent_ == node_) 了。

            為什么要分為 rb_tree_node 和 rb_tree_node_base 兩層結構,引入 node 基類的目的是什么?

            這是為了把迭代器的遞增遞減、樹的重新平衡等復雜函數從頭文件移到庫文件中,減少模板引起的代碼膨脹(set<int> 和 set<string> 可以共享這些的 rb_tree 基礎函數),也稍微加快編譯速度。引入 rb_tree_node_base 基類之后,這些操作就可以以基類指針(與模板參數類型無關)為參數,因此函數定義不必放在在頭文件中。這也是我們在頭文件里看不到 iterator 的 ++/-- 的具體實現的原因,它們位于 libstdc++ 的庫文件源碼中。注意這里的基類不是為了 OOP,而純粹是一種實現技巧。

            為什么 iterator 的遞增遞減是分攤(amortized)常數時間?

            嚴格的證明需要用到分攤分析(amortized analysis),一來我不會,二來寫出來也沒多少人看,這里我用一點歸納的辦法來說明這一點。考慮一種特殊情況,對前面圖中的滿二叉樹(perfect binary tree)從頭到尾遍歷,計算迭代器一共走過多少步(即 follow 多少次指針),然后除以節點數 N,就能得到平均每次遞增需要走多少步。既然紅黑樹是平衡的,那么這個數字跟實際的步數也相差不遠。

            對于深度為 1 的滿二叉樹,有 1 個元素,從 begin() 到 end() 需要走 1 步,即從 root 到 header。

            對于深度為 2 的滿二叉樹,有 3 個元素,從 begin() 到 end() 需要走 4 步,即 1->2->3->header,其中從 3 到 header 是兩步

            對于深度為 3 的滿二叉樹,有 7 個元素,從 begin() 到 end() 需要走 11 步,即先遍歷左子樹(4 步)、走 2 步到達右子樹的最左節點,遍歷右子樹(4 步),最后走 1 步到達 end(),4 + 2 + 4 + 1 = 11。

            對于深度為 4 的滿二叉樹,有 15 個元素,從 begin() 到 end() 需要走 26 步。即先遍歷左子樹(11 步)、走 3 步到達右子樹的最左節點,遍歷右子樹(11 步),最后走 1 步到達 end(),11 + 3 + 11 + 1 = 26。

            后面的幾個數字依次是 57、120、247

            對于深度為 n 的滿二叉樹,有 2^n - 1 個元素,從 begin() 到 end() 需要走 f(n) 步。那么 f(n) = 2*f(n-1) + n。

            然后,用遞推關系求出 f(n) = sum(i * 2 ^ (n-i)) = 2^(n+1) - n - 2(這個等式可以用歸納法證明)。即對于深度為 n 的滿二叉樹,從頭到尾遍歷的步數小于 2^(n+1) - 2,而元素個數是 2^n - 1,二者一除,得到平均每個元素需要 2 步。因此可以說 rb_tree 的迭代器的遞增遞減是分攤后的常數時間。

            似乎還有更簡單的證明方法,在從頭到尾遍歷的過程中,每條邊(edge)最多來回各走一遍,一棵樹有 N 個節點,那么有 N-1 條邊,最多走 2*(N-1)+1 步,也就是說平均每個節點需要 2 步,跟上面的結果相同。

            說一點題外話。

            為什么 muduo 網絡庫的 Poller 要用 std::map<int, Channel*> 來管理文件描述符?

            muduo 在正常使用的時候會用 EPollPoller,是對 epoll(4) 的簡單封裝,其中用 std::map<int, Channel*> channels_ 來保存 fd 到 Channel 對象的映射。我這里沒有用數組,而是用 std::map,原因有幾點:

            • epoll_ctl() 是 O(lg N),因為內核中用紅黑樹來管理 fd。因此用戶態用數組管理 fd 并不能降低時間復雜度,反而有可能增加內存使用(用 hash 倒是不錯)。不過考慮系統調用開銷,map vs. vector 的實際速度差別不明顯。(題外話:總是遇到有人說 epoll 是 O(1) 云云,其實 epoll_wait() 是 O(N),N 是活動fd的數目。poll 和 select 也都是 O(N),不過 N 的意義不同。仔細算下來,恐怕只有 epoll_create() 是 O(1) 的。也有人想把 epoll 改為數組,但是被否決了,因為這是開歷史倒車 https://lkml.org/lkml/2008/1/8/205 。)
            • channels_ 只在 Channel 創建和銷毀的時候才會被訪問,其他時候(修改關注的讀寫事件)都是位于 assert() 中,用于 Debug 模式斷言。而 Channel 的創建和銷毀本身就伴隨著 socket 的創建和銷毀,涉及系統調用,channels_ 操作所占的比重極小。因此優化 channels_ 屬于優化 nop,是無意義的。

            (.完.)

            posted @ 2013-01-20 13:26 陳碩 閱讀(8510) | 評論 (1)編輯 收藏

            muduo多機協作網絡編程示例一:單詞計數及排序

            去年我寫了《Muduo 網絡編程示例》系列文章,這些文章已經收入《Linux 多線程服務端編程:使用 muduo C++ 網絡庫》一書。這些文章講的基本都是運行在單機上的網絡程序,每個例子都只有一個程序(第7.13節例外)。我接下來打算繼續寫幾篇文章,談一談分布在多臺機器上、協作發揮作用的網絡編程例子。

            今天先講第一個,單詞計數及排序。單詞計數(word count),顧名思義就是統計一個文本文件里邊每個詞出現了多少次。排序指的是按出現次數從多到少排序,也可以把問題改為“找出出現次數最多的1000個單詞”。

            這個問題有三個層次,第一是輸入文件比較小,能完全放入內存;第二是輸入文件比較大,不能一次性都放入內存;第三是輸入文件分布在多臺機器上,這需要用到網絡編程。

            第一個層次很好解決,幾十行代碼就搞定了。https://gist.github.com/4519962

            第二個層次不難解決,基本思路是分而治之,先hash分塊統計單詞出現次數,將每一塊按出現次數排序,最后歸并。代碼見 https://github.com/chenshuo/recipes/blob/master/puzzle/query_freq.cc ,分析見 http://www.cnblogs.com/baiyanhuang/archive/2012/11/11/2764914.html

            第三個層次也不難,可以當做網絡編程的練習來做。如果有合適的框架,可以輕松解決,因為單詞計數是map reduce的經典范例,對出現次數排序也可以再用一步map reduce搞定(估計需要一個好的 shuffle 函數,簡單hash是不行的)。

            如果用普通網絡編程,一種設計思路如下圖,其中方框代表機器,橢圓代表輸入輸出文件,圓角矩形代表進程。思路跟第二個層次一樣,先hash到多個shard文件(由hasher和receiver負責),再對每個shard文件排序(由sender負責),最后歸并(merger)。

            topk

            注意這種思路適合求top K元素,不適合按出現次數排序全部單詞,因為最終結果收集在一臺機器上。目前這個sender實現的一個限制是,每個shard必須能全部放入內存,因為sender對shard排序是在內存中進行的。如果數據更大,還需要實現單機外部排序。

            圖中hasher和receiver的代碼見muduo示例中的 muduo/examples/wordcount ;sender和merger的代碼見 https://github.com/chenshuo/recipes/tree/master/topk 。注意merger沒有使用muduo,而是采用阻塞網絡編程。有興趣的讀者可以思考其背后的原因。要想發揮 merger 正常的性能,需要修改 /usr/include/boost/asio/basic_socket_streambuf.hpp ,增大緩沖區,即 enum { buffer_size = 8192 };

            這可以看作是map reduce的原始實現,或者說用map reduce的思想手寫了一些原始工具。如果把map reduce比作C語言,這里的幾個程序相當于匯編寫的函數。

            以后我再寫一個按出現次數全排序的例子吧,需要替換這里的sender和merger。

            (.完.)

            posted @ 2013-01-13 04:01 陳碩 閱讀(3595) | 評論 (2)編輯 收藏

            《Linux 多線程服務端編程:使用 muduo C++ 網絡庫》網上書店預訂

            內容簡介

            本書主要講述采用現代 C++ 在 x86-64 Linux 上編寫多線程 TCP 網絡服務程序的主流常規技術,重點講解一種適應性較強的多線程服務器的編程模型,即 one loop per thread。這是在 Linux 下以 native 語言編寫用戶態高性能網絡程序最成熟的模式,掌握之后可順利地開發各類常見的服務端網絡應用程序。本書以 muduo 網絡庫為例,講解這種編程模型的使用方法及注意事項。

            本書的宗旨是貴精不貴多。掌握兩種基本的同步原語就可以滿足各種多線程同步的功能需求,還能寫出更易用的同步設施。掌握一種進程間通信方式和一種多線程網絡編程模型就足以應對日常開發任務,編寫運行于公司內網環境的分布式服務統。

            基本信息

            cover

            出版社:電子工業出版社

            頁數:xvi+600

            定價:人民幣89元

            ISBN:9787121192821

            豆瓣及網上書店預訂

            豆瓣:http://book.douban.com/subject/20471211/
            互動:http://product.china-pub.com/3021861
            亞馬遜:http://www.amazon.cn/dp/B00AYS2KL0
            當當:http://product.dangdang.com/product.aspx?product_id=23162953
            京東:http://book.360buy.com/11163782.html

            試讀樣章

            前言與目錄:https://chenshuo-public.s3.amazonaws.com/pdf/preamble.pdf
            第1章:線程安全的對象生命期管理:https://chenshuo-public.s3.amazonaws.com/pdf/chap1.pdf
            第6章:muduo網絡庫簡介:https://chenshuo-public.s3.amazonaws.com/pdf/chap6.pdf
            附錄:https://chenshuo-public.s3.amazonaws.com/pdf/appendix.pdf
            樣章合集下載:http://vdisk.weibo.com/s/mtupb 共150頁,包括第 11.5 節。

            前言(節選)

            本書主要講述采用現代 C++ 在 x86-64 Linux 上編寫多線程 TCP 網絡服務程序的主流常規技術,這也是我對過去 5 年編寫生產環境下的多線程服務端程序的經驗總結。本書重點講解多線程網絡服務器的一種 IO 模型,即 one loop per thread。這是一種適應性較強的模型,也是 Linux 下以 native 語言編寫用戶態高性能網絡程序最成熟的模式, 掌握之后可順利地開發各類常見的服務端網絡應用程序。本書以 muduo 網絡庫為例,講解這種編程模型的使用方法及注意事項。

            muduo 是一個基于非阻塞 IO 和事件驅動的現代 C++ 網絡庫,原生支持 one loop per thread 這種 IO 模型。muduo 適合開發 Linux 下的面向業務的多線程服務端網絡應用程序,其中“面向業務的網絡編程”的定義見附錄 A。 “現代 C++”指的不是 C++11 新標準,而是 2005 年 TR1 發布之后的 C++ 語言和庫。 與傳統 C++ 相比,現代 C++ 的變化主要有兩方面:資源管理(見第 1 章)與事件回調(見第 449 頁)。

            本書不是多線程編程教程,也不是網絡編程教程,更不是 C++ 教程。讀者應該已經大致讀過《UNIX 環境高級編程》、《UNIX 網絡編程》、《C++ Primer》或與之內容相近的書籍。本書不談 C++11,因為目前(2012 年)主流的 Linux 服務端發行版的 g++ 版本都還停留在 4.4,C++11 進入實用尚需一段時日。

            本書適用的硬件環境是主流 x86-64 服務器,多路多核 CPU、幾十 GB 內存、千兆以太網互聯。除了第 5 章講診斷日志之外,本書不涉及文件 IO。

            本書分為四大部分,第 1 部分“C++ 多線程系統編程”考察多線程下的對象生命期管理、線程同步方法、多線程與 C++ 的結合、高效的多線程日志等。第 2 部分“muduo 網絡庫”介紹使用現成的非阻塞網絡庫編寫網絡應用程序的方法,以及 muduo 的設計與實現。第 3 部分“工程實踐經驗談”介紹分布式系統的工程化開發方法和 C++ 在工程實踐中的功能特性取舍。第 4 部分“附錄”分享網絡編程和 C++ 語言的學習經驗。

            本書的宗旨是貴精不貴多。掌握兩種基本的同步原語就可以滿足各種多線程同步的功能需求,還能寫出更易用的同步設施。掌握一種進程間通信方式和一種多線程網絡編程模型就足以應對日常開發任務,編寫運行于公司內網環境的分布式服務系統。(本書不涉及分布式存儲系統,也不涉及 UDP。)

            術語與排版范例


            本書大量使用英文術語,甚至有少量英文引文。設計模式的名字一律用英文,例如 Observer、Reactor、Singleton。在中文術語不夠突出時,也會使用英文,例如 class、heap、event loop、STL algorithm 等。注意幾個中文 C++ 術語:對象實體(instance) 、函數重載決議(resolution) 、模板具現化(instantiation) 、覆寫(override)虛函數、提領(dereference)指針。本書中的英語可數名詞一般不用復數形式,例如兩個 class,6 個 syscall;但有時會用 (s) 強調中文名詞是復數。fd 是文件描述符(file descriptor)的縮寫。“CPU 數目”一般指的是核(core)的數目。用諸如§11.5 表示本書第 11.5 節,L42 表示上下文中出現的第 42 行代碼。[JCP]、[CC2e] 等是參考文獻,見書末清單。

            代碼

            本書的示例代碼以開源項目的形式發布在 GitHub 上,
            地址是 http://github.com/chenshuo/recipes/http://github.com/chenshuo/muduo/ 。本書配套頁面提供全部源代碼打包下載,正文中出現的類似 recipes/thread 的路徑是壓縮包內的相對路徑,讀者不難找到其對應的 GitHub URL。

            本書假定讀者熟悉 diff -u 命令的輸出格式,用于表示代碼的改動。

            本書正文中出現的代碼有時為了照顧排版而略有改寫,例如改變縮進規則,去掉單行條件語句前后的花括號等。就編程風格而論,應以電子版代碼為準。

            聯系方式


            郵箱:giantchen_at_gmail.com

            主頁:http://chenshuo.com/book (正文和腳注中出現的 URL 可從這里找到。 )

            微博:http://weibo.com/giantchen

            博客:http://blog.csdn.net/Solstice

            代碼:http://github.com/chenshuo

            陳碩

            中國•香港

            posted @ 2013-01-11 12:43 陳碩 閱讀(4865) | 評論 (6)編輯 收藏

            新書預告:《Linux 多線程服務端編程——使用 muduo C++ 網絡庫》

            看完了 W. Richard Stevens 的傳世經典《UNIX 網絡編程》, 能照著例子用 Sockets API 編寫 echo 服務, 卻仍然對稍微復雜一點的網絡編程任務感到無從下手? 書中示例代碼把業務邏輯和 Sockets 調用混在一起,似乎不利于將來擴展?
            • 程序在本機測試正常,放到網絡運行上就經常出現數據收不全的情況?
            • TCP 協議真的有所謂的“粘包問題”嗎?該如何設計打包拆包的協議?又該如何編碼實現才不會掉到陷阱里?
            • 帶外數據(OOB)、信號驅動IO這些高級特性到底有沒有用?
            • 網絡協議格式該怎么設計?發送 C struct 會有對齊方面的問題嗎?對方不用 C/C++ 怎么通信? 將來服務端軟件升級,需要在協議中增加一個字段,現有的客戶端就必須強制升級?
            • 要處理幾千上萬的并發連接,似乎書上講的傳統 fork() 模型應付不過來,該用哪種并發模型呢? 試試 select、poll、epoll 這種 IO 復用模型吧,又感覺非阻塞IO陷阱重重,怎么程序的 CPU 使用率一直是100%?
            • 要不改用現成的 libevent 網絡庫吧,怎么查詢一下數據庫就把其他連接上的請求給耽誤了? 再用個線程池吧。萬一發回響應的時候對方已經斷開連接了怎么辦?會不會串話?
            • 讀過《UNIX 環境高級編程》,想用多線程來發揮多核 CPU 的效率, 但對程序該用哪種多線程模型感到一頭霧水? 有沒有值得推薦的適用面廣的多線程 IO 模型? 互斥器、條件變量、讀寫鎖、信號量這些底層同步原語哪些該用哪些不該用? 有沒有更高級的同步設施能簡化開發? 《UNIX 網絡編程(第二卷)》介紹的那些琳瑯滿目的IPC機制到底用哪個才能兼顧開發效率與可伸縮性?
            網絡編程和多線程編程的基礎打得差不多,開始實際做項目了,更多問題撲面而來:
            • 網上聽人說服務端開發要做到 7x24 運行,為了防止內存碎片連動態內存分配都不能用, 那豈不是連 C++ STL 也一并禁用了?硬件的可靠性高到值得去這么做嗎?
            • 傳聞服務端開發主要通過日志來查錯,那么日志里該寫些什么?日志是寫給誰看的?怎樣寫日志才不會影響性能?
            • 分布式系統跟單機多進程到底有什么本質區別?心跳協議為什么是必須的,該如何實現?
            • C++ 的大型工程該如何管理?庫的接口如何設計才能保證升級的時候不破壞二進制兼容性?

            這本《Linux 多線程服務端編程》中,作者憑借多年的工程實踐經驗試圖解答以上疑問。當然,內容還遠不止這些……


            本書配套頁面: http://chenshuo.com/book ,將不定期更新。

            posted @ 2012-09-21 07:20 陳碩 閱讀(3700) | 評論 (9)編輯 收藏

            從《C++ Primer 第四版》入手學習 C++

            《C++ Primer 第4版 評注版》即將出版,這是序言。PDF 版見:

            https://github.com/downloads/chenshuo/documents/LearnCpp.pdf

            從《C++ Primer 第四版》入手學習 C++

            為什么要學習C++?

            2009 年本書作者 Stan Lippman 先生來華參加上海祝成科技舉辦的C++技術大會,他表示人們現在還用C++的惟一理由是其性能。相比之下,Java/C#/Python等語言更加易學易用并且開發工具豐富,它們的開發效率都高于C++。但C++目前仍然是運行最快的語言[1],如果你的應用領域確實在乎這個性能,那么 C++ 是不二之選。

            這里略舉幾個例子[2]。對于手持設備而言,提高運行效率意味著完成相同的任務需要更少的電能,從而延長設備的操作時間,增強用戶體驗。對于嵌入式[3]設備而言,提高運行效率意味著:實現相同的功能可以選用較低檔的處理器和較少的存儲器,降低單個設備的成本;如果設備銷量大到一定的規模,可以彌補C++開發的成本。對于分布式系統而言,提高10%的性能就意味著節約10%的機器和能源。如果系統大到一定的規模(數千臺服務器),值得用程序員的時間去換取機器的時間和數量,可以降低總體成本。另外,對于某些延遲敏感的應用(游戲[4],金融交易),通常不能容忍垃圾收集(GC)帶來的不確定延時,而C++可以自動并精確地控制對象銷毀和內存釋放時機[5]。我曾經不止一次見到,出于性能原因,用C++重寫現有的Java或C#程序。

            C++之父Bjarne Stroustrup把C++定位于偏重系統編程(system programming) [6]的通用程序設計語言,開發信息基礎架構(infrastructure)是C++的重要用途之一[7]。Herb Sutter總結道[8],C++注重運行效率(efficiency)、靈活性(flexibility)[9]和抽象能力(abstraction),并為此付出了生產力(productivity)方面的代價[10]。用本書作者的話來說,C++ is about efficient programming with abstractions。C++的核心價值在于能寫出“運行效率不打折扣的抽象[11]”。

            要想發揮C++的性能優勢,程序員需要對語言本身及各種操作的代價有深入的了解[12],特別要避免不必要的對象創建[13]。例如下面這個函數如果漏寫了&,功能還是正確的,但性能將會大打折扣。編譯器和單元測試都無法幫我們查出此類錯誤,程序員自己在編碼時須得小心在意。

            inline int find_longest(const std::vector<std::string>& words)
            {
              // std::max_element(words.begin(), words.end(), LengthCompare());
            }

            在現代CPU體系結構下,C++ 的性能優勢很大程度上得益于對內存布局(memory layout )的精確控制,從而優化內存訪問的局部性[14](locality of reference)并充分利用內存階層(memory hierarchy)提速[15],這一點優勢在近期內不會被基于GC的語言趕上[16]

            C++的協作性不如C、Java、Python,開源項目也比這幾個語言少得多,因此在TIOBE語言流行榜中節節下滑。但是據我所知,很多企業內部使用C++來構建自己的分布式系統基礎架構,并且有替換Java開源實現的趨勢。

            學習C++只需要讀一本大部頭

            C++不是特性(features)最豐富的語言,卻是最復雜的語言,諸多語言特性相互干擾,使其復雜度成倍增加。鑒于其學習難度和知識點之間的關聯性,恐怕不能用“粗粗看看語法,就擼起袖子開干,邊查Google邊學習[17]”這種方式來學習C++,那樣很容易掉到陷阱里或養成壞的編程習慣。如果想成為專業C++開發者,全面而深入地了解這門復雜語言及其標準庫,你需要一本系統而權威的書,這樣的書必定會是一本八九百頁的大部頭[18]

            兼具系統性和權威性[19]的C++教材有兩本,C++之父Bjarne Stroustrup的代表作《The C++ Programming Language》和Stan Lippman的這本《C++ Primer》。侯捷先生評價道:“泰山北斗已現,又何必案牘勞形于墨瀚書海之中!這兩本書都從C++盤古開天以來,一路改版,斬將擎旗,追奔逐北,成就一生榮光[20]。”

            從實用的角度,這兩本書讀一本即可,因為它們覆蓋的C++知識點相差無幾。就我個人的閱讀體驗而言,Primer更易讀一些,我十年前深入學習C++正是用的《C++ Primer第三版》。這次借評注的機會仔細閱讀了《C++ Primer第四版》,感覺像在讀一本完全不同的新書。第四版內容組織及文字表達比第三版進步很多[21],第三版可謂“事無巨細、面面俱到”,第四版重點突出詳略得當,甚至篇幅也縮短了,這多半歸功于新加盟的作者Barbara Moo。

            《C++ Primer 第四版》講什么?適合誰讀?

            這是一本C++語言的教程,不是編程教程。本書不講八皇后問題、Huffman編碼、漢諾塔、約瑟夫環、大整數運算等等經典編程例題,本書的例子和習題往往都跟C++本身直接相關。本書的主要內容是精解C++語法(syntax)與語意(semantics),并介紹C++標準庫的大部分內容(含STL)。“這本書在全世界C++教學領域的突出和重要,已經無須我再贅言[22]。”

            本書適合C++語言的初學者,但不適合編程初學者。換言之,這本書可以是你的第一本C++ 書,但恐怕不能作為第一本編程書。如果你不知道什么是變量、賦值、分支、條件、循環、函數,你需要一本更加初級的書[23],本書第1章可用作自測題。

            如果你已經學過一門編程語言,并且打算成為專業C++開發者,從《C++ Primer 第四版》入手不會讓你走彎路。值得特別說明的是,學習本書不需要事先具備C語言知識。相反,這本書教你編寫真正的C++程序,而不是披著C++ 外衣的C程序。

            《C++ Primer 第四版》的定位是語言教材,不是語言規格書,它并沒有面面俱到地談到C++的每一個角落,而是重點講解C++程序員日常工作中真正有用的、必須掌握的語言設施和標準庫[24]。本書的作者一點也不炫耀自己的知識和技巧,雖然他們有十足的資本[25]。這本書用語非常嚴謹(沒有那些似是而非的比喻),用詞平和,講解細致,讀起來并不枯燥。特別是如果你已經有一定的編程經驗,在閱讀時不妨思考如何用C++來更好地完成以往的編程任務。

            盡管本書篇幅近900頁,其內容還是十分緊湊,很多地方讀一個句子就值得寫一小段代碼去驗證。為了節省篇幅,本書經常修改前文代碼中的一兩行,來說明新的知識點,值得把每一行代碼敲到機器中去驗證。習題當然也不能輕易放過。

            《C++ Primer 第四版》體現了現代C++教學與編程理念:在現成的高質量類庫上構建自己的程序,而不是什么都從頭自己寫。這本書在第三章介紹了string和vector這兩個常用的類,立刻就能寫出很多有用的程序。但作者不是一次性把string的上百個成員函數一一列舉,而是有選擇地講解了最常用的那幾個函數。

            《C++ Primer 第四版》的代碼示例質量很高,不是那種隨手寫的玩具代碼。第10.4.2節實現了帶禁用詞的單詞計數,第10.6利用標準庫容器簡潔地實現了基于倒排索引思路的文本檢索,第15.9節又用面向對象方法擴充了文本檢索的功能,支持布爾查詢。值得一提的是,這本書講解繼承和多態時舉的例子符合Liskov替換原則,是正宗的面向對象。相反,某些教材以復用基類代碼為目的,常以“人、學生、老師、教授”或“雇員、經理、銷售、合同工”為例,這是誤用了面向對象的“復用”。

            《C++ Primer 第四版》出版于2005年,遵循2003年的C++語言標準[26]。C++新標準已于2011年定案(稱為C++11),本書不涉及TR1[27]和C++11,這并不意味著這本書過時了[28]。相反,這本書里沉淀的都是當前廣泛使用的C++編程實踐,學習它可謂正當時。評注版也不會越俎代庖地介紹這些新內容,但是會指出哪些語言設施已在新標準中廢棄,避免讀者浪費精力。

            《C++ Primer 第四版》是平臺中立的,并不針對特定的編譯器或操作系統。目前最主流的C++編譯器有兩個, GNU G++和微軟Visual C++。實際上,這兩個編譯器陣營基本上“模塑[29]”了C++語言的行為。理論上講, C++語言的行為是由C++標準規定的。但是 C++不像其他很多語言有“官方參考實現[30]”,因此C++的行為實際上是由語言標準、幾大主流編譯器、現有不計其數的C++產品代碼共同確定的,三者相互制約。C++編譯器不光要盡可能符合標準,同時也要遵循目標平臺的成文或不成文規范和約定,例如高效地利用硬件資源、兼容操作系統提供的C語言接口等等。在C++標準沒有明文規定的地方,C++編譯器也不能隨心所欲自由發揮。學習C++的要點之一是明白哪些行為是由標準保證的,哪些是由實現(軟硬件平臺和編譯器)保證的[31],哪些是編譯器自由實現,沒有保證的;換言之,明白哪些程序行為是可依賴的。從學習的角度,我建議如果有條件不妨兩個編譯器都用[32],相互比照,避免把編譯器和平臺特定的行為誤解為C++語言規定的行為。盡管不是每個人都需要寫跨平臺的代碼,但也大可不必自我限定在編譯器的某個特定版本,畢竟編譯器是會升級的。

            本著“練從難處練,用從易處用”的精神,我建議在命令行下編譯運行本書的示例代碼,并盡量少用調試器。另外,值得了解C++的編譯鏈接模型[33],這樣才能不被實際開發中遇到的編譯錯誤或鏈接錯誤絆住手腳。(C++不像現代語言那樣有完善的模塊(module)和包(package)設施,它從C語言繼承了頭文件、源文件、庫文件等古老的模塊化機制,這套機制相對較為脆弱,需要花一定時間學習規范的做法,避免誤用。)

            就學習C++語言本身而言,我認為有幾個練習非常值得一做。這不是“重復發明輪子”,而是必要的編程練習,幫助你熟悉掌握這門語言。是寫一個復數類或者大整數類[34],實現基本的運算,熟悉封裝與數據抽象。是寫一個字符串類,熟悉內存管理與拷貝控制。是寫一個簡化的vector<T>類模板,熟悉基本的模板編程,你的這個vector應該能放入int和string等元素類型。是寫一個表達式計算器,實現一個節點類的繼承體系(右圖),體會面向對象編程。前三個練習是寫獨立的值語義的類,第四個練習是對象語義,同時要考慮類與類之間的關系。

            表達式計算器能把四則運算式3+2*4解析為左圖的表達式樹[35],對根節點調用calculate()虛函數就能算出表達式的值。做完之后還可以再擴充功能,比如支持三角函數和變量。


            在寫完面向對象版的表達式樹之后,還可以略微嘗試泛型編程。比如把類的繼承體系簡化為下圖,然后用BinaryNode<std::plus<double> >和BinaryNode<std:: multiplies<double> >來具現化BinaryNode<T>類模板,通過控制模板參數的類型來實現不同的運算。


            在表達式樹這個例子中,節點對象是動態創建的,值得思考:如何才能安全地、不重不漏地釋放內存。本書第15.8節的Handle可供參考。(C++的面向對象基礎設施相對于現代的語言而言顯得很簡陋,現在C++也不再以“支持面向對象”為賣點了。)

            C++難學嗎?“能夠靠讀書看文章讀代碼做練習學會的東西沒什么門檻,智力正常的人只要愿意花功夫,都不難達到(不錯)的程度。[36]” C++好書很多,不過優秀的C++開源代碼很少,而且風格迥異[37]。我這里按個人口味和經驗列幾個供讀者參考閱讀:Google的protobuf、leveldb、PCRE的C++ 封裝,我自己寫的muduo網絡庫。這些代碼都不長,功能明確,閱讀難度不大。如果有時間,還可以讀一讀Chromium中的基礎庫源碼。在讀Google開源的C++代碼時要連注釋一起細讀。我不建議一開始就讀STL或Boost的源碼,因為編寫通用C++模板庫和編寫C++應用程序的知識體系相差很大。 另外可以考慮讀一些優秀的C或Java開源項目,并思考是否可以用C++更好地實現或封裝之(特別是資源管理方面能否避免手動清理)。

            繼續前進

            我能夠隨手列出十幾本C++好書,但是從實用角度出發,這里只舉兩三本必讀的書。讀過《C++ Primer》和這幾本書之后,想必讀者已能自行識別C++圖書的優劣,可以根據項目需要加以鉆研。

            第一本是《Effective C++ 第三版》[38]。學習語法是一回事,高效地運用這門語言是另一回事。C++是一個遍布陷阱的語言,吸取專家經驗尤為重要,既能快速提高眼界,又能避免重蹈覆轍。《C++ Primer》加上這本書包含的C++知識足以應付日常應用程序開發。

            我假定讀者一定會閱讀這本書,因此在評注中不引用《Effective C++ 第三版》的任何章節。

            《Effective C++ 第三版》的內容也反映了C++用法的進步。第二版建議“總是讓基類擁有虛析構函數”,第三版改為“為多態基類聲明虛析構函數”。因為在C++中,“繼承”不光只有面向對象這一種用途,即C++的繼承不一定是為了覆寫(override)基類的虛函數。第二版花了很多筆墨介紹淺拷貝與深拷貝,以及對指針成員變量的處理[39]。第三版則提議,對于多數class而言,要么直接禁用拷貝構造函數和賦值操作符,要么通過選用合適的成員變量類型[40],使得編譯器默認生成的這兩個成員函數就能正常工作。

            什么是C++編程中最重要的編程技法(idiom)?我認為是“用對象來管理資源”,即RAII。資源包括動態分配的內存[41],也包括打開的文件、TCP網絡連接、數據庫連接、互斥鎖等等。借助RAII,我們可以把資源管理和對象生命期管理等同起來,而對象生命期管理在現代C++里根本不是困難(見注5),只需要花幾天時間熟悉幾個智能指針[42]的基本用法即可。學會了這三招兩式,現代的C++程序中可以完全不寫delete,也不必為指針或內存錯誤操心。現代C++程序里出現資源和內存泄漏的惟一可能是循環引用,一旦發現,也很容易修正設計和代碼。這方面的詳細內容請參考《Effective C++ 第三版》第3章資源管理。

            C++是目前惟一能實現自動化資源管理的語言,C語言完全靠手工釋放資源,而其他基于垃圾收集的語言只能自動清理內存,而不能自動清理其他資源[43](網絡連接,數據庫連接等等)。

            除了智能指針,TR1中的bind/function也十分值得投入精力去學一學[44]。讓你從一個嶄新的視角,重新審視類與類之間的關系。Stephan T. Lavavej有一套PPT介紹TR1的這幾個主要部件[45]

            第二本書,如果讀者還是在校學生,已經學過數據結構課程[46],可以考慮讀一讀《泛型編程與STL》[47];如果已經工作,學完《C++ Primer》立刻就要參加C++項目開發,那么我推薦閱讀《C++編程規范》[48]

            泛型編程有一套自己的術語,如concept、model、refinement等等,理解這套術語才能閱讀泛型程序庫的文檔。即便不掌握泛型編程作為一種程序設計方法,也要掌握C++中以泛型思維設計出來的標準容器庫和算法庫(STL)。坊間面向對象的書琳瑯滿目,學習機會也很多,而泛型編程只有這么一本,讀之可以開拓視野,并且加深對STL的理解(特別是迭代器[49])和應用。

            C++模板是一種強大的抽象手段,我不贊同每個人都把精力花在鉆研艱深的模板語法和技巧。從實用角度,能在應用程序中寫寫簡單的函數模板和類模板即可(以type traits為限),不是每個人都要去寫公用的模板庫。

            由于C++語言過于龐大復雜,我見過的開發團隊都對其剪裁使用[50]。往往團隊越大,項目成立時間越早,剪裁得越厲害,也越接近C。制定一份好的編程規范相當不容易。規范定得太緊(比如定為團隊成員知識能力的交集),程序員束手束腳,限制了生產力,對程序員個人發展也不利[51]。規范定得太松(定為團隊成員知識能力的并集),項目內代碼風格迥異,學習交流協作成本上升,恐怕對生產力也不利。由兩位頂級專家合寫的《C++編程規范》一書可謂是現代C++編程規范的范本。

            《C++編程規范》同時也是專家經驗一類的書,這本書篇幅比《Effective C++ 第三版》短小,條款數目卻多了近一倍,可謂言簡意賅。有的條款看了就明白,照做即可:

            ·         第1條,以高警告級別編譯代碼,確保編譯器無警告。

            ·         第31條,避免寫出依賴于函數實參求值順序的代碼。C++操作符的優先級、結合性與表達式的求值順序是無關的。裘宗燕老師寫的《C/C++ 語言中表達式的求值》[52]一文對此有明確的說明。

            ·         第35條,避免繼承“并非設計作為基類使用”的class。

            ·         第43條,明智地使用pimpl。這是編寫C++動態鏈接庫的必備手法,可以最大限度地提高二進制兼容性。

            ·         第56條,盡量提供不會失敗的swap()函數。有了swap()函數,我們在自定義賦值操作符時就不必檢查自賦值了。

            ·         第59條,不要在頭文件中或#include之前寫using。

            ·         第73條,以by value方式拋出異常,以by reference方式捕捉異常。

            ·         第76條,優先考慮vector,其次再選擇適當的容器。

            ·         第79條,容器內只可存放value和smart pointer。

            有的條款則需要相當的設計與編碼經驗才能解其中三昧:

            ·         第5條,為每個物體(entity)分配一個內聚任務。

            ·         第6條,正確性、簡單性、清晰性居首。

            ·         第8、9條,不要過早優化;不要過早劣化。

            ·         第22條,將依賴關系最小化。避免循環依賴。

            ·         第32條,搞清楚你寫的是哪一種class。明白value class、base class、trait class、policy class、exception class各有其作用,寫法也不盡相同。

            ·         第33條,盡可能寫小型class,避免寫出大怪獸。

            ·         第37條,public繼承意味著可替換性。繼承非為復用,乃為被復用。

            ·         第57條,將class類型及其非成員函數接口放入同一個namespace。

            值得一提的是,《C++編程規范》是出發點,但不是一份終極規范。例如Google的C++編程規范[53]和LLVM編程規范[54]都明確禁用異常,這跟這本書的推薦做法正好相反。

            評注版使用說明

            評注版采用大開本印刷,在保留原書板式的前提下,對原書進行了重新分頁,評注的文字與正文左右分欄并列排版。本書已依據原書2010年第11次印刷的版本進行了全面修訂。為了節省篇幅,原書每章末尾的小結和術語表還有書末的索引都沒有印在評注版中,而是做成PDF供讀者下載,這也方便讀者檢索。評注的目的是幫助初次學習C++的讀者快速深入掌握這門語言的核心知識,澄清一些概念、比較與其他語言的不同、補充實踐中的注意事項等等。評注的內容約占全書篇幅的15%,大致比例是三分評、七分注,并有一些補白的內容[55]。如果讀者拿不定主意是否購買,可以先翻一翻第5章。我在評注中不談C++11[56],但會略微涉及TR1,因為TR1已經投入實用。

            為了不打斷讀者閱讀的思路,評注中不會給URL鏈接,評注中偶爾會引用《C++編程規范》的條款,以[CCS]標明,這些條款的標題已在前文列出。另外評注中出現的soXXXXXX表示http://stackoverflow.com/questions/XXXXXX 網址。

            網上資源

            代碼下載:http://www.informit.com/store/product.aspx?isbn=0201721481
            豆瓣頁面:http://book.douban.com/subject/10944985/
            術語表與索引PDF下載:http://chenshuo.com/cp4/
            本文電子版發布于https://github.com/chenshuo/documents/downloads/LearnCpp.pdf,方便讀者訪問腳注中的網站。

            我的聯系方式:giantchen_AT_gmail.com                    http://weibo.com/giantchen

             

            陳碩

            2012年3月

            中國·香港

             

            評注者簡介 :

            陳碩,北京師范大學碩士,擅長 C++ 多線程網絡編程和實時分布式系統架構。現任職于香港某跨國金融公司 IT 部門,從事實時外匯交易系統開發。編寫了開源 C++ 網絡庫 muduo; 參與翻譯了《代碼大全(第二版)》和《C++ 編程規范(繁體版)》;2009 年在上海 C++ 技術大會做技術演講《當析構函數遇到多線程》,同時擔任 Stanley Lippman 先生的口譯員;2010 年在珠三角技術沙龍做技術演講《分布式系統的工程化開發方法》;2012年在“我們的開源項目”深圳站做《Muduo 網絡庫:現代非阻塞C++網絡編程》演講。

             



            [1] 見編程語言性能對比網站 http://shootout.alioth.debian.org/ 和Google 員工寫的語言性能對比論文

               https://days2011.scala-lang.org/sites/days2011/files/ws3-1-Hundt.pdf

            [2] C++之父Bjarne Stroustrup維護的C++用戶列表:http://www2.research.att.com/~bs/applications.html

            [3] 初窺C++在嵌入式系統中的應用,請見http://aristeia.com/TalkNotes/MISRA_Day_2010.pdf

            [4] Milo Yip在《C++強大背后》提到大部分游戲引擎(如Unreal/Source)及中間件(如Havok/FMOD)是C++實現的。http://www.cnblogs.com/miloyip/archive/2010/09/17/behind_cplusplus.html

            [5] 孟巖《垃圾收集機制批判》:C++利用智能指針達成的效果是,一旦某對象不再被引用,系統刻不容緩,立刻回收內存。這通常發生在關鍵任務完成后的清理(clean up)時期,不會影響關鍵任務的實時性,同時,內存里所有的對象都是有用的,絕對沒有垃圾空占內存。http://blog.csdn.net/myan/article/details/1906

            [6] 有人半開玩笑地說“所謂系統編程,就是那些CPU時間比程序員的時間更重要的工作。”

            [7] 《Software Development for Infrastructure》 http://www2.research.att.com/~bs/Computer-Jan12.pdf

            [8] Herb Sutter在C++ and Beyond 2011會議上的開場演講《Why C++?》
               http://channel9.msdn.com/posts/C-and-Beyond-2011-Herb-Sutter-Why-C

            [9] 這里的靈活性指的是編譯器不阻止你干你想干的事情,比如為了追求運行效率而實現即時編譯(just-in-time compilation)。

            [10] 我曾向Stan Lippman介紹目前我在Linux下的工作環境(編輯器、編譯器、調試器),他表示這跟他在1970年代的工作環境相差無幾,可見C++在開發工具方面的落后。另外C++的編譯運行調試周期也比現代的語言長,這多少影響了工作效率。

            [11] 可參考Ulrich Drepper在《Stop Underutilizing Your Computer》中舉的SIMD例子。
               http://www.redhat.com/f/pdf/summit/udrepper_945_stop_underutilizing.pdf

            [12]《Technical Report on C++ Performance》 http://www.open-std.org/jtc1/sc22/wg21/docs/18015.html

            [13] 可參考Scott Meyers的《Effective C++ in an Embedded Environment》

               http://www.artima.com/shop/effective_cpp_in_an_embedded_environment

            [14] 我們知道std::list的任一位置插入是O(1)操作,而std::vector的任一位置插入是O(N)操作,但由于std::vector的元素布局更加緊湊(compact),很多時候std::vector的隨機插入性能甚至會高于std::list。見http://ecn.channel9.msdn.com/events/GoingNative12/GN12Cpp11Style.pdf 這也佐證std::vector是首選容器。

            [15] 可參考Scott Meyers的技術報告《CPU Caches and Why You Care》和任何一本現代的計算機體系結構教材 http://aristeia.com/TalkNotes/ACCU2011_CPUCaches.pdf

            [16] Bjarne Stroustrup有一篇論文《Abstraction and the C++ machine model》對比了C++和Java的對象內存布局。 http://www2.research.att.com/~bs/abstraction-and-machine.pdf

            [17] 語出孟巖《快速掌握一個語言最常用的50%》 http://blog.csdn.net/myan/article/details/3144661

            [18] 同樣篇幅的Java/C#/Python教材可以從語言、標準庫一路講到多線程、網絡編程、圖形編程。

            [19] “權威”的意思是說你不用擔心作者講錯了,能達到這個水準的C++圖書作者全世界也屈指可數。

            [20] 侯捷《大道之行也——C++ Primer 3/e譯序》 http://jjhou.boolan.com/cpp-primer-foreword.pdf

            [21] Bjarne Stroustrup在《Programming--- Principles and Practice Using C++》的參考文獻中引用了本書,并特別注明 use only the 4th edition.

            [22] 侯捷《C++ Primer 4/e譯序》

            [23] 如果沒有時間精讀注21中提到的那本大部頭,短小精干的《Accelerated C++》亦是上佳之選。另外如果想從C語言入手,我推薦裘宗燕老師的《從問題到程序:程序設計與C語言引論(第2版)》

            [24] 本書把iostream的格式化輸出放到附錄,徹底不談locale/facet,可謂匠心獨運。

            [25] Stan Lippman曾說:Virtual base class support wanders off into the Byzantine... The material is simply too esoteric to warrant discussion...

            [26] 基本等同于1998年的初版C++標準,修正了編譯器作者關心的一些問題,與普通程序員基本無關。

            [27] TR1是2005年C++標準庫的一次擴充,增加了智能指針、bind/function、哈希表、正則表達式等。

            [28]作者正在編寫《C++ Primer 第五版》,會包含C++11的內容。

            [29] G++統治了Linux平臺,并且能用在很多Unix平臺上;Visual C++統治了Windows平臺。其他C++編譯器的行為通常要向它們靠攏,例如Intel C++在Linux上要兼容G++,而在Windows上要兼容Visual C++。

            [30] 曾經是Cfront,本書作者正是其主要開發者。http://www.softwarepreservation.org/projects/c_plus_plus

            [31] 包括C++標準有規定,但編譯器拒絕遵循的。http://stackoverflow.com/questions/3931312/value-initialization-and-non-pod-types

            [32] G++ 是免費的,可使用較新的4.x版,最好32-bit和64-bit一起用,因為服務端已經普及64位。微軟也有免費的編譯器,可考慮Visual C++ 2010 Express,建議不要用老掉牙的Visual C++ 6.0作為學習平臺。

            [33] 可參考陳碩寫的《C++工程實踐經驗談》中的“C++編譯模型精要”一節。

            [34] 大整數類可以以std::vector<int>為成員變量,避免手動資源管理。

            [35] “解析”可以用數據結構課程介紹的逆波蘭表達式方法,也可以用編譯原理中介紹的遞歸下降法,還可以用專門的Packrat算法。可參考http://www.relisoft.com/book/lang/poly/3tree.html

            [36]  孟巖《技術路線的選擇重要但不具有決定性》 http://blog.csdn.net/myan/article/details/3247071

            [37] 從代碼風格上往往能判斷項目成型的時代。

            [38] Scott Meyers著,侯捷譯,電子工業出版社。

            [39] Andrew Koenig的《Teaching C++ Badly: Introduce Constructors and Destructors at the Same Time》

            http://drdobbs.com/blogs/cpp/229500116

            [40] 能自動管理資源的string、vector、shared_ptr等等,這樣多數class連析構函數都不必寫。

            [41] “分配內存”包括在堆(heap)上創建對象。

            [42] 包括TR1中的shared_ptr、weak_ptr,還有更簡單的boost::scoped_ptr。

            [43] Java 7有try-with-resources語句,Python有with語句,C#有using語句,可以自動清理棧上的資源,但對生命期大于局部作用域的資源無能為力,需要程序員手工管理。

            [44] 孟巖《function/bind的救贖(上)》http://blog.csdn.net/myan/article/details/5928531

            [45] http://blogs.msdn.com/b/vcblog/archive/2008/02/22/tr1-slide-decks.aspx

            [46] 最好再學一點基礎的離散數學。

            [47] Matthew Austern著,侯捷譯,中國電力出版社。

            [48] Herb Sutter等著,劉基誠譯,人民郵電出版社。(這本書繁體版由侯捷先生和我翻譯。)

            [49] 侯捷先生的《芝麻開門:從Iterator談起》 http://jjhou.boolan.com/programmer-3-traits.pdf

            [50] 孟巖《編程語言的層次觀點——兼談C++的剪裁方案》 http://blog.csdn.net/myan/article/details/1920

            [51] 一個人通常不會在一個團隊工作一輩子,其他團隊可能有不同的C++剪裁使用方式,程序員要有“一桶水”的本事,才能應付不同形狀大小的水碗。

            [52] http://www.math.pku.edu.cn/teachers/qiuzy/technotes/expression2009.pdf

            [53] http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions

            [54] http://llvm.org/docs/CodingStandards.html#ci_rtti_exceptions

            [55] 第10章繪制了數據結構示意圖,第11章補充lower_bound 和 upper_bound的示例。

            [56] 從Scott Meyers的講義可以快速學習C++11 http://www.artima.com/shop/overview_of_the_new_cpp

            posted @ 2012-07-06 09:00 陳碩 閱讀(2734) | 評論 (3)編輯 收藏

            《Muduo 網絡庫:現代非阻塞C++網絡編程》演講

            2012年6月30日下午將在深圳做《Muduo 網絡庫:現代非阻塞C++網絡編程》演講,

            這是PPT:

            http://www.slideshare.net/chenshuo/muduo-network-library

            演講視頻:

            http://v.youku.com/v_show/id_XNDIyNDc5MDMy.html

            http://youtu.be/YDnCAs894Bg 


            活動介紹:

            http://ouropensource.51qiangzuo.com/

            posted @ 2012-07-01 23:55 陳碩 閱讀(5423) | 評論 (29)編輯 收藏

            發布一個適合服務端C++程序的高效日志庫

            PPT 見 http://www.slideshare.net/chenshuo/efficient-logging-in-multithreaded-c-server/

            2012年6月30日在深圳的簡短演講:

            http://v.youku.com/v_show/id_XNDIyMjUwMDYw.html

            http://www.youtube.com/watch?v=KM_eQ6uRYdU

             


            代碼位于 muduo 網絡庫中的 muduo/base

            https://github.com/chenshuo/muduo

            muduo 0.5.0 也包含了這個日志庫 http://code.google.com/p/muduo/

            posted @ 2012-06-06 21:27 陳碩 閱讀(8583) | 評論 (5)編輯 收藏

            C++ 工程實踐(12):C++ 編譯鏈接模型精要

            《C++ 工程實踐》新增第15節“C++ 編譯鏈接模型精要”  


            PDF 下載: https://github.com/downloads/chenshuo/documents/CppPractice.pdf  

            posted @ 2012-04-20 08:18 陳碩 閱讀(2496) | 評論 (1)編輯 收藏

            C++ 工程實踐(11):用 STL algorithm 秒殺幾道算法面試題

            《C++ 工程實踐》新增第14節“用 STL algorithm 秒殺幾道算法面試題” 


            PDF 下載: https://github.com/downloads/chenshuo/documents/CppPractice.pdf 

            posted @ 2012-04-01 10:08 陳碩 閱讀(2781) | 評論 (0)編輯 收藏

            C++ 工程實踐(10):再探std::string

            本文總結了std::string的三種常見實現方式。 


            全文見 https://github.com/downloads/chenshuo/documents/CppPractice.pdf 第13節。 

            posted @ 2012-03-17 16:58 陳碩 閱讀(3368) | 評論 (3)編輯 收藏

            僅列出標題
            共6頁: 1 2 3 4 5 6 
            <2025年5月>
            27282930123
            45678910
            11121314151617
            18192021222324
            25262728293031
            1234567

            導航

            統計

            常用鏈接

            隨筆分類

            隨筆檔案

            相冊

            搜索

            最新評論

            閱讀排行榜

            評論排行榜

            一本一本久久aa综合精品| 色8久久人人97超碰香蕉987| 久久精品无码专区免费| 亚洲午夜无码久久久久小说| 久久精品国产亚洲77777| 亚洲天堂久久精品| 久久亚洲中文字幕精品一区| av无码久久久久不卡免费网站| 精品人妻伦九区久久AAA片69 | 久久亚洲欧美国产精品| 亚洲国产成人久久精品动漫| 东方aⅴ免费观看久久av| 久久成人18免费网站| 久久久无码一区二区三区| 武侠古典久久婷婷狼人伊人| 99久久久精品免费观看国产| 久久强奷乱码老熟女网站| 精品一久久香蕉国产线看播放| 精品综合久久久久久888蜜芽| 国产一区二区久久久| 久久av高潮av无码av喷吹| avtt天堂网久久精品| 7777精品伊人久久久大香线蕉| 狠狠人妻久久久久久综合| 2021久久国自产拍精品| 日产精品久久久一区二区| 久久天天躁夜夜躁狠狠| 久久免费视频一区| 国产AⅤ精品一区二区三区久久| 精品国产乱码久久久久久1区2区| 色狠狠久久综合网| 色综合久久88色综合天天 | 国内精品伊人久久久久影院对白| 久久国产乱子伦精品免费强| 久久久久亚洲AV无码专区体验| 精品伊人久久大线蕉色首页| 一日本道伊人久久综合影| 日韩va亚洲va欧美va久久| 欧美亚洲另类久久综合婷婷| 久久男人中文字幕资源站| 久久影院亚洲一区|