如何让我的班级免受 C++ 中“自动值=代理副本”地雷的影响?

use*_*705 153 c++ copy-assignment

我正在开发一个相当复杂的数学库,当客户端代码使用 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.
}
Run Code Online (Sandbox Code Playgroud)

住在 Godbolt 上。(有趣的事实:Clang 实际上将其优化为写入“7” - 3 个真位 - 以及对 __assert_fail 的调用。)

是的,我知道 std::vector<bool> 很烂- 但在这种情况下,创建一个只有几行长的最小可重现示例很方便)这是一个较长的示例,它不使用 std::vector<bool>,并且使用自定义容器类型,删除了分配和复制/移动,仍然显示问题。

我了解幕后发生的事情,operator[] 返回的代理类旨在实现allTheData[1] = true和相关功能,编写的客户端代码好像正在读取值实际上将代理存储在 boolValue 中,然后当客户端稍后修改它认为是 bool 的内容,而是修改原始源数据。TLDR: 'auto' 复制了代理。

代码做了程序员告诉它要做的事情,而不是程序员的意思。

如果程序员想要 boolValue 的更改来更新源数据,他们会这样做auto& boolValue = ...,这适用于operator[]返回的实现T&,而不是那些需要伪造类似引用行为的自定义代理。

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

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

我宁愿移除地雷也不愿一直绕过它,并张贴“不要使用自动”或“始终使用常量”的标志,只是标记雷区,它不会移除它。

我怎样才能让我的图书馆不受这个陷阱的影响?(无需更改客户端代码!)

  • 首选是代码按书面方式工作 -assert(allTheData[1] == false)通过
    • 一种在写入 auto? 时定义代理衰减类型的方法。所以decltype(boolValue)bool
    • 优先于复制的隐式转换运算符?
    • 有没有其他方法可以在不更改上面的代码片段的情况下通过此操作?
  • 第二个偏好有没有办法使向变量编写代理成为编译错误?
    • 我将复制和移动构造函数声明为删除,并将移动和复制赋值运算符声明为删除。还是编译。
    • 无论如何声明一个类不能成为左值?
  • 提议的 C++ 未来标准中是否有任何内容可以解决此问题?

还有一个问题是代码如下:

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

我在这里只使用矢量,因为它是一个非常简单的问题示例。这不是向量的错误,这是代理类型模式的错误,其中向量是显示问题的示例。

奇怪的是,MSVC 的 Intellisense 引擎有时会将复制 no-move-no-copy 代理类型报告为编译错误,但无论如何编译它

在此处输入图片说明
如果这个智能感知编译错误是一个真正的编译错误,那就太好了。叹

use*_*705 67

通过在代理类的 operator= 末尾添加“&&”来减少损害

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

我花了很多时间尝试,但我最终找到了一种方法来缓解最常见的问题,这会收紧它,因此您仍然可以复制代理,但是一旦将其复制到堆栈变量中,就无法修改它无意中损坏了源容器。

#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.
}
Run Code Online (Sandbox Code Playgroud)

  • 这看起来只能解决一个方向的问题。如果 vector&lt;bool&gt; 使用了您的修复,那么您问题中的代码将是安全的,但是 `vector&lt;bool&gt; v = {false}; 自动 b = v[0]; v[0] = 真;assert(!b);` 仍然会爆炸。 (4认同)
  • @Jose 对“b”的所有操作都需要“&amp;&amp;”限定。那么 `assert(!b)` 将无法编译。 (3认同)
  • @spectras:但是没有人会写`someFunc() = false`,除非他们知道这是一个代理并且想要他们将获得的行为。回到“vector&lt;bool&gt;”示例,“vector&lt;bool&gt;”示例中的“someFunc() = false”的类似物是“v[0] = false”,我们*希望*允许这样做。 (3认同)
  • 该功能的名称是什么?我从未见过它被使用过,并且想阅读有关它的内容。 (2认同)
  • @oarfish 这样的成员函数被认为是*rvalue-ref-qualified*。 (2认同)

