祖传破代码

前几天回爷爷家重装系统,在整理旧硬盘的时候找到了我早年写的一些破烂代码。查看了下时间,这些代码大多书写于2012-2013年间,那段时间我是个什么样的状态呢?使劲回忆了一下,那段时间我是一个小透明(当然我现在仍然是一个小透明)。当时我大专毕业一年,书写的代码都很简陋幼稚,经不起推敲。那段时期,从好友同事WK处学习了与跨平台相关的一个C++的语言技巧,这思路影响了我很长一段时间代码书写的方式。

是什么样的跨平台语言技巧?

我们都知道 DirectX 这套API只能在Windows上使用。想要在 Linux / MacOS 相关系统上进行渲染,我们需要使用不同的API比如:OpenGL / Metal。但是我仍然希望我的程序,如果运行在windows平台上的时候,使用 Direct3D进行渲染,怎么办呢?

代码写两遍!这当然没有什么魔法可言,但是写两遍代码的方法却有许多种。2012年的我,一直都只知道把所有的API直接写在程序里。这意味着假如我需要换个API的话,我得把程序里使用API的每一处地方都找到,然后再全都改掉,有时候甚至需要对结构做出一些修正。这是多么可怕的一件事情!当时WK告诉我,许多时候我们并非直接使用API,而是通过C++的纯虚函数来包装一层,解决这个问题。

怎么通过纯虚函数来包装API ?

最一开始,我们需要定义一个BaseClass,去描述我们需要使用的功能,一般我把他叫做一个“接口”,这就包含了1)一组纯虚函数,作为接口函数使用;为了让析构正确,我们需要显式地定义一个2)虚析构函数和一个3)Release接口。

class Interface
{
public:
    inline ~virtual Interface() { } 
    virtual void Release() = 0;
    virtual const char* GetName() = 0;
};

接下来,我们在不同的文件中去实现这个接口:

// in InterfaceDX.cpp
class InterfaceDX : public Interface
{
public:
    virtual const char* GetName() override { return "direct3d";}
    virtual void Release() override { delete this; }
};

// in InterfaceGL.cpp
class InterfaceGL : public Interface
{
public:
    virtual const char* GetName() override { return "opengl";}
    virtual void Release() override { this->~InterfaceGL(); free(this); } //see below
};

从代码可知,这两个不同的文件会分别使用不同的(Direct3D / OpenGL)API,会引入不同的头文件。而对于用户,他只需要关心class Interface这个接口定义,并不需要关心这个接口是通过哪个API实现的。当然最终我们不会把这两个CPP放在一起编译,我们仍然需要让这些不同的实现提供同一个 Factory 接口的实现:

class Interface
{
public:
    static Interface* Create();
public:
    inline ~virtual Interface() { } 
    virtual void Release() = 0;
    virtual const char* GetName() = 0;
};

// in InterfaceGL.cpp
Interface* Interface::Create() { return new InterfaceDX(); }

// in InterfaceGL.cpp
Interface* Interface::Create() { 
    auto p = (InterfaceGL*) malloc( sizeof(InterfaceGL) );
    p->InterfaceGL();
    return p; 
}

我们把这两个CPP放在不同的项目里,分别成不同的链接库(Whatever,随你喜欢) 。现在我们就获得了两个不同的DLL/SO,其中一个是使用Direct3D实现的DLL,而另一个,则是使用OpenGL实现的SO。在宿主程序里,我们根据系统判断的结果,加载对应版本的模块;然后通过查询Interface::Create接口,构造出处Interface对象,宿主程序的用户客户只能获取到一个Interface的指针,亦只需要关心Interface的接口。

为何提供虚析构和额外的Release接口?

有时候我们需要通过 BaseClass 的指针去引用一个 DerivedClass 的对象,我们在通过 BaseClassPtr 去释放 DerivedClassObject 的时候,需要正确递归调用析构函数。正如以上情况,提供虚析构函数是必要的。考虑如下代码:

Interface* pObject = new InterfaceDX();
// ... omitted ...
delete pObject();

析构函数如果不是虚函数,则系统会调用 BaseClass 的默认析构,不会产生递归效果,导致了 DerivedClassObject(或者继承自Derived的子类对象)的析构函数没有被调用而产生潜在的内存泄露。

