《C++0x漫谈》系列之:瘦身前后——兼谈语言进化

2024-01-18 04:48

本文主要是介绍《C++0x漫谈》系列之:瘦身前后——兼谈语言进化,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

瘦身前后——谈语言进化

By 刘未鹏(pongba)

C++的罗浮宫(http://blog.csdn.net/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++标准主页上的一些introductiveproposals,如这里,此外C++社群中老当益壮的Lawrence Crowl也在google做了非常漂亮的talk)。所以我就不作重复劳动了:),我会尽量从一个宏观的层面,如特性引入的动机,特性引入过程中经历的修改,特性本身的最具代表性的使用场景,特性对编程范式的影响等方面进行介绍。至于细节,大家可以见每篇介绍末尾的延伸阅读。

瘦身前后——谈语言进化

前一阵子写了一篇文章,提到语言进化的职责之一,就是去除语言中的tricks(职责之二是去除非本质复杂性)。

常看blog的朋友肯定记得我曾写过的boost源码剖析系列。本来这个系列是打算成书的,但随着C++的认识发生了一些转变语言级技术的热衷逐渐消退,再回过头来看boost中的一些组件,发现原本觉得很有写的必要的东西顿时消失了。Scott Meyers的主页上也列有一个写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 ::iterator

iter = v.begin();

iter != v.end();

++iter) {

 

}

其实最大的问题就是一天三遍的写,麻烦。for循环的这个问题上篇讲auto时候也提到。

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 (), 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源码剖析就是boost.function的,当时还觉得能看懂那样的代码牛得不行...话说回来,那篇文章主要剖析了两个方面,一个是它对不同参数的函数类型是如何处理的,第二个是一个type-erase设施。其中第一个方面就占去了大部分的篇幅。

简而言之,要实现一个泛型的callback类,就必须实现以下最常见的应用场景:

function caller = f;

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

为此function类模板里面肯定要有一个operator(),然而,接下来,如何定义这个operator()就成了问题:

template

class function

{

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

};

???处填什么?返回值处的???可以解决,用一个traitstypename result_type ::type ,但参数列表处的???呢?

boost采用的办法也是C++98唯一的办法,就是为不同参数个数的Signature进行特化:

template

class function

{

R operator()(T 1 a 1);

};

template

class function

{

R operator()(T 1 a 1, T 2 a 2);

};

template

class function

{

R operator()(T 1 a 1, T 2 a 2, T 3 a 3);

};

… // 再写下去页宽不够了,打住

如此一共NN由一个宏控制)个版本。

这种做法有两个问题:一,函数的参数个数始终还是受限的,你作出N个特化版本,那么对N+1个参数的函数就没辙了。boost::tuple也是这个问题。二,代码重复。每个特化版本里面除了参数个数不同之外基本其它都是相同的;boost解决这个问题的办法是利用宏,宏本身的一大堆问题就不说了,你只要打开boost.function主体实现代码就知道有多糟糕了,近一千行代码,其中涉及元编程和宏技巧无数,可读性可以说基本为0。好在这是个标准库(boost.function将加入tr1)不用你维护,如果是你自己写了用的库,恐怕除了你谁也别想动了。所以第二个问题其实就是可读性可维护性问题,用Matthew Wilson的说法就是可发现性和透明性的问题,这是一个很严重的问题,许多C++现代库因为这个问题而遭到诟病。

现在,让我们来看一看加入了variadic templates之后的C++09实现:

template typename... Args>

struct invoker_base {

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

  virtual ~invoker_base() { }

};

template typename... Args>

struct functor_invoker : public invoker_base Args...>

{

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

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

private:

  F f;

};

template

class function;

template typename... Args>

class function Args...)>

{

public:

  template

  function(F f) : invoker(0)

  {

    invoker = new functor_invoker Args...>(f);

  }

 

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

  {

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

  }

 

private:

  invoker_base Args...>* invoker;

};

整个核心实现就这些!一共才36行!加上析构函数拷贝构造函数等边角料一共也就70行!更重要的是,整个代码清晰无比,所有涉及到可变数目个模板参数的地方都由variadic templates代替。“Args…”恰如其分的表达了我们想要表达的意思——多个参数(数目不管)。与C++98boost.function实现真是天壤之别!

这里function_invoker是用的type-erase手法,具体可参见我以前写的boost.any源码剖析,或上篇讲auto,或《C++ Template Metaprogramming》(内有元编程慎入!)。type-erase手法是像C++这样的弱RTTI支持的语言中少数真正实用的手法,某种程度上设计模式里面的adapter模式也是type-erase的一个变种。

如果还觉得不够的话,可以参考variadic-templates的主页,上面的variadic templates proposal中带了三个tr1实现,分别是tuplebindfunction,当然,variadic-templates的好处远远不仅仅止于这三个实现,从本质上它提供了一种真正直接的表达意图的工具,完全避开了像下面这种horribleworkaround

template

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) {}

更多的源代码见这里tupleC++98实现,代码近千行。利用variadic-templates实现,代码仅百行

和这种更horribleworkaround

template

    _bi::bind_t ::type>

    BOOST_BIND(boost::type , F f, A 1 a 1, A 2 a 2, A 3 a 3, A 4 a 4, A 5 a 5, A 6 a 6)

{

    typedef typename _bi::list_av_6 ::type list_type;

    return _bi::bind_t (f, list_type(a1, a2, a3, a4, a5, a6));

}

更多源代码见这里这里

小小的boost.bind,实现代码逾两千行,其间重复代码无数。用了variadic-templates,实现不过百行。

BTW. variadic templatesC++大会上一次性几乎全数投票通过。lambda能不能进标准则要看几个提案者的工作。目前还没有wording出来。不过只要出了wording想必也会像variadic templates那样压倒性通过的。

--

我的讨论组

TopLanguage




这篇关于《C++0x漫谈》系列之:瘦身前后——兼谈语言进化的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/618114

相关文章

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(

C++变换迭代器使用方法小结

《C++变换迭代器使用方法小结》本文主要介绍了C++变换迭代器使用方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、源码2、代码解析代码解析:transform_iterator1. transform_iterat

详解C++中类的大小决定因数

《详解C++中类的大小决定因数》类的大小受多个因素影响,主要包括成员变量、对齐方式、继承关系、虚函数表等,下面就来介绍一下,具有一定的参考价值,感兴趣的可以了解一下... 目录1. 非静态数据成员示例:2. 数据对齐(Padding)示例:3. 虚函数(vtable 指针)示例:4. 继承普通继承虚继承5.

C++中std::distance使用方法示例

《C++中std::distance使用方法示例》std::distance是C++标准库中的一个函数,用于计算两个迭代器之间的距离,本文主要介绍了C++中std::distance使用方法示例,具... 目录语法使用方式解释示例输出:其他说明:总结std::distance&n编程bsp;是 C++ 标准

C++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa

C语言中的数据类型强制转换

《C语言中的数据类型强制转换》:本文主要介绍C语言中的数据类型强制转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C语言数据类型强制转换自动转换强制转换类型总结C语言数据类型强制转换强制类型转换:是通过类型转换运算来实现的,主要的数据类型转换分为自动转换