1. 程式人生 > >《C++0x漫談》系列之:瘦身前後——兼談語言進化

《C++0x漫談》系列之:瘦身前後——兼談語言進化

瘦身前後——談語言進化

By 劉未鵬(pongba)

C++0x漫談》系列導言

這個系列其實早就想寫了,斷斷續續關注C++0x也大約有兩年餘了,其間看著各個重要proposals一路review過來:rvalue-referencesconceptsmemory-modelvariadic-templatestemplate-aliasesauto/decltypeGCinitializer-lists…

總的來說C++09C++98相比的變化是極其重大的。這個變化體現在三個方面,一個是形式上的變化,即在編碼形式層面的支援,也就是對應我們所謂的程式設計正規化(paradigm)

C++09不會引入新的程式設計正規化,但在對泛型程式設計(GP)這個正規化的支援上會得到質的提高:conceptsvariadic-templatesauto/decltypetemplate-aliasesinitializer-lists皆屬於這類特性。另一個是內在的變化,即並非程式碼組織表達方面的,memory-modelGC屬於這一類。最後一個是既有形式又有內在的,r-value references屬於這類。

這個系列如果能夠寫下去,會陸續將C++09的新特性介紹出來。鑑於已經有許多牛人寫了很多很好的tutor這裡這裡,還有C++標準主頁上的一些introductive

proposals,如這裡,此外C++社群中老當益壯的Lawrence Crowl也在google做了)。所以我就不作重複勞動了:),我會盡量從一個巨集觀的層面,如特性引入的動機,特性引入過程中經歷的修改,特性本身的最具代表性的使用場景,特性對程式設計正規化的影響等方面進行介紹。至於細節,大家可以見每篇介紹末尾的延伸閱讀。

瘦身前後——談語言進化

前一陣子寫了一篇文章,提到語言進化的職責之一,就是去除語言中的tricks(職責之二是去除非本質複雜性)。

常看blog的朋友肯定記得我曾寫過的。本來這個系列是打算成書的,但隨著發生了一些轉變逐漸消退,再回過頭來看中的一些元件,發現原本覺得很有寫的必要的東西頓時消失了。

的主頁上也列有一個寫Boost Under The Hood的計劃,一直也不見成文,興許也有類似的原因。

一門語言應該是“Make simple things simple, make complex things possible”的。當我們用語言來表達思想的時候,這門語言應該能夠提供這樣的能力:即讓我們能夠最直接地表達我們的意思,多一分則太多,少一分則太少,好比古人形容美女:增一分則太肥,減一分則太瘦。

這個問題上,有一個我認為是廣泛的誤解,就是“KISS便意味著要精簡語言,並避免在編碼中使用‘高階’語言特性”。對此有一句話我覺得說得好:你不能通過從一門語言中去掉東西來增加表達力。高階特性是一面利刃,用得不好固然傷了自己,但這並不表明就沒有用。任何東西都是在它真正適用的地方適用,霸王硬上弓的話弓斷絃崩反而傷及自身。所以,僅僅因為高階特性容易誤用(而且高階特性的確也容易吸引人去用且容易誤用,不過這是另一個問題),就斷然在任何地方都不用並宣稱這樣才是KISS的話,便因噎廢食了。舉個例子,高階函式是有用的,如果在真正需要高階函式的地方不用高階函式,那不是KISS,只能讓解決方案(或者更確切地說,workaround)更復雜。lambda函式是有用的,但如果在真正需要lambda的地方不使用lambda,也只能導致更復雜更不直觀的workaroundsOOP是有用的,但如果你的程式本來就只是簡單的“資料+操作”你偏要硬上OOP的話,不僅多了編碼時間,而且還降低程式的可見度和可維護性,後者就意味著專案的money。拿C++來說,這是一個廣為詬病的問題。C++的偏向底層的應用領域決定了有不少地方使用C++其實就是“資料+操作”,然而很多人卻因為用的是C++編譯器,便忍不住去使用高階特性,結果把本來簡單的事情複雜化——我自己就有不少次這樣的經歷:用了一大堆類之後,做完了回過頭來再看,這些類都幹嘛來著?需要嗎?最關鍵的就是要清楚自己做的是什麼事情,以及什麼工具才是對你所做的事情最適合的。

