祖传破代码

前几天回爷爷家重装系统,在整理旧硬盘的时候找到了我早年写的一些破烂代码。查看了下时间,这些代码大多书写于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,他就会根据你的观察、表现出特定样子。

你可能感兴趣的:(祖传破代码)