如何检测 Delphi 类是否有虚拟构造函数?

发布于 2024-07-17 20:32:07 字数 405 浏览 5 评论 0原文

例如,有没有办法找出这个类有一个虚拟构造函数(在运行时)?

   TMyClass = class(TObject)
     MyStrings: TStrings;
     constructor Create; virtual;   
   end;

例如,在这段代码中,我想测试 Clazz 引用的类是否有虚拟构造函数:

procedure Test;
var
  Clazz: TClass;
  Instance: TObject;
begin
  Clazz := TMyClass;
  Instance := Clazz.Create;
end;

是否有一个简单的解决方案,例如使用 RTTI,它适用于 Delphi 6 到 2009?

For example, is there a way to find out that this class has a virtual constructor (at runtime)?

   TMyClass = class(TObject)
     MyStrings: TStrings;
     constructor Create; virtual;   
   end;

For example, in this code I would like to test if the class referenced by Clazz has a virtual constructor:

procedure Test;
var
  Clazz: TClass;
  Instance: TObject;
begin
  Clazz := TMyClass;
  Instance := Clazz.Create;
end;

Is there a simple solution, for example using RTTI, which works in Delphi 6 to 2009?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

静谧幽蓝 2024-07-24 20:32:07

查看 TypInfo 单元,似乎没有任何方法可以使用 RTTI 来判断方法是否为虚拟方法。 不过,如果您有类参考,您可能可以通过检查 VMT 来推出自己的方法。

根据 Allen Bauer 的说法,在对 这个问题,你可以在vmtClassName指向的值之前找到VMT的结尾。 第一个用户定义的虚方法(如果有)可在类引用的地址处找到。 换句话说,指针(Clazz)^。 既然您知道了 VMT 的用户定义部分的起点和终点,那么创建一个 while 循环将表中的每个指针与指向 Clazz.create 的方法指针的代码部分进行比较应该不会太困难转换为 TMethod。 如果你得到一个匹配,那么它就是一个虚拟方法。 如果不是,那就不是。

是的,这有点麻烦,但它会起作用。 如果有人能找到更好的解决方案,他们就会有更多的力量。

Looking through the TypInfo unit, it doesn't look like there's any way to tell if a method is virtual using RTTI or not. If you have a class reference, though, you can probably roll your own method by examining the VMT.

According to Allen Bauer, in an answer to this question, you can find the end of the VMT immediately before the value pointed to by vmtClassName. The first user-defined virtual method (if any) is found at the address of the class reference. In other words, pointer(Clazz)^. Now that you know the start and end points of the user-defined section of the VMT, it shouldn't be too difficult to make a while loop that compares each pointer in the table against the Code section of a method pointer to Clazz.create casted to a TMethod. If you get a match, then it's a virtual method. If not, then it isn't.

Yes, it's a bit of a hack, but it'll work. If anyone can find a better solution, more power to them.

楠木可依 2024-07-24 20:32:07

你知道,我想得越多,我就越不喜欢我给出的最终被接受的答案。 问题是,编写的代码只能处理编译时已知的信息。 如果 Clazz 被定义为 TClass,那么将 Clazz.Create 放入 TMethod 中总是会为您提供指向 TObject.Create 的方法指针。

您可以尝试将 Clazz 定义为“TMyClass 的类”。 事实是,您已经在那里有了一个虚拟构造函数,因此它将为您提供它可以到达的最高级别的构造函数,该构造函数会覆盖该构造函数。 但从你的评论来看,你试图找到的是一个非虚拟构造函数(使用reintroduce;),它会破坏你的虚拟构造。 您很可能正在使用工厂模式,这可能是一个问题。

唯一的解决方案是使用 RTTI 来查找实际附加到该类的构造函数。 您可以获取“名为 Create 的方法”的方法指针,并将其用于我在其他答案中解释的技巧中。 为此,您的基本虚拟构造函数必须声明为已发布。 这将强制发布所有覆盖它的方法。 问题是,有人仍然可以使用 reintroduce; 来声明更高层的未发布的构造函数,而你的方案就会崩溃。 您无法保证后代类会做什么。

