【问题标题】:Should I not pass an interface as const?我不应该将接口作为 const 传递吗?
【发布时间】:2015-09-10 18:09:24
【问题描述】:

我最近(再次)遇到the Delphi compiler code-gen bug when passing an interface as const 泄露参考。

如果您的方法被声明为将接口变量传递为const,则会发生这种情况,例如:

procedure Frob(const Grob: IGrobber);

解决方法是简单地删除const:

procedure Frob(Grob: IGrobber);

我了解const(以及varout)允许您通过引用传递项目。在结构的情况下,这会保存一个参数副本;让您只需将指针传递给项目。

对于Object/Pointer/Interface,不需要通过引用传递,因为它引用;它已经可以放入寄存器了。

为了不再有这个问题,我进行了讨伐。我搜索了所有源代码树:

const [A-Za-z]+\: I[A-Z]

我删除了大约 150 个实例,其中我将接口作为 const 传递。

但有些是我无法改变的。 TWebBrowser 回调事件声明为:

OnDocumentComplete(Sender: TObject; const pDisp: IDispatch; var URL: OleVariant);
                                    \___/
                                      |
                                      ?

我走得太远了吗?我做了坏事吗?

编辑:或者,用一个较少“基于意见” 风格的问题来表述它:是否有任何严重的缺点 将接口作为 const 传递?

奖励:当 Delphi 不(总是)增加接口引用计数时,它们违反了The Rules of COM

引用计数规则

规则 1: AddRef 必须为接口指针的每个新副本调用,Release 必须在接口指针的每次销毁时调用,除非后续规则明确允许。

规则 2:一段代码的特殊知识可以允许 AddRef 的两个或多个接口指针的生命周期的开始和结束的关系/Release 对被省略。

因此,虽然它可能是编译器可以利用的优化,但它必须正确执行,以免违反规则。

【问题讨论】:

  • Embarcadero 多年来一直知道这一点。丹尼索普和巴里凯利都告诉我这是一个缺陷。最近,Marco 在 Google+ 上发表了一些狡辩,解释了为什么改变并非易事。我猜他们在编译器中有一个实现选择,如果不进行重大的重新设计,就很难解决这个问题,而且他们真的不知道如何进行重新设计。
  • 我相信你知道这一点,但 const 带有接口/字符串/dyn_array 的重点不是要避免 pass_by_value,而是要避免 ref-counting 和隐藏的 try-finally随之而来。您在问题中的pass_by_value部分详述了问题。
  • @Johan 这很有趣。语义上const 表示函数的实现不能修改参数。引用计数等是在不进行任何修改的情况下进行的优化。如果编译器更有意义,它甚至会通过观察没有进行任何修改来对值参数进行相同的优化。
  • @IanBoyd:您不需要从界面参数中大量删除每个const。您真正需要做的就是在代码中搜索您在函数调用中内联的接口对象Create()'的任何情况,例如:Func(TMyObject.Create)。这是唯一的泄漏,而不是 const 本身。
  • @IanBoyd:Delphi 没有违反 COM 规则。仅将接口指针作为输入的函数不需要触摸接口的引用计数。如果每个函数调用都这样做,那将是浪费开销。查看用 C/C++ 编写的 any COM 示例(请记住,COM 主要是为 C/C++ 设计的)。 AddRef()Release() 仅在函数正在修改接口参数时使用,或者接口需要在函数调用之后持续存在。否则,接口参数被视为只是一个常规指针。除非使用 const,否则 Delphi 不会这样做。

标签: delphi interface constants


【解决方案1】:

如果您的方法被声明为将接口变量作为 const 传递,则会发生这种情况,例如:

procedure Frob(const Grob: IGrobber);

这不太对。为了发生泄漏,您需要在代码中没有任何内容引用新创建的对象。所以如果你写:

Frob(grob);

没有问题,因为接口grob已经至少有一个引用。

问题出现在你写的时候:

Frob(TGrobberImplementer.Create);

在那种情况下,没有任何东西引用接口,因此它被泄露了。好吧,只要Frob 的实现中没有任何东西引用它,它就会被泄露。

我做了坏事吗?

嗯,这取决于。我认为你所做的不会有什么特别糟糕的事情发生。在性能方面有一个缺点,因为所有接受接口参数的函数现在都必须使用隐式 try/finally 块添加和释放引用。只有你能判断这是否重要。

更重要的问题与您无法控制的代码有关。你给

procedure OnDocumentComplete(Sender: TObject; const pDisp: IDispatch; var URL: OleVariant);

例如。那里没有问题,因为您从不调用该方法。它是您实现的事件处理程序。框架调用它,它传递一个已经被引用的接口。

真正的问题来自 RTL 中声明的方法或您调用的任何其他第三方代码。如果您正在调用方法,并且如果它们使用const 接口参数,那么您可能会落入陷阱。

这很容易解决,尽管很烦人。

grob := TGrobberImplementer.Create;
Frob(grob);

我处理这个问题的理由是这样的:

  1. 按值传递接口参数会降低性能。
  2. 我无法确保我调用的每个方法都会按值接受接口参数。
  3. 因此,我接受这样一个事实,即我至少在某些时候需要处理调用const 接口参数。
  4. 由于有时我必须处理它,而且我讨厌不一致,所以我选择始终接受处理。
  5. 因此,我选择将我写的方法中的所有接口参数设为const
  6. 因此,我确保永远不会将接口作为参数传递,除非它已被变量引用。

【讨论】:

  • 我想知道为什么 dcc 不能像字符串表达式那样为接口表达式(如 Frob(TGrobberImplementer.Create);)创建隐藏变量...
  • @Arioch'The 是的,这很蹩脚,不是吗。
  • @Arioch'The:这将是一个理想的解决方案,但他们已经记录在案,然后说这样做对他们来说在当前的 DCC 编译器架构中实现起来并不容易,这就是为什么它还没有完成。这不是他们可以随便插入并完成的事情,必须重新设计编译器的各个部分以适应它,而且他们还不想重新设计。
  • FWIW,而不是答案末尾的两行和额外变量,您可以在没有额外变量的情况下在一行中完成,但只能使用演员:Frob(TGrobberImplementer.Create as IGrobber);。这也会将 refcount 增加到 1,就像在你的两班轮中一样。
  • @RudyVelthuis 我没有想到这一点,但它会发生,因为编译器觉得有必要防范 as 失败并且它仍然必须处理原始接口。因此它引入了一个隐式局部变量。我发现很难相信很难更改编译器来修复这个长期存在的错误。
猜你喜欢
  • 1970-01-01
  • 2018-03-25
  • 2021-02-22
  • 2021-08-26
  • 2011-05-29
  • 2011-02-23
  • 1970-01-01
  • 2016-01-09
  • 1970-01-01
相关资源
最近更新 更多