【发布时间】:2021-12-03 23:41:32
【问题描述】:
基本 COM,Don Box
第 1 章,作为二进制接口的抽象基,第 18 页
如果您没有这本书的副本,并且对组件对象模型 (COM) 感到好奇,请向下滚动到底部,我会在此处提供更多背景信息。
IFastString接口类定义为
// ifaststring.h
class IFastString
{
public:
virtual void Delete() = 0;
virtual int Length() const = 0;
virtual int Find(const char*) const = 0;
};
extern "C"
IFastString* CreateIFastString(const char* psz);
FastString 类定义为
// faststring.h
#include "ifaststring.h"
class FastString : public IFastString
{
private:
const int m_ch;
char *m_psz;
public:
FastString(const char *psz);
~FastString();
void Delete();
int Length() const;
int Find(const char *psz) const;
};
最后,这里是FastString的实现
// faststring.cpp
#include <string.h>
#include <faststring.h>
IFastString* CreateFastString(const char *psz)
{
return new FastString(psz);
}
FastString::FastString(const char *psz)
: m_ch(strlen(psz))
, m_psz(new char[m_ch + 1])
{
strcpy(m_psz, psz);
}
FastString::~FastString()
{
delete [] m_psz;
}
void FastString::Delete()
{
delete this;
}
int FastString::Length() const
{
return m_ch;
}
int FastString::Find(const char *psz) const
{
// find algorithm implementation goes here
}
当我第一次看到这段代码时,我觉得它有点“奇怪”。我很困惑为什么没有以与删除函数类似的方式定义创建函数。
在我看来,此代码有两种可能的修改。据我所知,以下两种选择都可以。
1:创建函数可以成为成员函数。 (?)
我最初认为这是可能的,但我现在认为这是不可能的,因为 C++ 类成员函数不能用extern "C" 链接定义。
因此,将创建函数定义为非成员的选择并不是完全随意的。
2:删除函数可以变成非成员函数。然后它将与创建功能更紧密地匹配。
// h (interface)
extern "C" // is this needed?
void DeleteIFastString(IFastString *s);
// cpp
void DeleteIFastString(IFastString *s)
{
delete s;
}
// remove the virtual void Delete() function from both
// IFastString and FastString classes
我认为没有做出这个决定的唯一原因是它用另一个非成员函数污染了全局命名空间。然而,另一方面,可以说有一个与“创建者”函数在语法上相似的“删除器”是可取的。
删除器被实现为成员函数还有其他原因吗?
更多背景信息 (COM)
上面例子中ideom的目的是为了解决两个问题:
- 动态库的设计应具有不改变的已定义接口。
这样就可以在不需要修改客户端代码的情况下更改实现。有人可以更新“FastString”动态库,保持接口不变,以便客户端代码可以链接到新版本的动态库,而无需更改客户端源代码。
- 动态库应设计为具有不变的公开对象大小。
这样可以将更改后的动态库发送到客户端,并且编译后的客户端代码可以加载动态库代码,并且函数将访问正确偏移处的数据。
请考虑以下情况:如果接口类的成员变量发生更改,那么这些数据在内存中存储的偏移量或位置将发生更改。如果接口类暴露给客户端代码,那么该接口类的不同编译版本将是二进制不兼容的。
- 如果客户端代码使用与用于编译动态库代码的编译器不同的编译器进行编译,则动态库应可由客户端在运行时加载并按预期工作。
这可能不会发生的原因是编译器以不兼容的方式使用编译代码。例如,虚函数有多种可能的实现方式,因为 C++ 标准没有定义应该如何实现多态性和虚函数。不同的编译器可能会产生不同的不兼容的二进制代码。
Don Box 在几页中比我在这里更准确地描述了这一点。希望我提供了足够的信息以便理解。
COM 通过定义一个没有成员变量的接口类和一个实现类(其数据成员和函数完全不能被客户端代码访问)来解决上述三个问题。除此之外,所有涉及实现类成员数据的函数都使用同一个编译器进行编译。 (用于编译动态库的编译器。)这意味着函数和数据由同一个编译器编译,因此是二进制兼容的。最后,用户可访问的函数使用外部 C 链接定义,以便它们兼容并且可以与使用不同编译器编译的客户端代码链接。
值得注意的是,以上假设架构是相同的。例如,COM 没有解决 x86 代码与 ARM 系统不兼容的问题。 (可能很明显,但值得一提以避免混淆。)
【问题讨论】:
-
Delete 在接口上被调用,即你必须已经有一个对象来调用delete。对于创建,没有接口(或现有对象);如果有的话,你已经有了一个对象,不需要调用 create。
-
一个
extern "C"工厂函数没有对应的相同语言链接的删除器对我来说似乎很奇怪。清理已经只能从 C++ 中实现了……如果语言链接确实没有实际意义并且没有其他目的,那它就变成了一个哲学问题。使用extern "C++"工厂,它也可以是静态成员函数。 -
这是一个教导您 COM 设计原则的教学示例。总是有一个单独的工厂函数,CoCreateInstance() 是规范的。释放对象总是使用接口成员函数 IUnknown::Release() 来完成。避免考虑 C++,客户端程序员可能使用任何语言,而 C++ 不直接支持接口,尽管可以将 C++ 类转换为合理的近似值。
-
COM 可以被 C(和其他与 C 兼容的语言 - VB、C# 等)访问,所以你不能用 C++ 术语来理解 COM。
-
我会在其他“COM 不是 C++”的 cmets 中添加,事实上,是的,COM 确实解决了“x86 代码与 ARM 系统不兼容的问题”的问题。 COM 定义了如何通过 RPC 在两个进程和两台机器之间进行通信 docs.microsoft.com/en-us/windows/win32/com/… 这已使用多年,以允许 Windows 上的 x86 x64 COM 通信