本周小贴士#120:返回值是不可触碰的
Posted -飞鹤-
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了本周小贴士#120:返回值是不可触碰的相关的知识,希望对你有一定的参考价值。
作为TotW#120最初发于2012年8月16日
由Samuel Benzaquen, (sbenza@google.com)创作
假设你有如下代码片段,它依赖于RAII清理函数,似乎如同预期那样生效:
MyStatus DoSomething()
MyStatus status;
auto log_on_error = RunWhenOutOfScope([&status]
if (!status.ok()) LOG(ERROR) << status;
);
status = DoA();
if (!status.ok()) return status;
status = DoB();
if (!status.ok()) return status;
status = DoC();
if (!status.ok()) return status;
return status;
重构最后一行return status;为return MyStatus();接着代码突然中止日志打印错误。
接下来发生了什么呢?
概述
在return语句运行之后,永远不要访问(读取或写入)函数的返回变量。除非你非常小心地正确执行此操作,否则该行为是未指定的。
返回变量在被拷贝或移动后,会被析构函数隐式访问(参见C++11标准[stmt.return]的第6.6.节3),这就是这种意外访问的发生方式,但是拷贝/移动可能被省略,这就是行为未定义的原因。
此贴士仅适用于在你返回非引用局部变量时,返回任何其他表达式都不会触发此问题。
问题
return语句有2种不同的优化可以修改我们的初始代码片段的行为:NRVO(参见TotW#11)和隐式移动。
前面的代码有效,因为正在发拷贝省略,且return语句实际上并没有做任何事情。变量状态已经在返回地址中构造,并且清理对象在返回语句之后看到了MyStatus对象的这个唯一实例。
在后面的代码中,拷贝省略无法应用,并且返回变量正被移动到返回值中。RunWhenOutOfScope()在移动操作完成后运行,接着它看到了一个来自MyStatus的移动对象。
请注意,前面的代码也不正确,因为它依赖于拷贝省略优化来确保正确性。我们鼓励你依靠拷贝省略来提高性能(参见TotW#24),但是不要依赖它的正确性。毕竟,拷贝省略是一种可选的优化,编译器选项或编译器的实现质量会影响它是否发生。
解决方案
不要在return语句之接触返回变量。注意局部变量的析构函数隐式的操作。
最简单的解决方案是将函数一分为二。一个完成所有处理,一个调用第一个并进行后续处理(即登录错误)。例如:
MyStatus DoSomething()
MyStatus status;
status = DoA();
if (!status.ok()) return status;
status = DoB();
if (!status.ok()) return status;
status = DoC();
if (!status.ok()) return status;
return status;
MyStatus DoSomethingAndLog()
MyStatus status = DoSomething();
if (!status.ok()) LOG(ERROR) << status;
return status;
如果你只是阅读这些值,你也需要确保禁用优化。这将强制始终制作副本,并且后处理将不会看到移出的值。
MyStatus DoSomething()
MyStatus status_no_nrvo;
// 'status' is a reference so NRVO and all associated optimizations
// will be disabled.
// The 'return status;' statements will always copy the object and Logger
// will always see the correct value.
MyStatus& status = status_no_nrvo;
auto log_on_error = RunWhenOutOfScope([&status]
if (!status.ok()) LOG(ERROR) << status;
);
status = DoA();
if (!status.ok()) return status;
status = DoB();
if (!status.ok()) return status;
status = DoC();
if (!status.ok()) return status;
return status;
另一个示例
std::string EncodeVarInt(int i)
std::string out;
StringOutputStream string_output(&out);
CodedOutputStream coded_output(&string_output);
coded_output.WriteVarint32(i);
return out;
CodedOutputStream在析构函数中做了一些工作来修剪未使用的尾部字节。如果NRVO没有发生,此函数可以在字符串上留下垃圾字节。
请注意,在这种情况下,你不能强制NRVO发生,禁用它的技巧也无济于事。我们必须在return语句运行前修改返回值。
一个好的解决方案是添加一个块并将函数限制为仅在块完成后返回。像这样:
std::string EncodeVarInt(int i)
std::string out;
StringOutputStream string_output(&out);
CodedOutputStream coded_output(&string_output);
coded_output.WriteVarint32(i);
// At this point the streams are destroyed and they already flushed.
// We can safely return 'out'.
return out;
结论
不要保留对正在返回的变量的引用。
你无法控制NRVO是否发生。编译器版本和选项可以背着你更改此设置。不要依赖它来追求正确性。
你无法控制返回的局部变量是否触发隐式移动。你使用的类型将来可能会更新以支持移动操作。此外,未来的语言标准将在更多情况下应用隐式移动,因此你不能仅仅因为现在没有发生就认为将来不会发生。
以上是关于本周小贴士#120:返回值是不可触碰的的主要内容,如果未能解决你的问题,请参考以下文章