huaxiazhihuo

 

2018年5月26日

string类的设计

String类的设计一点都不容易,先不论C++,那怕是其他语言,在面对string的时候,一不小心也会掉坑,好比java,好比C#,一开始假设utf16是定长编码,后来Unicode发展到两个字节就装不下一个码位,字符串在java下,就有点尴尬了。就算是昧着良心用utf32编码,码元与码位终于一一对应了,也会遇到物理字符与逻辑字符不对应的时候,好像有些语言的字符要用两个unicode值来表示(很奇怪),有些语言的一个小写字符对应着好几个大写字符。即便是字符串选定了一种编码方式,始终还是要解决与其他编码的交互问题,这些交互接口也不容易设计。另外,每次从长字符串中截取字符串都要重新new出来一条新的字符串,难免有一点点浪费,当然,现在计算机性能过剩,这纯粹是强迫症。

而到了c++下,设计字符串所遇到的问题,就远比想象中复杂,无中生有的又凭空多出来很多不必要的要求,内存资源管理(这个在C++几乎是无解),异常安全(往字符串添加新内容,假如内存分配失败,必须保持原有值的完整性),还有性能要求(截取字符串避免生成新的字符串)。很多很多的要求,导致语言层面上压根就没法也不可能提供原生的字符串支持,而这一点上又引出来新的问题,代码里面,逻辑意义上看,就不止存在一种字符串类型。好在,大C++拥有丰富多彩的feature,应该足以实现字符串类型了,这也是大C++的设计哲学,既然语言上没法实现的东西,就提供用以支持这种东西的feature,用户要怎么实现就怎么实现,选择权交到用户手里。

所以,C++的库要怎么做出来一道string,这道菜的味道如何,就很让人好奇。一路考察下来,让人大跌眼镜,竟然没有一个c++库能提供品质优良字符串, 其抽象充其量也就是比字符数组好一点点,完全就没有Unicode编码的抽象。Stl的字符串更让人发指,竟然有几个模板参数,本来多类型的字符串问题就更是雪上加霜了,另外stlstring还不能作为dll函数的参数类型。其实,很多时候,猿猴的要求真的不高,只要求一种utf8编码的string,带有格式化,还有一些splittrimFindOneOftoupper等常用字符串处理的操作就行了,只可惜,没有一个c++库能基本满足这样的基本要求。其实,这些要求,具体到C++下,要基本满足,也的确很困难。

除了c++,很多语言的string类型都是原子属性,一个string值,但凡一点风吹草动,都要生成新的string值,原有的值必须保持不变。此外,其官方也提供了类似于StringBuffer或者StringBuilder用以构造很长很长,以弥补这种动不动就生成新String的性能问题。这两种类型泾渭分明。而c++string,似乎是把这两种类型糅合在一块了,由此带来语义上的不清晰,也造成很多不必要的麻烦,因为绝大多数场合下,只需要使用string的原子属性,可变的string只是用来保存字符缓冲而已。知道吗,stlstring有一百多个成员函数,很多都是不必要的重载,不过是为了避免字符串的复制而已。

所以,首先要对只读的string做抽象,也即是string_view,只需两个成员字段,字符串的起始地址以及缓冲长度,并且不要求以0结束,它有一个很好的特性,字符串的任何一部分,也都是字符串,甚至,必要时,一个字符,通过取地址,也可以看做是长度为1string_view。任何连续的内存字符块,都可以看做是string_view。其不必涉及内存的分配,显得非常的轻量级,可以在程序中到处使用,只需注意到字符缓冲的生命周期,就不必担心会引来什么问题。在string_view上,可以做trim,比较,查找,反向查找等操作,除了读取单个字节的迭代器,还提供两套迭代器,用以取到unicode码位值(uin32),和用以访问逻辑字符,其值也为stirng_view

剩下来就是可写可修改的string,要求以0结束,也即是stlstring,因为很多函数都在string_view上,所以这里基本上都只是插入、添加、删除、替换的操作,要注意的是,中括号操作符不能返回字符引用,因为那样完全没有任何意义,就算是保留中括号返回字符值,意义也很小。Trim、查找、比较等操作,必须通过其成员函数view来返回代表自己的string_viewString的很多成员函数,大多数参数类型就是string_view,因此也没有像是在stl下垃圾string的那么多乱七八糟的重载。很简明的设计,性能与简单的良好统一,不知为何,stl要到c++17的时候,才会加入stirng_view这么重要的类型,即便是如此,stlstring既有代码已成定局,也没办法用string_view来简化它的一百多个的成员函数了