說到這裡不妨順便說說另一個誤解:“如果我反正用不著C++裡面的高階特性,那還不如用C罷了”,鑑於C/C++的應用領域,的確有不少地方是可以用C++C部分完成得很好的,所以這個誤解被傳播得還是蠻廣泛的。這裡的一個微妙的忽視在於:用C的話,你就用不到許多很好的C++庫了。用C++的話,你完全可以在你自己的編碼中不使用高階特性(說實話,這需要清醒的頭腦和豐富的經驗,以及剋制能力),但你還是可以利用眾多的C++庫來簡化你的工作的:如果一個transform明明可以搞定的你偏要寫一個for出來難道能叫KISS?如果一個vector就能避免絕大多數記憶體管理漏洞和簡化記憶體管理工作你偏偏要手動malloc/free那能叫KISS(我見過不少用C++編碼卻到處都是malloc/free的)?如果最直接的方式是gc你偏偏要繞一大堆彎子才能保證正確釋放那也不叫KISS(等C++09吧)。如果一個for_each(readdir_sequence(".", readdir_sequence::files), ::remove);能搞定的你偏要寫:

// in C

DIR*dir = opendir(".");

if(NULL != dir)

{

struct dirent*de;

for(; NULL != (de = readdir(dir)); )

{

struct stat st;

if( 0 == stat(de->d_name, &st) &&

S_IFREG == (st.st_mode & S_IFMT))

{

remove(de->d_name);

}

}

closedir(dir);

}

那能叫KISS

總之還是那句話:明確知道你想要表達的是什麼並用最簡潔(在不損害容易理解性的前提下)的方式去表達它。但我認為,KISS不代表最原始

進化——兩個例子

先舉一個平易近人的例子(Walter Bright——D語言發明者——曾在他的一個presentation中使用這個例子),如果我們想要遍歷一個數組,在C裡面我們是這麼做(或者用指標,不過指標有指標自己的問題):

int arr[10];

… // initialize arr

for(int i = 0; i < 10; ++i)

{

int value = arr[i];

printf

}

這個貌似簡單的迴圈其實有幾個主要的問題:

1. 下標索引不應該是int,而應該是size_tint未必能足夠存放一個數組的下標。

2. value的型別依賴於arr內元素的型別,違反DRY,如果arr的型別改變為longunsigned,就可能發生截斷。

3. 這種for只能對陣列工作,如果是另一個自定義容器就不行了。

在現代C++裡面,則是這麼做:

for(std::vector<int>::iterator

iter = v.begin();

iter != v.end();

++iter) {

}

其實最大的問題就是一天三遍的寫,麻煩。for迴圈的這個問題時候也提到。

Walter Bright然後就把D裡面支援的foreach拿出來對比(當然,支援foreach的語言太多了,這也說明了這個結構的高效性)。

foreach(i; v) {

}

不多不少,剛好表達了意思:對v中的每個元素i做某某事情。

這個例子有人說太Naïve了,其實我也贊成,的確,每天不知道有多少程式設計師寫下一個個的迴圈結構,究竟有多少出了上面提到的三個問題呢?最大的問題恐怕還是陣列越界。此外大家也都親身體驗過違反DRY原則的後果:改了一處地方的型別,編譯,發現到處都是型別錯誤,結果一通“查詢——替換”是免不了的了,誰說程式設計師的時間是寶貴的來著?

既然這個例子太Naïve,那就說一個不那麼Naïve的。Java為什麼要加入closure?以C++STL為例,如果我們要:

transform(v1.begin(), v1.end(), v2.begin(), v3.begin(), _1 + _2);

也就是說將v1v2裡面的元素對應相加然後放到v3當中去。這裡用了boost.lambda,但大家都知道boost.lambda又是一個經典的雞肋。_1 + _2還算湊活,一旦表示式複雜了,或者其中牽涉到對其它函式的呼叫了,簡直就是一場噩夢,比如說我們想把v1v2中相應元素這樣相加:f(_1) + f(_2),其中f是一個函式或仿函式,可以做加權或者其它處理,那麼我們可以像下面這樣寫嗎:

transform(…, f(_1) + f(_2));

答案是不行,你得這樣寫:

transform(…,

boost::bind(std::plus<int>(), boost::bind(f, _1), boost::bind(f, _1))

);

Lisper們笑了,Haskeller們笑了,就連Javaer們都笑了。It’s not even funny! 這顯然違反了“simple things should be simple”原則。

