很遗憾,这不会自动完成。
但请记住,虚函数表只是运行时多态的语法糖。如果您愿意重新设计代码,有几种替代方案。
- 外部多态
- 手工虚函数表
- 手工多态
1)外部多态
这个想法是有时您只需要暂时使用多态。例如:
std::vector<Cat> cats;
std::vector<Dog> dogs;
std::vector<Ostrich> ostriches;
void dosomething(Animal const& a);
在这种情况下,将虚拟指针嵌入Cat
或Dog
似乎是浪费的,因为您知道它们的动态类型(它们存储为值)。
外部多态性涉及纯具体类型和纯接口,以及中间的简单桥梁,用于临时(或永久,但这不是您想要的)将具体类型适配到接口。
class Animal {
public:
virtual ~Animal() {}
virtual size_t age() const = 0;
virtual size_t weight() const = 0;
virtual void eat(Food const&) = 0;
virtual void sleep() = 0;
private:
Animal(Animal const&) = delete;
Animal& operator=(Animal const&) = delete;
};
class Cat {
public:
size_t age() const;
size_t weight() const;
void eat(Food const&);
void sleep(Duration);
};
这个桥梁只需要编写一次即可:
template <typename T>
class AnimalT: public Animal {
public:
AnimalT(T& r): _ref(r) {}
virtual size_t age() const override { return _ref.age(); }
virtual size_t weight() const { return _ref.weight(); }
virtual void eat(Food const& f) override { _ref.eat(f); }
virtual void sleep(Duration const d) override { _ref.sleep(d); }
private:
T& _ref;
};
template <typename T>
AnimalT<T> iface_animal(T& r) { return AnimalT<T>(r); }
你可以这样使用它:
for (auto const& c: cats) { dosomething(iface_animal(c)); }
它每个项目都会产生两个指针的开销,但只要您需要多态性。
另一种选择是让AnimalT<T>
也使用值(而不是引用)并提供一个clone
方法,这样您可以根据情况完全选择是否有v指针。
在这种情况下,我建议使用一个简单的类:
template <typename T> struct ref ;
template <typename T>
T& deref(T& r)
template <typename T>
T& deref(ref<T> const& r)
然后稍微修改一下桥:
template <typename T>
class AnimalT: public Animal {
public:
AnimalT(T r): _r(r) {}
std::unique_ptr< Animal<T> > clone() const { return { new Animal<T>(_r); } }
virtual size_t age() const override { return deref(_r).age(); }
virtual size_t weight() const { return deref(_r).weight(); }
virtual void eat(Food const& f) override { deref(_r).eat(f); }
virtual void sleep(Duration const d) override { deref(_r).sleep(d); }
private:
T _r;
};
template <typename T>
AnimalT<T> iface_animal(T r) { return AnimalT<T>(r); }
template <typename T>
AnimalT<ref<T>> iface_animal_ref(T& r) { return Animal<ref<T>>(r); }
这样,您可以选择何时需要多态存储,何时不需要。
2)手动制作的v-tables
(仅在封闭层次结构上容易使用)
在C语言中,通过提供自己的v-table机制来模拟面向对象编程是很常见的。如果您知道什么是v-table以及v-pointer如何工作,那么您完全可以自己实现它。
struct FooVTable {
typedef void (Foo::*DoFunc)(int, int);
DoFunc _do;
};
然后在以 Foo
为锚点的层次结构中提供一个全局数组:
extern FooVTable const* const FooVTableFoo;
extern FooVTable const* const FooVTableBar;
FooVTable const* const FooVTables[] = ;
enum class FooVTableIndex: unsigned short ;
然后您在
Foo
类中所需的就是保持最终派生类型:
class Foo {
public:
void dofunc(int i, int j) {
(this->*(table()->_do))(i, j);
}
protected:
FooVTable const* table() const { return FooVTables[_vindex]; }
private:
FooVTableIndex _vindex;
};
由于 FooVTables
数组和 FooVTableIndex
枚举需要知道层次结构中所有类型的信息,所以存在封闭的层次结构。
然而,可以绕过枚举索引,并通过使数组非常量来预先初始化到更大的大小,然后在初始化时让每个派生类型自动注册到其中。因此,在此初始化阶段检测索引冲突,甚至可以进行自动解决(扫描数组以查找空闲插槽)。
这可能不太方便,但确实提供了一种打开层次结构的方法。显然,在启动任何线程之前编写代码更容易,因为我们在这里谈论全局变量。
3) 手工多态
(只适用于封闭的层次结构)
后者是基于我探索 LLVM/Clang 代码库的经验。编译器面临的问题与您面临的问题完全相同:对于成千上万的小项目,每个项目的虚函数指针真的会增加内存消耗,这很烦人。
因此,他们采取了一个简单的方法:
- 每个类层次结构都有一个伴随的
enum
列出所有成员
- 层次结构中的每个类在构造时将其伴随的
enumerator
传递给其基类
- 通过对
enum
进行切换并适当地进行转换来实现虚拟性
代码如下:
enum class FooType { Foo, Bar, Bor };
class Foo {
public:
int dodispatcher() {
switch(_type) {
case FooType::Foo:
return static_cast<Foo&>(*this).dosomething();
case FooType::Bar:
return static_cast<Bar&>(*this).dosomething();
case FooType::Bor:
return static_cast<Bor&>(*this).dosomething();
}
assert(0 && "Should never get there");
}
private:
FooType _type;
};
这些开关很烦人,但是可以通过一些宏和类型列表来实现更多或更少的自动化。LLVM通常使用类似以下的文件:
// FooList.inc
ACT_ON(Foo)
ACT_ON(Bar)
ACT_ON(Bor)
然后你需要执行:
void Foo::dodispatcher() {
switch(_type) {
# define ACT_ON(X) case FooType::X: return static_cast<X&>(*this).dosomething();
# include "FooList.inc"
# undef ACT_ON
}
assert(0 && "Should never get there");
}
Chris Lattner评论说,由于开关是使用代码偏移量表生成的,因此产生的代码类似于虚分配,并且具有大约相同数量的CPU开销,但内存开销较低。
显然,唯一的缺点是Foo.cpp
需要包含所有其派生类的头文件。这有效地封闭了层次结构。
我自愿从最开放的解决方案到最封闭的解决方案进行演示。它们具有各种复杂度/灵活性,您可以选择最适合您的那个。
一个重要的事情是,在后两种情况下,销毁和复制需要特别注意。
gcc
选项可以做到这一点,我肯定会选择对makefile进行一行修改,而不是重新设计大型代码库 :-) - Steve Jessop