众所周知,编译器或 CPU 可以根据需要重新排序执行,前提是它们遵循 as-if 规则。例如,如果我们有这样一段代码:
C = A + B;
D = E + F;
编译器或 CPU 可能会D = E + F
在C = A + B
. 我可以理解。
今天同事尝试用C++搭建一个日志库。他的想法是使用构造函数和析构函数在一些重载流函数的帮助下制作一些日志,例如operator<<()
.
基本上,他提供了这样一种课程:
class Log
{
public:
Log() { streamObj << "start: " << getCurrentTime() << endl; }
~Log() { streamObj << "end: " << getCurrentTime() << endl; }
};
现在,我是日志库的用户。我的同事告诉我,我可以使用以下库:
void func()
{
Log log;
// do something1
// do something2
// do something3
return;
}
所以当第一行执行时,构造函数被调用,所以我可以在日志中得到一个“开始”。当函数返回时,log
将调用析构函数,所以我会end
在日志中得到一个。借助 object log
,我们可以清楚地找到函数的开始和函数的结束。
这听起来清晰而伟大。
但是,正如我在文章开头提到的那样,机器可能会根据需要进行一些重新排序。所以我现在想知道是否有可能log
调用的构造函数比我们想象的要晚和/或调用的析构函数log
比我们想象的要早,因此log
不能按我们预期的那样工作。我的意思是,代码func
确实如上所示,但是当它被编译或执行时,真正的顺序变成了:
// do something1
Log log;
// do something2
// call the destructor of `log`
// do something3
return
顺便说一句,类中的流Log
被定向到其他地方,例如文件、共享内存或 TCP 套接字。
那我讲道理吗?或者这种重新排序永远不会发生?如果可能发生,是否有某种技术可以禁止这种重新排序,或者是否有某种技术可以提供一个可用的日志库来告诉我们任何函数的开始和结束?
事实上,我听说过一些 C++11 中的新技术,例如std::atomic
and std::atomic_thread_fence
. 据我了解,如果这种重新排序是可能的,我需要的可能是……栅栏?
class Log
{
public:
Log() {
streamObj << "start: " << getCurrentTime() << endl;
// build a fence here!
}
~Log() { streamObj << "end: " << getCurrentTime() << endl; }
};
我真的不知道这是否可能......
关于副作用/可观察的行为
据我了解,这是一种副作用/可观察到的行为:
A = B + C
为什么?因为 的值发生了A
变化。
现在,如果我这样编码怎么办:
void func()
{
Log log;
A = B + C;
}
所以顺序可能是:
- 的构造函数
log
A = B + C
- 的析构函数
log
但是,如果订单变为:
A = B + C
- 的构造函数
log
- 的析构函数
log
我认为那会很好。为什么?因为A
关于副作用/可观察行为的 的值没有被修改。无论我们采用哪种顺序,它的值总是B + C
。
我对吗?如果我是对的,我认为这意味着Log
不会按预期工作。
更新
A = B + C
有一个副作用,即 的值发生了A
变化,但这不是可观察到的行为。