如果不想捲入C++ functional的噩夢的話,你也可以這麼寫:

struct Op

{

int operator()(int a1, int a2) { return f(a1) + f(a2); }

};

transform(…, Op());

稍微好一點,但這種做法也有很嚴重的問題。

為什麼Java加入closure,其實還是一個語法問題。從嚴格意義上,Javaanonymous class已經可以實現出一樣的功能了,正如C++functor一樣。然而,程式碼是給人看的,語言是給人用來寫程式碼的,程式碼的主要代價在維護,維護則需要閱讀、理解。寫程式碼的人不希望多花筆墨來寫那些自己本不關心的東西,讀程式碼的人也希望“所讀即所表”,不想看到程式碼裡面有什麼彎子,最好是自然語言自然抽象才好呢。

所以,儘管closure是一顆語法糖,但卻是一顆很甜很甜的糖,因為有了closure你就可以寫:

transform(…, <>(a1, a2){ f(a1) + f(a2) });

Simple things should be simple!

此外,closure最強大的好處還是在於對區域性變數的方便的引用,設想我們想要建立的表示式是:

int weight1 = 0.3, weight2 = 0.6;

transform(…, f(_1)*weight1 + f(_2)*weight2);

當然,上面的語句是非法的,不過使用closure便可以寫成:

int weight1 = 0.3, weight2 = 0.6;

transform(…, <&>(_1, _2){ f(_1)*weight1 + f(_2)*weight2 } );

functor class來實現同樣的功能則要麻煩許多,一旦麻煩,就會error-prone,一旦error-prone,就會消耗人力,而人力,就是金錢。

C++09也有希望加入lambda,不過這是另一個話題,下回再說。

The Real Deal——variadic templates

C++callback類,google一下,沒有一打也有半打。其中尤數boost.function實現得最為靈活周到。然而,就在其靈活周到的介面下面,卻是讓人不忍卒讀的實現;03年的時候我寫的就是boost.function的,當時還覺得能看懂那樣的程式碼牛得不行...話說回來,那篇文章主要剖析了兩個方面,一個是它對不同引數的函式型別是如何處理的,第二個是一個type-erase設施。其中第一個方面就佔去了大部分的篇幅。

簡而言之,要實現一個泛型的callback類,就必須實現以下最常見的應用場景:

function<int(int, int)> caller = f;

int r = caller(1, 2); // call f

為此function類模板裡面肯定要有一個operator(),然而,接下來,如何定義這個operator()就成了問題:

template<Signature>

class function

{

??? operator()(???);

};

???處填什麼?返回值處的???可以解決,用一個traitstypename result_type<Signature>::type,但引數列表處的???呢?

boost採用的辦法也是C++98唯一的辦法,就是為不同引數個數的Signature進行特化:

template<typename R, typename T1>

class function<R(T1)>

{

R operator()(T1 a1);

};

template<typename R, typename T1, typename T2>

class function<R(T1, T2)>

{

R operator()(T1 a1, T2 a2);

};

template<typename R, typename T1, typename T2, typename T3>

class function<R(T1, T2, T3)>

{

R operator()(T1 a1, T2 a2, T3 a3);

};

… // 再寫下去頁寬不夠了,打住

如此一共NN由一個巨集控制)個版本。

這種做法有兩個問題:一,函式的引數個數始終還是受限的,你作出N個特化版本,那麼對N+1個引數的函式就沒轍了。boost::tuple也是這個問題。二,程式碼重複。每個特化版本里面除了引數個數不同之外基本其它都是相同的;boost解決這個問題的辦法是利用巨集,巨集本身的一大堆問題就不說了,你只要開啟就知道有多糟糕了,近一千行程式碼,其中涉及超程式設計和巨集技巧無數,可讀性可以說基本為0。好在這是個標準庫(boost.function將加入tr1)不用你維護,如果是你自己寫了用的庫,恐怕除了你誰也別想動了。所以第二個問題其實就是可讀性可維護性問題,用就是可發現性和透明性的問題,這是一個很嚴重的問題,許多C++現代庫因為這個問題而遭到詬病。

現在,讓我們來看一看加入了variadic templates之後的C++09實現:

template<typename R, typename... Args>

struct invoker_base {

virtual R invoke(Args...) = 0;

virtual ~invoker_base() { }

};

template<typename F, typename R, typename... Args>

struct functor_invoker : public invoker_base<R, Args...>

