【问题标题】:Do object arguments for dummy parameters get constructed?是否构造了虚拟参数的对象参数?
【发布时间】:2023-03-25 22:49:02
【问题描述】:

假设一个函数模板:

template <class T>
void foo(T /* dummy */) {...}

假设foo 是这样调用的:

foo(Widget());

在这种情况下会构造一个Widget 对象吗?

This 帖子询问了一个关于未使用参数的类似问题(并且虚拟参数的参数肯定是未使用的)。答复表明,除非通过函数指针调用函数,否则编译器将优化未使用的参数。

但是,请考虑 Alexandrescu 的 Modern C++ 第 2.5 节中的以下文本:

现在假设您的应用程序中有一条规则:Widget 类型的对象是不可触及的遗留代码,并且在构造时必须采用两个参数,第二个是固定值,例如 -1。你自己的类,派生自 Widget,没有这个问题。

...

在函数没有部分特化的情况下,唯一可用的工具还是重载。一个解决方案是传递一个 T 类型的虚拟对象并依赖于重载:

template <class T, class U>
T* Create(const U& arg, T /* dummy */)
{
   return new T(arg);
}
template <class U>
Widget* Create(const U& arg, Widget /* dummy */)
{
   return new Widget(arg, -1);
}

这样的解决方案会产生构建一个未使用的任意复杂对象的开销

这表明编译器不够聪明,无法避免为虚拟参数构造参数...

那么,哪个是正确的?如果 Alexandrescu 是正确的,那为什么不进行这种优化呢?

【问题讨论】:

  • clang 和 gcc 都觉得需要构造对象。 coliru.stacked-crooked.com/a/40a50d9448543450
  • @BaummitAugen 会是这种“感觉”背后的逻辑吗?
  • @BaummitAugen 是的,因为构造有副作用。检查这一点的唯一方法是使用无副作用的构造类和程序集。
  • 解释这将是一个答案。但既然已经有了,我什至不需要尝试。 :)
  • 顺便说一句,我希望他们描述template&lt;class T&gt;struct tag{};,然后使用tag&lt;Widget&gt;tag&lt;T&gt; 重载作为上述问题的解决方案。不需要将类型信息作为实际实例传递。

标签: c++


【解决方案1】:

创建对象可能会产生副作用。

除非编译器可以证明没有副作用发生,或者标准中没有规定副作用发生的部分,否则在以下两种情况下都不允许消除对象的创建-if(编译器可以对你的代码做任何事情,只要它表现得好像他们没有做改变,达到标准的要求)或省略(在某些情况下你合并某些对象的生命周期,甚至如果它的行为不像你没有合并它们)规则。

例如,假设小部件在一个中心位置注册了它们的存在。创建对象时,存在的 Widget 数量会增加 1 - 根据标准,不发生这种情况是非法的。

即使没有副作用,证明没有副作用也需要编译器收集所有涉及创建 Widget 的代码,并分析它“最终什么都不做” .这可以从困难(具有特殊“对象将在 Y 时间消失”约束以确定是否强制执行任何副作用的大量代码的链接时优化)到不可能(我们正在谈论分析非图灵完备计算结果的琐碎性质)。

所有这些都是针对一个相对奇怪的极端情况,其中“有人无缘无故地创建了一个对象,然后在没有使用它的情况下将其丢弃”。

【讨论】:

  • 可能也很简单,例如如果struct Widget {int x; int y; int widgetID;};
  • @immibis 是的,可以;您的示例复制课程并不昂贵,因此您不在乎(添加int data[1&lt;&lt;18] 使其仍然是 POD 并且价格昂贵)。即使在那里,如果函数在一个编译单元中定义并在另一个编译单元中使用,它也需要 LTO 来消除副本。
【解决方案2】:

这与“聪明”无关;这是关于正确的。标准中没有允许省略这种结构的规则。只有在构造函数没有副作用的情况下,才能通过 as-if 规则避免。

更一般地说,调用站点的代码并不总是可能知道参数在定义站点是否为“虚拟”。否则,语言可能指定不同,尽管这纯粹是推测。


让我们以std::string 为例,只是为了论证(哈哈):

翻译单元 1

// (N.B. no variable name; therefore "dummy")
void foo(std::string)
{}

翻译单元 2

#include <string>

void foo(std::string argument);

int main()
{
   foo("will this be used to construct a std::string?");
}

是的,因为就 TU2 而言,它必须是。


在相反的例子中,省略构造显然是一个可怕的错误:

翻译单元 1

#include <string>
#include <iostream>

void foo(std::string argument)
{
    std::cout << argument << std::endl;
}

翻译单元 2

#include <string>

// (N.B. no variable name; therefore "dummy")
void foo(std::string);

int main()
{
   foo("will this be used to construct a std::string?");
}

不(由您的“提议”),造成了正确的混乱。


根据 C++ 编译模型的性质,这两种情况都是 100% 无法检测到的。

【讨论】:

  • 当然!我怎么能忽略这个?!重点是副作用!您应该在回复中强调这一点。
  • 编译器不限于优化一个翻译单元(至少在理论上,如果不是在实践中)。 LTO 已经出现,编译器在这方面做得越来越好。
  • @Yakk:但它从根本上是有限的。现在考虑运行时链接。
  • @LightnessRacesinOrbit 简单:发送源代码(或等效的东西),然后重新编译双方。 ;) 更严重的是,C++ 标准中涵盖的运行时链接在哪里?但是是的,在实践中,尝试这样做是很难甚至是不可能的。
  • @Yakk:C++ 标准中没有涵盖它,我也没有声称它是。实际上,我的回答的全部信息是,该标准没有为这种“优化”提供任何便利,您唯一的希望是依赖于结合假设规则和您的现代工具链可以发挥的任何精神力量。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-17
  • 1970-01-01
  • 2019-05-12
  • 1970-01-01
相关资源
最近更新 更多