这个问题没有技术解决方案。 唯一真正有效的就是教育。 您的用户需要知道此类是由工厂实例化的(或者无论您需要虚拟构造函数的原因是什么),并且如果他们在派生类中重新引入构造函数,则可能会破坏事物。 在文档中添加注释,并在源代码中添加注释。 这几乎就是你能做的一切。

You know, the more I think about it, the less I like the answer I gave that ended up getting accepted. The problem is, the code as written can only deal with information known at compile-time. If Clazz is defined as a TClass, then putting Clazz.Create in a TMethod is always going to give you a method pointer to TObject.Create.

You could try defining Clazz as a "class of TMyClass". Thing is, you've already got a virtual constructor there, so it's going to give you the highest-level constructor it can reach that overrides that constructor. But from your comments, it looks like what you're trying to find is a non-virtual constructor (using reintroduce;) that will break your virtual construction. Most likely you're using a factory pattern, where this could be an issue.

The only solution to that is to use RTTI to find the constructor that's actually attached to the class. You can get a method pointer for "the method named Create" and use it in the trick I explained in my other answer. To do this, your base virtual constructor has to be declared published. This will force all methods that override it to also be published. Problem is, someone can still use reintroduce; to declare a non-published constructor higher up, and your scheme comes crashing to the ground. You don't have any guarantees about what descendant classes will do.

There's no technical solution to this question. The only thing that really works is education. Your users need to know that this class is instantiated by a factory (or whatever your reason is for needing a virtual constructor) and that if they reintroduce the constructor in a derived class, it could break things. Put a note in the documentation to this effect, and a comment in the source code. That's pretty much all you can do.

我很坚强 2024-07-24 20:32:07

迈克尔,

我明白你的问题,但由于你的源代码无法编译,我认为你错过了你的问题的要点;-)

我的回答是对梅森在他的第二个答案中试图解释的内容的一些阐述。

当前的问题是,您的问题暗示您有一个“类引用”(如 TClass 或 TComponentClass),它引用具有虚拟构造函数的基类。
然而,TClass 没有(TClass 引用具有非虚拟构造函数的类),但 TComponentClass 却有。

使用类引用反汇编对构造函数的调用时,您会看到差异。
当您通过类引用调用虚拟构造函数时,代码与调用非虚拟构造函数时的代码略有不同:

  • 调用虚拟构造函数具有间接调用,
  • 调用非虚拟构造函数执行直接调用

此反汇编显示了我的意思:

TestingForVirtualConstructor.dpr.37: ComponentClassReference := TMyComponentClass;
00416EEC A1706D4100       mov eax,[$00416d70]
TestingForVirtualConstructor.dpr.38: Instance := ComponentClassReference.Create(nil); // virtual constructor
00416EF1 33C9             xor ecx,ecx
00416EF3 B201             mov dl,$01
00416EF5 FF502C           call dword ptr [eax+$2c]
TestingForVirtualConstructor.dpr.39: Instance.Free;
00416EF8 E8CFCDFEFF       call TObject.Free
TestingForVirtualConstructor.dpr.41: ClassReference := TMyClass;
00416EFD A1946E4100       mov eax,[$00416e94]
TestingForVirtualConstructor.dpr.42: Instance := ClassReference.Create(); // non-virtual constructor
00416F02 B201             mov dl,$01
00416F04 E893CDFEFF       call TObject.Create
TestingForVirtualConstructor.dpr.43: Instance.Free;
00416F09 E8BECDFEFF       call TObject.Free

因此,当您有一个类引用类型的变量(其构造函数是虚拟的),并且通过该变量调用该构造函数时,您可以确定该变量中的实际类将具有虚拟构造函数。

您无法确定在哪个实际类上实现了构造函数(当然,如果没有额外的调试信息,例如来自 .DCU、.MAP、.JDBG 或其他来源)。

这是编译的示例代码:

program TestingForVirtualConstructor;

{$APPTYPE CONSOLE}

uses
  Classes, SysUtils;

type
  TMyComponentClass = class(TComponent)
    MyStrings: TStrings;
    constructor Create(Owner: TComponent); override;
  end;

constructor TMyComponentClass.Create(Owner: TComponent);
begin
  inherited;
end;

type
  TMyClass = class(TObject)
    MyStrings: TStrings;
    constructor Create();
  end;

constructor TMyClass.Create();
begin
  inherited;
