当前位置: 首页 > 知识库问答 >
问题:

我如何使我的类不受C++中的“自动值=代理的副本”地雷的影响?

姚向晨
2023-03-14

我有一个相当复杂的数学库我正在工作,我发现了一个令人讨厌的bug当客户端代码使用auto。在创建一个最小再生案例来询问有关它的问题的过程中,我意识到我可以单独使用标准库来再生类似的东西。请参阅这个简单的测试用例:

#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个真位--并调用__ASSERT_FAIL。)

(是的,我知道std::vector 很糟糕--但在本例中,创建一个只有几行长的最小可复制示例很方便)这里有一个较长的示例,它没有使用std::vector ,而是使用了自定义容器类型,删除了赋值和复制/移动,但仍然显示了问题所在。

我理解背后的情况,operator[]返回了一个代理类,目的是实现alltheData[1]=true和相关功能,编写的客户端代码就好像在读取值一样,实际上是将代理存储在boolValue中,然后当客户端稍后修改它认为是bool的内容时,将修改原始源数据。TLDR:“自动”复制了代理。

代码按照程序员告诉它要做的去做,而不是按照程序员的意思去做。

如果程序员希望修改boolValue以更新源数据,他们应该执行auto&boolValue=...,它与返回t&operator[]实现一起工作,但不需要那些需要伪造类似引用行为的自定义代理的实现。

代理的所有copy和move构造函数以及两个赋值运算符都声明为private(也尝试过=delete),但在编译时没有捕获到此错误。无论复制构造函数是否被删除,代理都将被复制。

我为这个bug找到的所有“修复”都集中在代码的客户端部分。它们是:“不要使用自动”、“转换为底层类型”、“通过常量引用访问”等等。这些都是不标准的修复,一旦你发现了坏的行为,你可以添加其中一个作为黑客修复,但底层问题仍然是抓住下一个不知情的用户。

我宁愿排除地雷,也不愿一直绕过它,挂个牌子说“不要使用auto”,或者“永远使用const”,只是标记雷场,它并不能排除它。

  • 第一个首选项是代码按编写方式工作-assert(allthedata[1]==false)通过
    • 在将代理写入Auto时定义代理衰减类型的方法?。因此decltype(boolValue)bool
    • 优先于复制的隐式转换运算符?
    • 在不更改上面的代码段的情况下执行此操作的其他方法吗?
    • 我将copy和move构造函数声明为delete,将move和copy赋值运算符声明为delete。仍在编译。
    • 是否存在将类声明为无法成为LVALUE的情况?

    还有一个问题是这样的代码:

    std::vector<bool> ReadFlags();
    ... later ...
    auto databaseIsLockedFlag = ReadFlags()[FLAG_DB_LOCKED];
    if (databaseIsLockedFlag) <-- Crash here. Proxy has outlived temporary vector.
    

    我在这里只使用向量,因为它是这个问题的一个非常简单的例子。这不是vector的bug,这是代理类型模式的bug,其中vector就是一个例子来说明问题。

    奇怪的是,MSVC的Intellisense引擎有时会将复制一个no-move-no-copy代理类型报告为编译错误,但随后仍将其编译得很好:

共有2个答案

戈曾琪
2023-03-14

是的,这确实是个问题。Afaik在当前的C++(C++20)中没有解决这一问题的方法,除了在调用站点更改代码之外,这并不理想。

有一个提案P0672R0“auto”变量的隐式评估(来自2017年)试图处理这个确切的问题。它在数学库中使用代理类作为示例,就像您的案例一样,并用std::vector 作为示例。它给出了更多从这种模式得出的问题的例子。

本文针对这一问题提出了3种解决方案,全部用语言实现:

>

  • 运算符表示法:

    class product_expr
    {
        matrix operator auto() { ... }
    };
    

    使用声明:

    class product_expr
    {
        using auto = matrix;
    };
    

    衰变的专门化:

    使auto x=expr定义为typeName std::decay ::type x=expr; ,然后使用used可以专门化std::decay

    标准委员会会议上的讨论强烈支持运算符表示法解决方案。然而,我没有找到更多关于这篇论文的更新,所以我个人认为这篇论文或类似的东西在不久的将来不会在该语言中实现。

    因此,不幸的是,目前您唯一的解决方案是教育您的用户了解您的库使用的代理类。

  • 呼延哲
    2023-03-14

    (和运算符+=、-=等)

    我花了很多时间进行了实验,但最终找到了一种方法来减轻最常见的问题,这会使它变得更紧,这样您仍然可以复制代理,但一旦您将它复制到堆栈变量中,您就不能修改它,从而无意中损坏了源容器。

    #include <cstdio>
    #include <utility>
    
    auto someComplexMethod()
    {
      struct s
      {
        void operator=(int A)&& {std::printf("Setting A to %i", A);}
      };
      return s();
    }
    
    int main()
    {
      someComplexMethod() = 4; // Compiles. Yay
    
      auto b = someComplexMethod(); 
      // Unfortunately that still compiles, and it's still taking a 
      // copy of the proxy, but no damage is done yet.
    
      b = 5; 
      // That doesn't compile. Error given is: 
      //   No overload for '='  note: candidate function not viable: 
      //   expects an rvalue for object argument
    
      std::move(b) = 6; 
      // That compiles, but is basically casting around the 
      // protections, aka shooting yourself in the foot.
    }
    
     类似资料:
    • 我有一个相当复杂的数学库,我正在工作,我发现当客户端代码使用Auto时有一个讨厌的bug。在创建一个最小的繁殖案例来问一个关于它的问题的中途,我意识到我可以单独使用标准库复制类似的东西。请参见这个简单的测试用例: 住在Godbolt上。(有趣的事实:Clang实际上将其优化为写入“7”--3个真位--并调用__assert_fail。) (是的,我知道std::vector 很糟糕--但在这种情况

    • 我正在尝试HTML/CSS并尝试学习它们。但是我有一个关于计算盒子大小的小问题。 我试图理解他们使用px作为一个单位,所有的东西加起来,但当我切换到vh,大众,%一些不寻常的happerens,我不知道为什么。 所以,如果你能解释一下是怎么回事,我会很高兴的。 谢谢. 如您所见,有一些溢出的

    • 响应请求时。如何使用jsonify在Flask中返回JSON列表?

    • 我已经调试这段代码好几个小时了,试图让输出正确无误。它在早期工作,但输出中存在逻辑错误,因此我进入并在输出函数中添加了循环和一个额外参数。 现在g给我以下错误: Student.cpp:在成员函数“void Student::input data(std::string,int,STD::string 如何修复此代码?:

    • 我写了这段代码: 我期望得到这样的结果:是的,第一次不,第二次 但GC似乎没有收集到我的WeakReference的目标(joe)。结果是:是的,第一次是的,第二次 我的问题是什么?...我误解了弱引用吗?