{

explicit functor_invoker(F f) : f(f) { }

R invoke(Args... args) { return f(args...); }

private:

F f;

};

template<typename Signature>

class function;

template<typename R, typename... Args>

class function<R (Args...)>

{

public:

template<typename F>

function(F f) : invoker(0)

{

invoker = new functor_invoker<F, R, Args...>(f);

}

R operator()(Args... args) const

{

return invoker->invoke(args...);

}

private:

invoker_base<R, Args...>* invoker;

};

整個核心實現就這些!一共才36行!加上解構函式拷貝建構函式等邊角料一共也就70行!更重要的是,整個程式碼清晰無比,所有涉及到可變數目個模板引數的地方都由variadic templates代替。“Args…”恰如其分的表達了我們想要表達的意思——多個引數(數目不管)。與C++98boost.function實現真是天壤之別!

這裡function_invoker是用的type-erase手法,具體可參見我以前寫的,或,或《C++ Template Metaprogramming》(內有超程式設計慎入!)。type-erase手法是像C++這樣的弱RTTI支援的語言中少數真正實用的手法,某種程度上設計模式裡面的adapter模式也是type-erase的一個變種。

如果還覺得不夠的話,可以參考,上面的中帶了三個tr1實現,分別是tuplebindfunction,當然,variadic-templates的好處遠遠不僅僅止於這三個實現,從本質上它提供了一種真正直接的表達意圖的工具,完全避開了像下面這種horribleworkaround

template<class T1>

cons(T1& t1, const null_type&, const null_type&, const null_type&,

const null_type&, const null_type&, const null_type&,

const null_type&, const null_type&, const null_type&)

: head (t1) {}

相關推薦

C++0x漫談系列身前——語言進化

瘦身前後——兼談語言進化 By 劉未鵬(pongba) 《C++0x漫談》系列導言 這個系列其實早就想寫了,斷斷續續關注C++0x也大約有兩年餘了,其間看著各個重要proposals一路review過來:rvalue-references,concepts,memo

C++0x漫談系列右值引用(或“move語意與完美轉發”)(下)

《C++0x漫談》系列之:右值引用 或“move語意與完美轉發”(下) By 劉未鵬(pongba) 《C++0x漫談》系列導言 這個系列其實早就想寫了,斷斷續續關注C++0x也大約有兩年餘了,其間看著各個重要proposals一路review過來:rvalue-r

C++0x漫談系列Concept, Concept!

《C++0x漫談》系列之:Concept, Concept! By 劉未鵬(pongba) 《C++0x漫談》系列導言 這個系列其實早就想寫了,斷斷續續關注C++0x也大約有兩年餘了,其間看著各個重要proposals一路review過來:rvalu

C#基礎拾遺系列使用ILSpy探索C#7.0新增功能點

第一部分: C#是一種通用的,型別安全的,面向物件的程式語言。有如下特點: (1)面向物件:c# 是面向物件的範例的一個豐富實現, 它包括封裝、繼承和多型性。C#面向物件的行為包括: 統一的型別系統 類與介面 屬性、方法、事件 (2)型別安全:C#還允許通過dynamic關鍵字動態

一分鐘開始持續整合系列C 語言 + Makefile

>作者:CODING - 朱增輝 ## 前言 make 工具非常強大,配合 makefile 檔案可以實現軟體的自動化構建,但是執行 make 命令依然需要經歷手動輸入執行、等待編譯完成、將目標檔案轉移到合適位置等過程,我們真正關心的是最終的輸出,卻在這些中間過程上浪費了很多時間。利用 CODIN

AR Drone系列使用ROS catkin創建package並使用cv_bridge實現對ar drone攝像頭數據的處理

ray 進行 mage exec source stp roc waitkey 效果 1 開發環境 Ubuntu 12.04 ROS Hydro 2 前提 可參考這篇blog:http://blog.csdn.net/yake827/arti

caffe日常坑系列undefined reference to symbol '_ZN2cv6String10deallocateEv'

iss ren and tor ssi symbols str mis locate 在使用caffe庫編譯C++時出現的 解決如下: /usr/bin/ld: /tmp/ccA5JGRP.o: undefined reference to symbol ‘_ZN2cv

Xtrabackup系列源碼安裝

xtrabackup 安裝 一、檢查依賴包 rpm -q cmake gcc gcc-c++ libaio libaio-devel automake autoconf bison libtool ncurses-devel libgcrypt-devel libev-devel libcurl-de

