什么是泛型

这里的泛型, 指的是一种可以存储任意数据的类型。类似于 C++ 标准库中的 anyvariant, 但在使用上更像 Qt 的 QVariant。使用泛型, 可以方便地用一个容器存储多种类型的数据。我最熟悉的使用泛型的例子是 Qt 的属性系统。很显然, 泛型为代码的编写带来了极大的便利。

HOW-TO

数据存储方案

为了存储各种类型, 我们的类必须以指针的形式来存放数据。因为只有用 void* 才可以使类的定义(注意不是成员函数的定义)彻底摆脱模板。至于为什么要摆脱模板? 下图做出了解释。

注意到, 模板类的不同实例化属于不同类型, 不符合一开始的要求。

于是这个类可以写成

class Variant
{
private:
    void* m_data;
};

构造函数与析构函数

最简单的是默认构造函数, 由于没有数据, 直接给个空指针就行

Variant() : m_data {nullptr} {}

这个类使用了堆空间, 不要忘了析构函数释放资源

//Variant.h
~Variant()

//Variant.cpp
Variant::~Variant()
{
    delete m_data;
}

然后是初始化时指定值了。这时, 我们不得不用上模板。

//Variant.h
template <typename T>
Variant(T value)
{
    m_data = new T(std::forward<T>(value));
}

虽然这里使用了模板, 可整个类并不属于模板类。这是非常巧妙的处理。此外, 这里还使用了 C++ 11 的完美转发 forward, 以尽可能降低开销, 同时确保不会因为临时变量而出现问题。

很有意思的一点: 在这个函数中使用 C++ 20 添加的 auto 模板形式时不行的, 会引发一个编译器内部错误。如图。

就很怪🤔🤔🤔

接下来是另外两个特殊的构造函数。由于我们并没有存储任何关于 m_data 类型的信息, 复制 m_data 似乎不太可能。故删除拷贝构造函数。

Variant(Variant&) = delete;

移动构造函数只需要转移数据的所有权, 而无需真正复制数据。

//Variant.h
Variant(Varinat&& other) noexcept;

//Variant.cpp
Variant::Variant(Variant&& other) noexcept
{
    m_data = other.m_data;
    other.m_data = nullptr;
}

等号赋值函数

和前面的大同小异, 同样要注意删掉传入 Variant& 的。

上代码

//Variant.h
template <typename T>
void operator=(T value) { m_data = new T(std::forward<T>(value)); }
void operator=(Variant&) = delete;
void operator=(Variant&&) noexcept;

//Variant.cpp
void Variant::operator=(Variant&& value) noexcept
{
	m_data = value.m_data;
	value.m_data = nullptr;
}

其他函数

写到这里, 我们已经可以创建这个类的对象了, 但并没有方法拿到它真实的值。下面就来解决这个问题。

知乎上有句名言:「先问是不是, 再问为什么」。类似地, 我们先要知道 Variant 有没有值, 再来获取它的值。

bool hasValue() const { return (nullptr != m_data); }

这个也很好理解, 把 nullptr 写前面也是为了防止不小心修改成员的值, 和 const 一起相当于双重保险了。

最重要的部分——「转换函数」要来了。打引号的原因是它并非真正的转换函数, 不过功能相同。先上代码

template<typename T>
const T& Variant::value() const
{
	if (!hasValue())
		return T();
	return *static_cast<T*>(m_data);
}

逐步解析一下。

首先, 它的返回值为什么是 const 引用? 因为不能是非 const (笑)。去掉 const 后, 编译报错: 非常量引用只能绑定到左值。因此只能加 const, 后续若需修改, 可以使用 const_cast 去掉 const

然后, 是一个判断。其实我本来想用 throw 的, 但已经不想增加代码量了, 于是创建了一个空对象。

最后一行, 就是把 m_data 转换为合适的类型后解引用, 这样就能得到所需的值了。

反思

这次的 demo, 和标准库的实现还有很大的差距, 体现了我的诸多不足。如在引用、值类别、模板方面还有很大的欠缺, 今后还要再接再厉。俺も頑張らないと!