而涉及CRT的问题,有可能DLL和宿主程序使用不相同的CRT,这意味着DLL和宿主程序之间内存地址是分离的,此时指针是夸CRT使用的。遵循一个简单的原则:谁申请谁释放。我们提供一个显式的内存释放的Release接口,让申请内存的对象去释放内存,以保证释放地址的正确性。

基本上,接口的构造告一段落。本质上这么做是把实现细节隐藏起来了,因为大部分平台相关的头文件,都被囊括在实现文件里,而对于接口头文件而言,是很干净的。不仅能够快速的替换不同的实现以达到跨平台的目的;同时获得了隐藏细节的好处,不对外暴露很多实现用的结构体、和内部对象。另外需要注意的是,接口头文件中尽量使用原生类型,而不要使用STL或者别的复杂类型,以避免DLL与宿主程序之间出现STL版本不相符合导致错误的问题。

无地自容

尽管现在看起来这是一个常识,大部分的库都会用这样或者那样的姿势包装。对于2012年的我来说,无异于打开了一扇奇妙的大门。我开始尝试许多有意思实践,比如用C/C++/NDK去实现一个FileSystem;用DX/GL/GLES实现了一个 GraphicDeviceInterface(难道这就是GDI的全拼吗);还有Socket、thread、system相关的乱七八糟的小玩具。不仅如此,我还在同年开通了GITHUB账号。尽管现在看起来这些破烂一无是处,只是推着我不断熟悉代码的味道。

时光荏苒,我也稍微了解一些不同的OO名词,什么duck type,closure云云的拼写方式,让我对面向对象编程又有了更深的认识。在踽踽独行的路上,也遇到了突破天际的大神,让我长跪不起。然而这些都不妨碍我现在查阅起以前代码的时候,会脸红害臊。

其中印象深刻的是一个智商148的大神,他告诉我要多看世界上厉害的人写代码,这样才能够取长补短。我一直不以为然,直到最近,我又遇到一个95后的大神,给我推开了模板元编程这个闪光的大门。我终于认识到,假如当年WK没有告诉纯虚函数这个书写方式,我现在可能还是一只小透明(当然现在确实仍然是小透明)。智商148说的是正确的,我最近又在疯狂的书写着各种让人看不懂的模板。

然而我在推演未来一定会对现在写的代码感到害臊到无地自容的情况时,我突然想起了我最开始的上司跟我说过的这么一句话:写代码就和说话一样,说清楚就行了。不要用那些文绉绉的字眼人家可能根本听不懂。我一直把这个理论当做真理,停留在舒适区里。但或许我这位上司也一定经历过了很多次我这样的疯狂练习,才说出这样的话吧!那他眼里的代码又是什么样子的呢?

仅此纪念我瞎折腾的2012。(和潜在可能的瞎折腾的2016?)

ps. duck type

我是从 typescript中才了解到 duck type 这个概念,并没有深究。也就只能随便说说。说到这个duck type,很是有趣:一般的我们看到一只鸭子,都长着羽毛、脚上有蹼、喙又长又硬、能在水里游,还能听到嘎嘎叫。那么我们把任何观察到的物体,只要符合以上这些特征,都认为是一只鸭子(或许它有可能是一直鹅、或者鸳鸯。)或许其实我们应该用水禽type更加合适一些……

这意味着,我们需要定义这样一个接口:

interface object2d
{
    float area;
    vector2 origin;
};

这意味着,只要我们的对象中有 area / origin这两个属性,我就就能把他认为是一个 object2d。比如:

class circle
{
    float area() { return r*r*π; }
    vector2 origin;
};

class rect
{
    float left, top, width,height;
    float area() { return width*height; }
    vector2 origin() { return (left + 0.5 * width, top + 0.5 * height); }
};

这意味着我在某个函数需要一个 object2d 的时候,我可以直接把 circle 或者 rect 当做参数传入函数内。当然,这个语法C++还不支持。但是这给我们提供了一个视野,就像薛定谔的猫一样,你怎么观测circle/rect,他就会根据你的观察、表现出特定样子。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容