【发布时间】:2012-02-14 10:10:57
【问题描述】:
这是我想要做的:
我正在开发一个支持插件的跨平台 IDE(Linux 和 Windows)。我需要使用类似于 Eclipse 提供的适配器框架来支持可扩展性。更多细节见here,但基本上我需要以下内容:
让Adaptee 和Adapted 成为完全不相关的类,它们已经存在并且我们不允许以任何方式更改它们。我想创建一个有方法的AdapterManager 类
template <class Adaptee, class Adapted> Adapted* adapt( Adaptee* object);
在给定Adaptee 的实例的情况下,这将创建Adapted 的实例。实例的具体创建方式取决于必须向AdapterManager 注册的适配器函数。每个新插件都应该能够为任意类型贡献适配器函数。
以下是我对可能的解决方案以及为什么它不起作用的想法:
-
C++11 的 RTTI 函数和
type_info类提供了一个hash_code()方法,它为程序中的每种类型返回一个唯一的整数。见here。因此AdapterManager可以简单地包含一个映射,该映射给定了 Adaptee 和 Adapter 类的哈希码,返回一个指向适配器函数的函数指针。这使得上面的adapt()函数的实现变得微不足道:template <class Adaptee, class Adapted> Adapted* AdapterManager::adapt( Adaptee* object) { AdapterMapKey mk( typeid(Adapted).hash_code(), typeid(Adaptee).hash_code()); AdapterFunction af = adapterMap.get(mk); if (!af) return nullptr; return (Adapted*) af(object); }任何插件都可以通过简单地在地图中插入一个附加功能来轻松扩展框架。另请注意,任何插件都可以尝试将任何类适配到任何其他类并成功,只要存在用
AdapterManager注册的相应适配器函数,无论是谁注册的。 - 其中一个问题是模板和插件(共享对象/DLL)的组合。由于两个插件可以用相同的参数实例化一个模板类,这可能会导致相应
type_info结构的两个单独实例和可能不同的hash_code()结果,这将破坏上述机制。从一个插件注册的适配器函数可能并不总是在另一个插件中工作。 - 在 Linux 中,根据this(第 4.2 点),在某些条件下,动态链接器似乎能够处理不同共享库中的多个类型声明。然而,真正的问题是在 Windows 中,似乎每个 DLL 都会获得自己的模板实例化版本,而不管它是否也在其他加载的 DLL 或主可执行文件中定义。与 Linux 中使用的动态链接器相比,动态链接器似乎相当不灵活。
- 我考虑过使用显式模板实例化,这似乎可以减少问题,但仍然没有解决问题,因为两个不同的插件可能仍然以相同的方式实例化同一个模板。
问题:
- 有人知道在 Windows 中实现此目的的方法吗?如果允许您修改现有的类,这会有所帮助吗?
- 您是否知道在 C++ 中实现此功能的另一种方法,同时仍保留所有所需属性:不更改现有类、使用模板、支持插件并且是跨平台的?
更新 1:
这个项目使用 Qt 框架做很多事情,包括插件基础设施。 Qt 确实有助于跨平台开发。如果您知道针对该问题的 Qt 特定解决方案,我们也欢迎您。
更新 2:
n.m.的评论让我意识到我只知道理论上的问题,并没有实际测试过。因此,我使用以下定义在 Windows 和 Linux 中进行了一些测试:
template <class T>
class TypeIdTest {
public:
virtual ~TypeIdTest() {};
static int data;
};
template <class T> int TypeIdTest<T>::data;
这个类在两个不同的共享库/DLL 中实例化,T=int。这两个库都是在运行时显式加载的。这是我发现的:
在 Linux 中一切正常:
- 两个实例使用相同的vtable。
-
typeid返回的对象位于同一地址。 - 即使是静态数据成员也是一样的。
- 因此,模板在多个动态加载的共享库中实例化这一事实完全没有区别。链接器似乎只是使用第一个加载的实例并忽略其余部分。
在 Windows 中,这两个实例是'有点'不同的:
- 不同实例的
typeid返回不同地址的type_info对象。然而,当使用==进行测试时,这些对象是相等的。对应的哈希码也是相等的。似乎在 Windows 上,类型之间的相等性是使用类型的名称建立的——这是有道理的。到目前为止一切顺利。 - 但是,两个实例的 vtables 不同。我不确定这是多大的问题。在我的测试中,我能够使用
dynamic_cast将TypeIdTest的实例向下转换为跨共享库边界的派生类型。 - 还有一个问题是每个实例都使用了自己的静态字段
data的副本。这可能会导致很多问题,并且基本上不允许模板类中的静态字段。
总的来说,即使在 Windows 中,情况似乎也没有我想象的那么糟糕,但我仍然不愿意使用这种方法,因为模板实例化仍然使用不同的 vtables 和静态存储。有谁知道如何避免这个问题?我没有找到任何解决方案。
【问题讨论】:
-
我认为您正在尝试在编译时解决运行时问题。我认为在这种情况下,继承和虚函数将比模板更直接,更不容易出错(因为正如您所指出的,您正在尝试做的事情有很多魔法)。
-
我认为问题的本质是我无法获得一个可以跨插件边界工作的唯一类型标识符(即使在运行时)。至少在 Windows 中似乎是这样。我不确定继承和虚函数会起作用,因为每个插件都可以贡献新的类,这些类也应该是任意适应的。你知道怎么做吗?
-
您不能在编译时从插件调整任意类型,因为您需要所述插件的头文件。这违背了插件的目的,您可以同时编译所有内容。显然,您不能在 C++ 运行时调整任意类型。另一方面,您可以调整具有相同基类的任意类型。如果这还不够,那么每个插件都必须提供一个可以在程序开头调用的注册函数,该函数将注册所有可适配类型及其各自的适配函数。
-
抱歉,再次,但我的大脑正在慢慢地寻找解决方案。你可以有一些类似于 COM 对象的东西。这意味着实现您自己的接口和虚拟表。有一个叫做 boost::reflect 的东西可能会有所帮助,github.com/bytemaster/boost_reflect,但我从来没有像这样使用过它。
-
那么
type_info有什么问题?该标准说两个type_info对象比较相等,如果相应的类型是“相同的”。如果您担心不同库中的两个相同模板实例化可能会以某种方式产生不“相同”的类型,那么您遇到的问题比这更大。您不能自信地在动态库之间共享std::strings 或std::streams,因为它们也是模板实例化。或者跨库边界抛出任何基于模板的异常。我认为有这些问题的实现不会走得太远。
标签: c++ templates dll cross-platform shared-libraries