Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make my class immune to the "auto value = copy of proxy" landmine in C++?

Reduce the damage by adding "&&" to the end of the proxy class's operator=

(And operator +=, -=, etc.)

Took me a lot of experimenting but I eventually found a way to mitigate the most common case of the problem, this tightens it so you can still copy the proxy, but once you've copied it to a stack variable, you can't modify it and inadvertently corrupt the source container.

#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.
}

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:

  • operator notation:

    class product_expr
    {
        matrix operator auto() { ... }
    };
    
  • using declaration:

    class product_expr
    {
        using auto = matrix;
    };
    
  • specialization of decay:

    make auto x= expr be defined as typename std::decay<decltype(expr)>::type x=expr; and then use used can specialize std::decay

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.


I have an obscure idea, not sure how practical it is. It doesn't override what auto deduces to (which seems impossible), but merely causes copying the proxy to a variable ill-formed.

  • Make the proxy non-copyable.

  • That alone doesn't stop you from saving the proxy to an auto variable, due to mandatory RVO. To counteract it, you return the proxy by reference instead.

  • To avoid getting a dangling reference, you construct the proxy in a default function argument, which have the same lifetime as regular arguments - until the end of a full expression.

  • User could still save a reference to the proxy. To make this kind of misuse harder, you return an rvalue-reference, and &&-qualify all member functions.

  • This prevents any interaction with the dangling proxy reference, unless you std::move it. This should be obscure enough to stop your users, but if not, you'll have to rely on some sanitizer or set a (volatile?) flag in the destructor of the proxy, and check it each time you access it (which is UB, but should be reliable enough).

Example:

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);
}

Then:

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.

This is harder to pull off with overloaded operators, which (except ()) can't have default arguments. But still doable:

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);
    }
};

The only downside is that since at most one user-defined implicit conversion is allowed for any argument, this operator[] will only work with int arguments, and not with classes with operator int.