posted @ 2018-05-26 11:51 华夏之火 阅读(813) | 评论 (0)编辑 收藏

2018年5月22日

U8String的重构体会

近两年来在写C++的运行时环境,反射、运行时类型信息、内存管理、并行、字符串、协程、ORM等等,基本上重写了一套标准库以及运行库。对于在c++下使用字符串,深有体会。一开始呕心沥血,殚精竭虑,支持多种编码方式(Utf8Utf7GB2312Utf16LEUtf16BE等)的字符串类型,以及在此之上的对这些字符串提供格式化、字符串解析、jsonxml、文件读写BOM等等功能,必须承认,大C++真是变态,像是这样变态无聊的概念都可以支持,还可以实现得很好,用起来确实也方便。可是,每次面临字符串操作的时候,都会心里发毛,都会嘀咕此时此刻,纠结的是哪门子的编码,也搞得很多代码必须以template的形式,放在头文件上,不放在头文件,就必须抽象出来一个通用的动态字符串类型,代表任意编码的一种字符串类型,代码里面引入各种各样臆造的复杂性。终于受不了啦,最后搞成统一用utf8编码,重构了几千行代码(十几个文件),然后,整个字符串世界终于清静了,接口api设计什么的,也一下子清爽了很多。整个程序内部,就应该只使用同一种编码的字符串。stl的带有多个模板的string设计,就是无病呻吟,画蛇添足。

为什么选择Utf8编码,首先,非unicode编码的字符串是不能考虑的;其次,utf16也是变长的编码方式,而且还有大小端的区别,所以也不能考虑;utf32又太占用内存了。想来想去,终于下定决心,utf8简直就是唯一的选择了。虽然可能有这样那样的小问题,比如说,纯中文文本,utf8占用多50%内存(相比于Utf16),windowsutf8有点不友好。但其实都不是问题,也都可以解决。比如说,windows下,所有的涉及字符串与系统的api交互,先临时转换成utf16,然后再调用apiapi的返回结果为utf16,再转换为utf8。好像有一点性能上的损失,其实没啥大不了的。windows对于多字节也是这样支持的,完全就感受不到性能上的影响。总之,utf8简直就是程序处理的唯一字符串编码。

吐槽一下std的字符串,以及与此相关的一切概念,iostreamlocale等等东西,垃圾设计的典范。接口不友好,功能弱,而且还性能差,更关键的是其抽象上的泄漏。一整天就只会在引用计数,写时复制,短字符串优化上做文章,时间精力都不用在刀刃上。C++17终于引入string_view的类型,情况稍微有些改善。由于字符串使用上不方便,也因此损失了一大片的用户,阵地一再失守。整体上讲,stl的设计,自然是有精心的考虑,但是,作出这些抽象的标准会上一大群的老爷子们,大概率上讲,应该是没有用stl正儿八经地开发工业级上的代码,臆造抽象,顾虑太多,表面上看起来好像是那么一回事,真正用起来的时候,就不太对劲,会有这样那样的不足,很不方便。

简单说一下U8String的设计思路。U8String用以管理字符串编码缓存的生命周期,追加缩短替换字符串,支持通过下标可以读取字节char,但是不支持将字节写入到某个索引上的位置,当然支持往字符串中插入unicode编码的字符。至于字符串的比较、查找、Trim、截取子字符串这些常用操作,就全部压在U8View上。如果U8String要使用这些,要先通过view的函数,获取自己字节缓存下的视图。U8View表示一段连续的字符编码内存,U8View的任意一部分也是U8View,不要求以0结束。只要求U8View的生存周期不能比其宿主(U8String,字符数组,U8原生字符串)长命。事实上,很多api的字符串参数,其实只是要求为U8View就行了,不需要是什么const string&类型。此外,还提供U8PointPtr的指针类型,用以遍历U8View,其取值为unicode编码值,也就是wchar_t类型。另外,既然有U8View,自然也就有ArrayView,代表连续内存块的任意类型。

自然,库中必须提供格式化Fmt以及解析字符串Scanf的函数。StrFmt用以生成新的U8String,而Fmt格式化函数中传入字符串的话,就将格式化结果追加到字符串后面。Fmt可以格式化数据到控制台,文本文件,日志等等输出结果上。StrFmt的实现只是简单地调用Fmt并返回U8String。有了FmtScanf,操作字符串就很方便很灵活了,同时也消除很多很多有关字符串相关的处理函数。Fmt不仅仅能格式化基本类型,自定义类型,还能格式化数组,vectorlistpairtuple等模板类型的数据。库中也提供了类似于iostream重载<<>>的操作符。大C++提高的feature,造出来的string类型,使用上的方便,一点都不逊色于其他任何语言的原生string类型。当然,std的那个string,简直就是废物。