zookeeper系列獨立模式部署zookeeper服務

pat 觀察 系統環境 centos 復制 init pac 很多 編輯 一、簡述   獨立模式是部署zookeeper服務的三種模式中最簡單和最基礎的模式,只需一臺機器即可,獨立模式僅適用於學習,開發和生產都不建議使用獨立模式。本文介紹以獨立模式部署zookeeper服務

別扯那些沒用的系列forEach迴圈

序 寫Java程式碼的程式設計師,集合的遍歷是常有的事,用慣了for迴圈、while迴圈、do while迴圈,我們來點別的,JDK8 使用了新的forEach機制,結合streams,讓你的程式碼看上去更加簡潔、更加高階,便於後續的維護和閱讀。好,不說了,"talk is cheap, show me t

大資料調錯系列自己總結的myeclipse連線hadoop會出現的問題

在我們學習或者工作中開始hadoop程式的時候,往往會遇到一個問題,我們寫好的程式需要打成包放在叢集中執行,這無形中在浪費我們的時間,因為程式可以需要不斷的除錯,然後把最終程式放在叢集中即可。為了解決這個問題,現在我們配置遠端連線hadoop,遠端除錯的方法。 一段程式如下:獲取更多大資料視訊資料請加QQ群

大數據調錯系列自己總結的myeclipse連接hadoop會出現的問題

repair tput windows -o 32位 apache qq群 ins mark 在我們學習或者工作中開始hadoop程序的時候,往往會遇到一個問題,我們寫好的程序需要打成包放在集群中運行,這無形中在浪費我們的時間,因為程序可以需要不斷的調試,然後把最終程序放在

別扯那些沒用的系列Java異常

引子 先來一起看看下面的程式碼: package com.huangzx.Exception; /** * @author huangzx * @date 2018/11/27 */ public class ExceptionTypeTest { public class Exceptio

hbase系列初識hbase

一、概述   在hadoop生態圈裡,hbase可謂是鼎鼎大名。江湖傳言,hbase可以實現數十億行X數百萬列的實時查詢,可橫向擴充套件儲存空間。如果傳言為真,那得好好了解了解hbase。本文從概念上介紹hbase,稍微有點抽象,但這是學習hbase必須要了解的基礎理論;如果想直接瞭解hbase的實操內容,

zookeeper系列zookeeper簡介淺

一、zookeeper的定義   開啟zookeeper官網,赫然一行大字,寫著:“Apache ZooKeeper致力於開發和維護實現高度可靠的分散式協調的開源伺服器”。什麼意思呢?就是Apache ZooKeeper的目標是開發和維護開源伺服器,這伺服器是幹什麼的呢?是做分散式協調的。這伺服器的特點是什麼

hbase系列獨立模式部署hbase

一、概述   在上一篇博文中,我簡要介紹了hbase的部分基礎概念,如果想初步瞭解hbase的理論,可以參看上一篇博文 hbase系列之:初識hbase 。本博文主要介紹獨立模式下部署hbase及hbase的幾個基本操作,需要具備一定的Linux基礎。 二、部署前準備   1、純淨的

C++刷題記錄集合union

前言 好久沒有寫C語言的題目了,畢竟現在在學習資料結構,還是要練習c++的,上課的時候老師提到一個萬能標頭檔案#include<bits/stdc++.h> 今天在一個平臺練習C語言的時候正好使用這個檔案頭,感覺挺輕鬆的,省去很多標頭檔案,接下來對題目進行簡單的分析。 題目描述:集合union

【 專欄 】- DevOps系列映象私庫

DevOps系列之:映象私庫 容器化是DevOps推動中一個重要的趨勢,這個專欄中將會介紹流行的映象私庫管理工具以及專案實踐經驗。

【 專欄 】- DevOps系列Ticket管理

DevOps系列之:Ticket管理 需求管理,缺陷跟蹤,Jira/Confluence/Redmine/Trac/Mentis/禪道,如何更好的進行整合,使得在實踐中需求/開發/測試/運維能夠更好銜接,進行一些相關的工具與實踐經

【 專欄 】- DevOps系列版本管理

DevOps系列之:版本管理 版本管理是DevOps中的基礎一環,在這裡會介紹版本管理相關的知識/分支模型/工具/使用方法/實踐經驗