C++:调用临时对象的构造函数

4
假设我有以下内容:
int main() {
    SomeClass();
    return 0;
}

如果不进行优化,SomeClass()构造函数将被调用,然后其析构函数将被调用,对象就不存在了。

然而,根据IRC频道的说法,如果编译器认为SomeClass构造函数/析构函数没有副作用,那么该构造函数/析构函数调用可能会被优化掉。

我想,显而易见的方法是不使用某些构造函数/析构函数(例如使用函数、静态方法等),但是否有一种方法可以确保调用构造函数/析构函数呢?


12
如果它不会改变世界,你为什么还想去执行它呢? - Khaled Alshaya
1
C++编译器将C++源代码转换为可执行形式。您已经为构造函数/析构函数编写了不起作用的源代码,因此编译器生成了什么也不做的可执行代码。 - JoeG
1
我在需要在函数中的某个时刻执行一个动作,并且希望确保在离开函数时发生另一个动作的情况下使用这种模式。例如:在分析代码时,我会有一个计时器对象,在构造时向管理器注册自身和开始时间,然后在析构时发出结束时间的信号。在msvc/gcc/llvm上,这段代码从未被优化掉过。我猜想如果构造函数和析构函数都是空操作,编译器可能会跳过堆栈分配,但那又何必在意呢? - Chris Subagio
@ChrisSubagio:感谢确认这些编译器遵循标准。 - sehe
1
@ChrisSubagio:我猜定时器的注册/信号是通过函数调用完成的。如果编译器不知道这些函数,它必须假设它们具有可观察的行为,如果编译器知道这些函数,它知道它们具有这种行为。 - celtschk
显示剩余2条评论
3个回答

7
然而,根据IRC频道的说法,如果编译器认为SomeClass构造函数/析构函数没有副作用,那么这些调用可能会被优化掉。粗体部分是错误的。应该是:知道没有可观察的行为。例如,来自最新标准的§ 1.9:

执行良好形式的程序的符合实现应产生与相应的抽象机器实例在具有相同程序和相同输入的情况下的可能执行之一相同的可观察行为。但是,如果任何这样的执行包含未定义的操作,则本国际标准对执行具有该输入的该程序(甚至是第一个未定义操作之前的操作)不提出要求。

事实上,这整个机制是C ++语言中最普遍的习惯用法之一:资源获取即初始化
让编译器优化掉简单的 case-constructors 极其有帮助。这使得迭代器能够编译成与使用原始指针/索引器完全相同的性能代码。
这也使得函数对象能够编译成与内联函数体完全相同的代码。
这使得 C++11 lambda 在简单用例中变得非常优秀。
factorial = std::accumulate(begin, end, [] (int a,int b) { return a*b; });

这句话的意思是:“lambda表达式会编译成类似于函数对象的东西。”
struct lambda_1
{
     int operator()(int a, int b) const 
     { return a*b; }
};

编译器看到构造函数/析构函数可以被省略,函数体会被内联。最终结果是最优的。

更多(不)可观察的行为

标准中提供了一个非常有趣的反例,以激发您的想象力。

§ 20.7.2.2.3

[ Note: The use count updates caused by the temporary object construction and destruction are not observable side effects, so the implementation may meet the effects (and the implied guarantees) via different means, without creating a temporary. In particular, in the example:

shared_ptr<int> p(new int);
shared_ptr<void> q(p);
p = p;
q = p;

both assignments may be no-ops. —end note ]

IOW: 不要低估优化编译器的威力。这绝不意味着语言保证应该被抛弃!虽然根据问题域可能存在更快的算法来获得阶乘 :)

在标准中添加了一些与“可观察行为”相关的引用。 - sehe
@LokiAstari:我试图通过单独加粗来使_knows_更加突出,就是为了这个原因! - sehe

0
我相信如果'SomeClass::SomeClass()'没有被实现为'inline',编译器就无法知道构造函数/析构函数没有副作用,并且它将始终调用构造函数/析构函数。

2
整个程序优化,有人吗?编译器可以注释这样的构造函数,并且当代码被静态链接时,链接器仍然可以省略对构造函数的冗余调用。现在,对于动态链接(共享对象),情况就不同了:共享对象的另一个版本可能会在构造函数中实现逻辑,因此它不能再进行优化。 - sehe

0

如果编译器优化掉了构造函数/析构函数调用的可见效果,那么它就有缺陷。如果没有可见效果,那么你也不应该注意到它。

然而,让我们假设你的构造函数或析构函数以某种方式具有可见效果(因此对象的构造和后续销毁不是有效的无操作),而编译器可以合理地认为它没有(虽然我想不出这样的情况,但这可能只是我缺乏想象力)。那么以下任何一种策略都应该奏效:

  • 确保编译器看不到构造函数和/或析构函数的定义。如果编译器不知道构造函数/析构函数的作用,它就不能假设它没有效果。但请注意,这也禁用了内联。如果您的编译器不进行跨模块优化,只需将构造函数/析构函数放入不同的文件中即可。

  • 确保您的构造函数/析构函数实际上具有可观察行为,例如通过使用易失变量(在C++中,每个易失变量的读取或写入都被视为可观察行为)。

然而,让我再次强调,除非您的编译器极其有缺陷(在这种情况下,我强烈建议您更换编译器 :-)),否则您很可能不需要做任何事情。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接