1

It's well known that operator= should return a const reference to *this to support chaining, but this only works if *this can be used as an rvalue is value-like.

Edit: Fine, operator= should return a non-const reference (do as the ints), and I meant that *this needs to be a meaningful rhs in an assignment.

I'm wrapping a C API of name=value setter functions through a C++ class ApiWrapper with operator[] returning a temporary write-only Proxy with overloaded operator=, but the API has no getter functions so Proxy is effectively write-only.

ApiWrapper x;
x["a"] = x["b"] = 42;  // x["b"] = 42;      fine:  consumes 42, returns *this
                       // x["a"] = x["b"];  error: x["b"] does not have the value

It seems to me that if I return a const reference to rhs instead of *this from operator=, chaining would work fine. Conceptually (proxy boilerplate code left out):

struct Proxy {
    template <typename T>
    T const& operator=(T const& rhs) const
    {
        ...         // pass rhs to the API but don't store it
        return rhs; // return rhs, not *this
    }
};

ApiWrapper x;
x["a"] = x["b"] = 42;  // x["b"] = 42;   fine: consumes and returns 42
                       // x["a"] = 42;   fine: consumes and returns 42

This makes me suspicious though. Are there any weird side effects from returning a const reference to rhs instead of *this? The only thing I can think of is that I won't be able to use it in expressions like (x["a"] = 42).doSomething() but my Proxy cannot support anything like that anyway, since it is write-only. Or would it be better to just disallow chaining (e.g. by returning void)?

Edit: Even if Proxy is not value-like, I think supporting assignment makes sense, it allows syntactic sugar like:

// this:                          // rather than:
ApiWrapper w;                     API * ptr = make_api_instance();
w["name"] = "Batman";             api_set_str(ptr, "name", "Batman");
w["age"]  = 42;                   api_set_int(ptr, "age", 42);
w["pi"]   = 3.14;                 api_set_double(ptr, "pi", 3.14);
Anders Johansson
  • 3,926
  • 19
  • 19
  • The copy assignment operator should return a non-const reference. – Captain Obvlious May 25 '13 at 12:05
  • 1
    How can `operator =` be `const`? – Andy Prowl May 25 '13 at 12:05
  • Your opening statement doesn't really make sense. First off, assignment can't be `const` and neither should the return value be. Second, `*this` is *always* an lvalue (since it's dereferencing a pointer), and there's always the standard lvalue-to-rvalue conversion. – Kerrek SB May 25 '13 at 12:06
  • @CaptainObvlious, if `operator=` returns a non-const reference, you allow `(a=b)=c;` , see eg http://stackoverflow.com/questions/4706690/c-why-the-assignment-operator-should-return-a-const-ref-in-order-to-avoid-a-b. – Anders Johansson May 25 '13 at 12:07
  • @AndersJohansson Which is *exactly* what it's supposed to do. – Captain Obvlious May 25 '13 at 12:08
  • @AndyProwl, I made `operator=` const because it is a proxy; the proxy itself does not change through assignment. – Anders Johansson May 25 '13 at 12:08
  • @KerrekSB, the point is that the proxy does not have anything to return from a conversion operator, therefore there cannot be any. – Anders Johansson May 25 '13 at 12:12
  • I'm not sure now what your question is. Your second piece of code more or less seems to work. What's the problem? – Kerrek SB May 25 '13 at 12:13
  • And why can't you give the proxy class itself a sensible copy-assignment operator? – Kerrek SB May 25 '13 at 12:14
  • @KerrekSB Because "it's well known the assignment operator returns a const reference" ;) – Captain Obvlious May 25 '13 at 12:15
  • @CaptainObvlious: How could I forget :-) – Kerrek SB May 25 '13 at 12:17
  • I'm just asking whether returning `rhs` makes sense at all or if I'm overlooking something major. A proxy copy constructor/assignment operator wouldn't work because the only thing it could do would be to make `x["a"]` refer to the same underlying API property as `x["b"]` which is not what is intended by chaining them. – Anders Johansson May 25 '13 at 12:17
  • @AndersJohansson: Well, if your objects are fundamentally not value-like, i.e. if it doesn't make sense to "give one the semantics of another" (like a thread or a network socket), then you probably shouldn't allow *any* assignment or copy operations. – Kerrek SB May 25 '13 at 12:19
  • @AndersJohansson Any member function that takes a const-ref parameter and returns that same const-ref parameter is subject to **undefined behavior**. Just don't do it. – Captain Obvlious May 25 '13 at 12:22
  • @CaptainObvlious, thanks! That's a very good point I hadn't thought of - if the const-ref parameter is a temporary, it won't persist after I return it. – Anders Johansson May 25 '13 at 12:27
  • @CaptainObvlious Why exactly is that? – jogojapan May 25 '13 at 13:27
  • @jogojapan Because as Anders mentioned you can pass in a temporary object that gets destroyed after the function returns leaving you with a dangling reference. – Captain Obvlious May 25 '13 at 13:29
  • @CaptainObvlious But that's the responsibility of the person who passes it a temporary. – jogojapan May 25 '13 at 13:32
  • 1
    @jogojapan IMHO that's a careless attitude and one reason UB happens as often as it does. If you _need_ to pass a temporary add a move assignment operator. – Captain Obvlious May 25 '13 at 13:35
  • @CaptainObvlious Sure, I didn't say it's good design. But returning a const-reference that you were passed doesn't as such cause undefined behaviour. The caller will receive a reference to an object that is still alive (because the object lives within the entire full-expression of which the function call is a part). The object dies at the end of that full-expression, not automatically after the return from the function. – jogojapan May 25 '13 at 13:38
  • @jogojapan All I said was it is subject to UB. The point is the developer needs to be aware that the assignment operator can return a reference to a temporary. It's counter-intuitive to the semantics of the operator and requires explicit knowledge of the different. I have a feeling you know better than to do this ;) – Captain Obvlious May 25 '13 at 14:07
  • @CaptainObvlious Sure, sure, no disagreement there... I was just irritated by your statement, which seemed to indicate that returning a const-reference as passed is itself UB. – jogojapan May 25 '13 at 14:11

