我有一个相当复杂的数学库,我的工作,我已经发现了一个讨厌的错误时,客户端代码使用自动。通过创建一个最小的再生实例来提出一个关于它的问题,我意识到我可以单独使用标准库再生一些类似的东西。看看这个简单的测试用例:
#include <vector>
#include <assert.h>
int main()
{
std::vector<bool> allTheData = {true, false, true};
auto boolValue = allTheData[1]; // This should be false - we just declared it.
assert(boolValue == false);
boolValue = !boolValue;
assert(boolValue == true);
assert(allTheData[1] == false); // Huh? But we never changed the source data! Only our local copy.
}
现场直播。(有趣的事实是: Clang 实际上优化了这个过程,写入“7”——3个真比特——以及一个对 _ _ asser_ fall 的调用。)
(是的,我知道 - but in this case it's handy to create a minimum reproducible example that's only a few lines long) Here's a 不使用 std: : Vector < bool > 的更长示例, and uses a custom container type, with assignment and copy/move deleted, and still shows the problem.
我知道底下发生了什么,有一个由操作符[]返回的代理类,用于实现 allTheData[1] = true
和相关功能,客户端代码就像读取值一样,实际上是将代理存储在 booolValue 中,然后当客户端稍后修改它认为是 bool 的内容时,原始源数据就被修改了。“ auto”复制了代理。
程序员让代码做什么,代码就做什么,而不是程序员的意思。
如果程序员需要 booolValue 的更改来更新源数据,他们可以使用 auto& boolValue = ...
,它可以与返回 T&
的 operator[]
实现一起工作,但是不需要那些需要假冒类似引用行为的自定义代理。
代理的所有复制和移动构造函数以及赋值操作符都被声明为 private (也尝试了 = delete
) ,但是在编译时没有捕捉到这个 bug。无论是否删除代理复制建构子,代理都会被复制。
我为这个 bug 找到的所有“修复”都集中在代码的客户端部分。比如: “不要使用 auto”、“强制转换为底层类型”、“通过 const ref 访问”等等。这些都是低于标准的修复程序,一旦发现了不良行为,您可以添加其中一个作为黑客修复程序,但潜在的问题仍然是抓住下一个不知情的用户。
我宁愿移除地雷也不愿意一直绕过它,并且竖起一个标志说“不要使用自动”,或者“一直使用常量”,只是标记雷区,它不会移除它。
assert(allTheData[1] == false)
通过
decltype(boolValue)
是 bool
?还有一个问题是代码如下:
std::vector<bool> ReadFlags();
... later ...
auto databaseIsLockedFlag = ReadFlags()[FLAG_DB_LOCKED];
if (databaseIsLockedFlag) <-- Crash here. Proxy has outlived temporary vector.
我在这里只使用向量,因为它是这个问题的一个非常简单的例子。这不是一个带有向量的 bug,而是一个带有代理类型模式的 bug,向量就是这个问题的一个例子。
奇怪的是,MSVC 的 Intellisense 引擎 有时候报告说,将一个“不移动、不复制”的代理类型复制为一个编译错误,但是 然后无论如何都能很好地编译它: