【转载】C++ 中的类型擦除

752 阅读7分钟

参考原文地址:(原创)C++ 中的类型擦除

我对文章的格式和错别字进行了调整,并标注出了重要的部分。以下是正文。

正文

关于类型擦除,可能很多人都不清楚,不知道类型擦除是干啥的,为什么需要类型擦除。有必要做个说明.

什么是类型擦除

类型擦除就是将原有类型消除或者隐藏。

为什么需要类型擦除

因为很多时候我不关心具体类型是什么或者根本就不需要这个类型,通过类型擦除我们可以获取很多好处,比如使得我们的程序有更好的扩展性、还能消除耦合以及消除一些重复行为,使程序更加简洁高效。

C++ 中类型擦除方式

归纳一下主要有如下五种:

  • 通过多态来擦除类型
  • 通过模板来擦除类型
  • 通过某种容器来擦除类型
  • 通过某种通用类型来擦除类型
  • 通过闭包来擦除类型

第一种类型

第一种类型隐藏的方式最简单也是我们经常用的,通过将派生类型隐式转换成基类型,再通过基类去多态的调用行为,在这种情况下,我不用关心派生类的具体类型,我只需要以一种统一的方式去做不同的事情,所以就把派生类型转成基类型隐藏起来,这样不仅仅可以多态调用还使我们的程序具有良好的可扩展性。然而这种方式的类型擦除仅仅是部分的类型擦除,因为基类型仍然存在,而且这种类型擦除的方式还必须是继承方式的才可以,而且继承使得两个对象强烈的耦合在一起了,正是因为这些缺点,通过多态来擦除类型的方式有较多局限性 效果也不好

第二种类型

这时我们通过第二种方式擦除类型,以解决第一种方式的一些缺点。通过模板来擦除类型,本质上是把不同类型的共同行为进行了抽象,这时不同类型彼此之间不需要通过继承这种强耦合的方式去获得共同的行为了,仅仅是通过模板就能获取共同行为,降低了不同类型之间的耦合,是一种很好的类型擦除方式。然而,第二种方式虽然降低了对象间的耦合,但是还有一个问题没解决,就是基本类型始终需要指定,并没有消除基本类型,例如,我不可能把一个 T 本身作为容器元素,必须在容器初始化时就要知名这个 T 是具体某个类型。这时多么希望有一种通用的类型啊,可以让我的容器容纳所有的类型,就像 C# 和 Java 中的 Object 类型一样,是所有类型的基类。

第三种类型

遗憾的是 C++ 中没有这种 Object 类型,怎么办?也许有人想到了,可以用 boost.variant 类型,是的,boost.variant 可以把各种不同的类型包起来,从而让我们获得了一种统一的类型,而且不同类型的对象间没有耦合关系,它仅仅是一个类型的容器。让我们看看怎么用 boost.variant 来擦除类型。

struct blob
{
    const char *pBuf;
    int size;
};
// 定义通用的类型,这个类型可能容纳多种类型
typedef boost::variant<double, int, uint32_t, sqlite3_int64, char *, blob, NullType> Value;

vector<Value> vt; // 通用类型的容器,这个容器现在就可以容纳上面的那些类型的对象了
vt.push_back(1);
vt.push_back("test");
vt.push_back(1.22);
vt.push_back({"test", 4});

第四种类型

上面的代码就擦除了不同类型,使得不同的类型都可以放到一个容器中了,如果要取出来就很简单,通过 get(Value) 就可以获取对应类型的值了。这种方式是通过某种容器把类型包起来了,从而达到类型擦除的目的。它的缺点是这个通用的类型必须事先定义好,它只能容纳声明的那些类型,增加一种新类型就不行了。通过第四种方式可以消除这个缺点,通过某种通用类型来擦除类型。类似于 C# 和 Java 中的 Object 类型。这种通用类型是通过 boost.any 实现的,它不需要预先定义类型,不同类型都可以转成 any 。让我们看看怎么用 any 来擦除类型的。