end;

procedure Test;
var
  // TComponentClass has a virtual constructor
  ComponentClassReference: TComponentClass;
  ClassReference: TClass;
  Instance: TObject;
begin
  ComponentClassReference := TMyComponentClass;
  Instance := ComponentClassReference.Create(nil); // virtual constructor
  Instance.Free;

  ClassReference := TMyClass;
  Instance := ClassReference.Create(); // non-virtual constructor
  Instance.Free;
end;

begin
  try
    Test;
  except
    on E: Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
end.

回到你原来的问题:
当您的类引用引用具有虚拟构造函数的基类时,您确信您将始终使用间接调用虚拟构造函数。
当您的类引用引用具有非虚拟构造函数的基类时,您确信您将始终使用直接调用来调用非虚拟构造函数。

希望这能让您的问题得到更多的了解。

——杰罗恩

Michael,

I get your question, but since your sourcecode does not compile, I think you miss the point of your question ;-)

My answer is a bit of an elaboration on what Mason tried to explain in his second answer.

The issue at hand is that your question imples that you have a 'class reference' (like TClass or TComponentClass) that references to a base class that has a virtual constructor.
However, TClass doesn't (TClass references a class that has a non-virtual constructor), but TComponentClass does.

You see the difference when disassembling the call to the constructor by using a class reference.
When you call a virtual constructor through a class reference, the code is slightly different than when you call a non-virtual constructor:

  • calling a virtual constructor has an indirection
  • calling a non-virtual constructor does a direct call

This disassembly shows what I mean:

TestingForVirtualConstructor.dpr.37: ComponentClassReference := TMyComponentClass;
00416EEC A1706D4100       mov eax,[$00416d70]
TestingForVirtualConstructor.dpr.38: Instance := ComponentClassReference.Create(nil); // virtual constructor
00416EF1 33C9             xor ecx,ecx
00416EF3 B201             mov dl,$01
00416EF5 FF502C           call dword ptr [eax+$2c]
TestingForVirtualConstructor.dpr.39: Instance.Free;
00416EF8 E8CFCDFEFF       call TObject.Free
TestingForVirtualConstructor.dpr.41: ClassReference := TMyClass;
00416EFD A1946E4100       mov eax,[$00416e94]
TestingForVirtualConstructor.dpr.42: Instance := ClassReference.Create(); // non-virtual constructor
00416F02 B201             mov dl,$01
00416F04 E893CDFEFF       call TObject.Create
TestingForVirtualConstructor.dpr.43: Instance.Free;
00416F09 E8BECDFEFF       call TObject.Free

So when you have a variable of type class reference for which the constructor is virtual, and you call that constructor through that variable, you are sure that the actual class in that variable will have a virtual constructor.

You can not determine on which actual class that constructor is implemented (well, not without extra debugging info, for instance from the .DCU, .MAP, .JDBG, or other sources).

Here is the example code that does compile:

program TestingForVirtualConstructor;

{$APPTYPE CONSOLE}

uses
  Classes, SysUtils;

type
  TMyComponentClass = class(TComponent)
    MyStrings: TStrings;
    constructor Create(Owner: TComponent); override;
  end;

constructor TMyComponentClass.Create(Owner: TComponent);
begin
  inherited;
end;

type
  TMyClass = class(TObject)
    MyStrings: TStrings;
    constructor Create();
  end;

constructor TMyClass.Create();
begin
  inherited;
end;

procedure Test;
var
  // TComponentClass has a virtual constructor
  ComponentClassReference: TComponentClass;
  ClassReference: TClass;
  Instance: TObject;
begin
  ComponentClassReference := TMyComponentClass;
  Instance := ComponentClassReference.Create(nil); // virtual constructor
  Instance.Free;

  ClassReference := TMyClass;
  Instance := ClassReference.Create(); // non-virtual constructor
  Instance.Free;
end;

begin
  try
    Test;
  except
    on E: Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
end.

To get back to your original question:
When your class reference references a base class having a virtual constructor, you are sure that you will always call a virtual constructor using an indirection.
When your class reference references a base class having a non-virtual constructor, you are sure that you will always call a non-virtual constructor using a direct call.

Hope this sheds some more light on your question.

--jeroen

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文