不管怎么说,本人还是很喜欢C++的,用c++写代码很舒畅,可比用C#haskelllispscala时要开心很多。C++发展到C++11,基本功能也都完备了,当然,C++14C++17自然功能更加强大,特别是实现模板库的时候,就更方便了,也确实很吸引人。自然,C++也非十全十美,也有很多的不足,比如不能自定义操作符,不提供非侵入式的成员函数,缺乏延迟求值的语言机制,引用的修改绑定(只要不绑定到nullptr就好了),成员函数指针的无端限制。但是,世界上又哪里存在完美的language呢,特别是对于这种直接操纵内存的底层语言来说。至于rust,叫嚣着要取代c++,就它那副特性,还远着呢。

posted @ 2018-05-22 17:10 华夏之火 阅读(792) | 评论 (0)编辑 收藏

2017年12月13日

私有继承小讨论

大家都知道,大C++里面可以私有继承,之后基类的一切,在子类中就成为private的了,不对外开放了。现在流行接口,组合优化继承,所以private继承这玩意,日渐式微,很久以前就很少使用了,嗯,不要说private,就算是大c++,也是江河日下。不过,存在即合理,c++语法里面的任何东西,都有其价值,平时可以用不到,但是关键时刻用一下,确实很方便,当然多数情况下,也可以其他途径来完成,但是,就是没那么舒服。

废话就不说了,直入正题吧。

假设,现在有接口,假设是IUnknown,里面有那三个著名的纯虚函数,QueryInterface, AddRef, Release,好像是这三个哥俩。

然后,有一个类,就叫ClassA,实现了IUnknown接口,其实就是继承IUnknown,也就是说,重写了那三个纯虚函数。此外,ClassA还有一大堆自己的东西,比如public的字段或者成员函数。

现在,有ClassB,想基于ClassA来做一些事情,但是又不想让用户看到ClassA里面那些乱七八糟的玩意,因此,这种情况下,用private似乎很合适。代码如下:

         struct IUnknown

         {

         public:

                            virtual HRESULT QueryInterface(REFIID riid,void** ppvObject) = 0;

                            virtual ULONG AddRef() = 0;

                            virtual ULONG Release() = 0;

         };

         struct ClassA : IUnknown

         {

                   virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) override { ... }

                   virtual ULONG AddRef() override { ... }

                   virtual ULONG Release() override { ... }

                   ...

         };

         struct ClassB : private ClassA

         {

                   ...

         };

这里,内存的使用上非常紧凑,可以说,没有多余的地方。但是,这里的private,不仅仅会private ClassA的一切,就连IUnknown也被private,这有时候就不符合要求了,因为这里意图是,private ClassA,但是又想public IUnknown,也就是说,对外界来说,ClassB不是ClassA,虽然其内部基于ClassA实现,但是,又希望ClassBIUnknown。对此,有几种解决做法,但是都不能让人满意。

方法1、让ClassB再次实现IUnknown接口,如下所示:

         struct ClassB : private ClassA, public IUnknown

         {

                   virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) override { ... }

                   virtual ULONG AddRef() override { ... }

                   virtual ULONG Release() override { ... }

         };

其好处是,ClassB的实例可以无缝用于IUnknown的一切场合,不管是引用或者指针,constconst。但是,代价也是很大的,首先要针对IUnknown的每个虚函数,都要一一手写,再次转发给private的基类,其次,ClassBClassA多了一个虚函数表指针,大小就比原来多了一个指针的大小,这就不是零惩罚了,这是最不该。

方法2,还是保持私有继承,再在ClassB中添加几个函数,用以返回IUnknown,代码如下

         struct ClassB : private ClassA

         {

                   //也可以using ClassA的三个IUnknown里面的函数

                   const IUnknown* GetUnknown()const { return this; }

                   IUnknown* GetUnknown()const { return this; }

         };

避开了方法1的不足,但是就不能无缝用于IUnknown下,每次使用必须调用一下GetUnknown(),对于引用的情况下,还必须加多一个星号*,也是挺不方便的。对了,这里就算添加了类型函数重载,也即是operator IUnknown,编译器也拒绝