bol*_*lov 60

Yes, this is indeed a problem. Afaik there is no solution to this in current C++ (C++20 at the time of writing) other than to change the code at the calling site which is not ideal.

There is the proposal P0672R0 Implicit Evaluation of “auto” Variables (from 2017) which tries to deal with this exact problem. It uses as example proxy classes in math libraries just like your case and gives example with std::vector<bool> just like you. And it gives more examples of problems that arrive from this pattern.

The paper proposes 3 solutions to this, all implemented in language:

The discussion at the standard committee meetings strongly favored the using declaration solution. However I wasn't able to find any more updates on this paper so I personally don't see this paper or something similar being implemented in the language in the near future.

So, unfortunately, for now your only solution is to educate your users on the proxy classes your library uses.


Hol*_*Cat 34

我有一个模糊的想法,不知道它有多实用。它不会覆盖auto推导出的内容(这似乎不可能),而只会导致将代理复制到格式错误的变量。

  • 使代理不可复制。

  • auto由于强制 RVO,仅凭这一点并不能阻止您将代理保存到变量中。为了抵消它,您可以通过引用返回代理。

  • 为避免获得悬空引用,您在默认函数参数中构造代理,该参数与常规参数具有相同的生命周期 - 直到完整表达式结束。

  • 用户仍然可以保存对代理的引用。为了使这种滥用更加困难,您返回一个右值引用,并&&限定所有成员函数。

  • 这可以防止与悬空代理引用的任何交互,除非您std::move这样做。这应该足够模糊以阻止您的用户,但如果不是,您将不得不依赖一些消毒剂或在代理的析构函数中设置一个(易失性?)标志,并在每次访问它时检查它(这是 UB ,但应该足够可靠)。

例子:

namespace impl
{
    class Proxy
    {
        Proxy() {}
      public:
        static Proxy construct() {return {};}
        Proxy(const Proxy &) = delete;
        Proxy &operator=(const Proxy &) = delete;
        int *ptr = nullptr;
        int operator=(int value) && {return *ptr = value;}
    };
}

impl::Proxy &&make_proxy(int &target, impl::Proxy &&proxy = impl::Proxy::construct())
{
    proxy.ptr = &target;
    return std::move(proxy);
}
Run Code Online (Sandbox Code Playgroud)

然后:

int x = 0;
make_proxy(x) = 1; // Works.
auto a = make_proxy(x); // error: call to deleted constructor of 'impl::Proxy'
auto &b = make_proxy(x); // error: non-const lvalue reference to type 'impl::Proxy' cannot bind to a temporary of type 'impl::Proxy'
const auto &c = make_proxy(x); // Compiles, is a dangling reference. BUT!
c = 2; // error: no viable overloaded '='
auto &&d = make_proxy(x); // Compiles, is a dangling reference.
d = 3; // error: no viable overloaded '='
std::move(d) = 2; // Compiles, causes UB. Needs a runtime check.
Run Code Online (Sandbox Code Playgroud)

使用重载运算符更难实现这一点,它(除了())不能有默认参数。但仍然可行:

namespace impl
{
    struct Index
    {
        int value = 0;
        Proxy &&proxy;
        Index(int value, Proxy &&proxy = Proxy::construct()) : value(value), proxy(std::move(proxy)) {}
        Index(const Index &) = delete;
        Index &operator=(const Index &) = delete;
    };
}

struct B
{
    int x = 0;
    impl::Proxy &&operator[](impl::Index index)
    {
        index.proxy.ptr = &x;
        return std::move(index.proxy);
    }
};
Run Code Online (Sandbox Code Playgroud)

唯一的缺点是,由于任何参数最多允许一个用户定义的隐式转换,这operator[]仅适用于int参数,而不适用于带有operator int.