2 Answers2

1

I think the cleanest solution would be to stick with the standard idioms. If you make your proxy class copy-constructible and copy-assignable in the usual way, this should work. Something like this:

struct Proxy
{
    Proxy(Proxy const & rhs)
    : // ...
    {
        // copy internal state of rhs
    }

    Proxy & operator=(Proxy const & rhs)
    {
        // copy internal state of rhs
        return *this;
    }

    template <typename T>
    Proxy & operator=(T const & rhs)
    {
        // ... perform T-specific operations ... #1
        return *this;
    }
};

An additional benefit is that whatever "generic logic" has to be performed by the first assignment at #1 doesn't need to be repeated in every subsequent assignment.

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • The internal state of the proxy is just the parent API wrapper and the name of the property. With the above `x["name"] = x["address"]` would make the proxy for "name" refer to the same property as the proxy for "address" does, which is not correct. – Anders Johansson May 25 '13 at 12:24
  • He is a bit unclear, but I interpreted this as adding an extra variable to ``Proxy``, which would store the last assigned value. When assigning a proxy to a proxy, the stored value in the RHS proxy (42 in this case) would then be used. It is a bit redundant (42 being stored both in Proxy and in the underlying api. – amaurea May 25 '13 at 12:36
  • @amaurea, nice idea but it counteracts the point of the proxy (which is to accept assignment of any type and call the correct API function depending on that type). To store the value, `Proxy` would have to be a template class instead of `operator=` being a template function, and `ApiWrapper::operator[]` would have to return `Proxy` objects of the correct type. Alternatively use `boost::any` or `void*` or something ugly like that. – Anders Johansson May 25 '13 at 12:47
0

I think your approach makes sense. Just to check if I understand your problem correctly, the struct could look something like this:

struct Proxy {
    template <typename T>
    T const& operator=(T const& rhs) const
    {
        send_to_abi(rhs);
        return rhs;
    }
};

As you say, since Proxy does not store rhs anywhere, and I'm assuming no receive_from_abi function exists, then returning *this will not work - the number would not be propagated in this case. As the comments point out, some behaviors such as (a=3)=3 will not work, but that isn't really surprising.

Edit: As the comments point out, this approach is dangerous if rhs is a temporary. This can be fixed by returning a copy instead:

struct Proxy {
    template <typename T>
    T operator=(T const& rhs) const
    {
        send_to_abi(rhs);
        return rhs;
    }
};

This might appear expensive a["a"] = a["b"] = a["c"] = foo looks like it would involve 3 copies. But these should be avoidable through a common compiler optmimization.

amaurea
  • 4,950
  • 26
  • 35
  • `const Proxy& a = b = Proxy(); a.BlowUp();` – Captain Obvlious May 25 '13 at 12:29
  • Yes that is exactly it - `rhs` is passed to another function but not stored and not retrievable later. However Captain Obvlious is right, I can't return rhs if it's a temporary. – Anders Johansson May 25 '13 at 12:30
  • Yes, he's right. You could return a copy, which would not necessarily be expensive. The other solution also involves copies, but it is easier for the compiler to optimize copies when returning than internally stored copies when they occur in this kind of chain, I think. So how about using this approach, but changing the signature to ``T operator=(T const& rhs) const``? Of course, that would not work for objects without a copy constructor. – amaurea May 25 '13 at 12:42
  • Accepting this answer since it is more in line with the spirit of what I'm asking for in the question (though after the discussion, I'm leaning more toward just disallowing chaining instead). Thanks! – Anders Johansson May 26 '13 at 13:25