unordered_map<string, boost::any> m_creatorMap;
m_creatorMap.insert(make_pair(strKey, new T)); // T may be any type
boost::any obj = m_creatorMap[strKey];
T t = boost::any_cast<T>(obj);

第五种类型

需要注意的是,第三、四种方式虽然解决了第二种方式不能彻底消除基本类型的缺点,但是还存一个缺点,就是取值的时候仍然依赖于具体类型,无论我是通过 get还是 any_case ,我都要 T 的具体类型,这在某种情况下仍然有局限性

例如,有这样一种场景:

我有 A、B、C、D 四种结构体,每个结构体中有某种类型的指针,名称且称为 info ,我现在提供了返回这些结构体的四个接口供外接使用,有可能是 C# 或者 dephi 调用这些接口,由于结构体中的 info 指针是我分配的内存,所以我必须提供释放这些指针的接口。代码如下:

struct A
{
    int *info;
    int id;
};

struct B
{
    double *info;
    int id;
};

struct C
{
    char *info;
    int id;
};

struct D
{
    float *info;
    int id;
};

// 对外提供的删除接口
void DeleteA(A &t)
{
    delete t.info;
}

void DeleteB(B &t)
{
    delete t.info;
}

void DeleteC(C &t)
{
    delete t.info;
}

void DeleteD(D &t)
{
    delete t.info;
}

大家可以看到,增加的四个删除函数内部都是重复代码,本来通过模板函数一行搞定,但是没办法,C# 可没有 C++ 的模板,还得老老实实的提供这些重复行为的接口,而且这种方式还有个坏处就是每增加一种类型就得增加一个重复的删除接口,怎么办?能统一成一个删除接口吗? 可以,一个可行的办法就是将分配的内存通过一个 ID 关联并保存起来,让外面传一个 ID,告诉我要删那块内存,新的统一删除函数可能是这样:

// 内部将分配的内存存到 map 中,让外面传 ID ,内部通过 ID 去删除对应的内存块
map<int, T> mapT;

template <typename R, typename T>
R GetT()
{
    R result{1, new T()};
    mapT.insert(std::pair<int, T>(1, R));
    return result;
}

// 通过 ID 去关联我分配的内存块,外面传 ID ,内部通过 ID 去删除关联的内存块
void DeleteT(const int &id)
{
    R t = mapT[id]->second();
    delete t.info;
}

很遗憾,上面的代码编译不过,因为 map<int, T> mapT 只能保存一种类型的对象,无法把分配的不同类型的对象保存起来,我们可以通过方式三和方式四,用 variant 或者 any 去擦除类型,解决 T 不能代表多种类型的问题第一个问题解决

但是还有第二个问题,DeleteT 时,从 map 中返回的 variant 或者 any,无法取出来,因为接口函数中没有类型信息,而取值方法 get 和 any_cast 都需要一个具体类型。似乎进入了死胡同,无法只提供一个删除接口了。但是办法总还是有的。

方式五隆重登场了,看似无解的问题,通过方式五就能解决了。通过闭包来擦除类型很好很强大。

在介绍方式五之前,我要先介绍一下闭包,闭包也可以称为匿名函数或者 lambda 表达式,C++11 中的 lambda 表达式就是 C++ 中的闭包,C++11 引入 lambda,实际上引入了函数式编程的概念,函数式编程有很多优点,使代码更简洁,而且声明式的编码方式更贴近人的思维方式。函数式编程在更高的层次上对不同类型的公共行为进行了抽象,从而使我们不必去关心具体类型。关于函数式编程的优点就不多说了。下面看看如何使用方式五去解决上面的问题。

std::map<int, std::function<void()>> m_freeMap; // 保存返回出去的内存块

template <typename R, typename T>
R GetResult()
{
    R result = GetTable<R, T>();

    m_freeMap.insert(std::make_pair(result.sequenceId, [this, result]
                                    { FreeResult(result); }));
}

bool FreeResultById(int &memId)
{
    auto it = m_freeMap.find(memId);
    if (it == m_freeMap.end())
        return false;

    it->second(); // delete by lambda
    m_freeMap.erase(memId);

    return true;
}