dustpg / BlogFM

Blog for Me
MIT License
155 stars 23 forks source link

自己常用的C/C++小技巧[1] #32

Open dustpg opened 6 years ago

dustpg commented 6 years ago

自己常用的小技巧

这里列出了自己常用的一些c/c++小技巧, 有些会有不足, 可以简单探讨一下.

接口

分类: 标准

这个是一个几乎为标准的一个东西, c++端将所有函数声明为纯虚函数, 而且不含成员变量, 如:

#if defined(_MSC_VER) && defined(__cplusplus)
#define NOVTABLE __declspec(novtable)
#else
#define NOVTABLE
#endif

struct NOVTABLE IObject {
    // 释放接口
    virtual void Dispose() = 0;
};

类微软编译器所实现的虚函数为虚表以兼容微软编译器(COM组件的实现), 同时微软编译提供了一个优化扩展语句__declspec(novtable)为不需要虚表的类, 不会创建虚表, 以减小程序体积.

c方面则没有标准, 可以像虚表一样实现以兼容:

typedef struct {
    // 释放接口
    void(*dispose)(void* user_ptr);

} object_vtable_t;

typedef struct {
    // 释放接口
    object_vtable_t*        vtable_ptr;

} object_t;

或者直接使用函数指针:

typedef struct {
    // 释放接口
    void(*dispose)(void* user_ptr);
    // 用户数据
    void*       user_ptr;
} object_t;

这样的目的, 用面向对象那一套说就是多态. 还有的, 用自己的话就是'自定义', '自定义'部分推荐使用虚函数实现, 因为编译器会发现只有一个实现而可能会直接调用(激进型优化).

更换接口

分类: 减少分支

常见于c库, 因为一般实现为直接使用函数指针:

typedef struct {
    // 输出数据
    void(*output)(void* user_ptr, const output_t* data);
    // 用户数据
    void*       user_ptr;
} object_t;

很多库会有, 或者说目的就是"输出数据". 比如音频解码之类的, 我们输入原始数据, 解码库输出数据.

但是有可能输出数据的格式每次会不一样, 我们可以在函数里面判断格式再跳转分支. 或者, 判断格式后直接更换接口, 以减少分支跳转.

混合接口

分类: 提高效率

这是自己"自定义"的东西了, 在接口中, 可能需要实现一些'get'操作, 如果'get'是一个简单的return的话, 自己称之为"不划算", 本来可以简单获得的东西没必要单独用虚函数, 浪费空间与时间. 例如:


struct IStream1 {
    // 读取流
    virtual int Read(void*, int) = 0;
    // 获取流长度
    virtual int GetLength() const = 0;
};

struct XStream2 {
    // 读取流
    virtual int Read(void*, int) = 0;
    // 获取流长度
    int GetLength() const { return m_length };
protected:
    // 流长度
    int         m_length;
};

如果是内部接口的话, 自己还会简单写为:

struct XStream3 {
    // 读取流
    virtual int Read(void*, int) = 0;
    // 流长度
    int         length;
};

一方面这多亏了c++允许多继承, 不过实际上多继承还是用的不多, 一般还是单继承.

当然, 这个只适合简单return的'get', 复杂的不算. 复杂的函数用虚函数就比较"划算"了.

pimpl

分类: 标准

pimpl对于c++来说是一个小技巧, 对于c来说几乎没必要或者说实际上处处都能用上. 即 pointer to implement, 自己会酌情使用pimpl:

// 头文件

struct CImpl;

class CPimpl {
    CImpl*       m_pImpl;
};

// 源文件

// 具体实现
struct Impl 
    int a;
};

这样目的就是隐藏实现, 合理使用可以变相地提高编译速度.

::Private

分类: 隐藏实现, 提高编译速度

C++中, 如果是一个比较大的类, 自己会看情况声明一个private, public或者其他权限的::Private结构体:

class CControl1 {
    struct Private;
};

class CControl2 {
public:
    struct Private;
};

这样主要是内层嵌套类允许访问外层的private部分, 自己会有两种用法, 一种就是和pimpl结合使用:

// 头文件
class CPimpl {
    struct Private;
    Private*       m_pImpl;
};

// 源文件
struct CPimpl::Private {
    // 具体实现
};

以及提高编译速度, 减少暴露细节:

试想一下, 如果要写一个大switch, 各个分支肯定是调用函数, 而不是直接写. 如果是这是一条类函数, 这时候就得修改该类的声明, 然后所有引用该头文件的源文件都必须重新编译一次.

// 头文件
class CControl1 {
    int x;
public:
    void OnCase0();
    void OnCase1();
    int GetX() const { return x; }
};
// 源文件
void CControl1::OnCase0() {

}
void CControl1::OnCase1() {

}

而这样实现则可以实现伪动态添加:

// 头文件

class CControl2 {
    int x;
public:
    struct Private;
};

// 源文件1
strtuct CControl2::Private{
    static void OnCase0(CControl2&) {

    }
};

// 源文件2
strtuct CControl2::Private{
    static void OnCase1(CControl2&) {

    }
    static int& GetX(CControl2& x) {
        return x.x;
    }
};

由于每个编译单元(源文件)是独立的, CControl2::Private允许在不同源文件声明地不一致. 这也极大地方便了我们.

private ::Super

分类: 编码技巧

如果自己的c++类存在真正的"继承"关系, 自己会在类声明的第一行(private区域)typedef/using一个Super类型指向基类(超类).

class A{

};

class B : public A {
    using Super = A;
};

在自己所谓真正的"继承关系", 主要是会重写(override)基类的一条或多条虚函数. 某些情况下, 我们派生类需要的是"强化"基类虚函数, 而不是完全的重写基类. 这时候我们会调用基类的该条函数:

void B::Update() {
    // xxxxxx
    A::Update();
}

如果使用了private ::Super的话, 可以直接无脑:

void B::Update() {
    // xxxxxx
    Super::Update();
}

这样, 如果重定向B的基类, 只需要修头文件紧贴的两行, 后面的源文件不用修改.

再特殊一点, 如果实现类有一条函数是跳过基类的实现而调用高阶基类的实现:

class A{

};

class B : public A {
    using Super = A;
};

class C : public B {
    using Super = B;
};

然后


void C::Call() {
    Super::Call();
}

void C::Update() {
    A::Update();
}

这样可以很明显知道C::Update是特殊实现, 提高了可读性.