4

After many years of using C++ I realized a quirk in the syntax when using custom classes. Despite being the correct language behavior it allows to create very misleading interfaces.

Example here:

class complex_arg {
    double r_;
    double phi_;
 public:
    std::complex<double> value() const {return r_*exp(phi_*std::complex<double>{0, 1});}
};

int main() {
    complex_arg ca;
    ca.value() = std::complex<double>(1000., 0.);  // accepted by the compiler !?
    assert( ca.value() != std::complex<double>(1000., 0.) ); // what !?
}

https://godbolt.org/z/Y5Pcjsc8d

What can be done to the class definition to prevent this behavior? (Or at the least flag the user of the clas that the 3rd line is not really doing any assignment.)

I see only one way out, but it requires modifying the class and it doesn't scale well (to large classes that can be moved).

    const std::complex<double> value() const;

I also tried [[nodiscard]] value() but it didn't help.

As a last resort, maybe something can be done to the returned type std::complex<double> ? (that is, assuming one is in control of that class)


Note that I understand that sometimes one might need to do (optimized) assign to a newly obtained value and passe it to yet another function f( ca.value() = bla ). I am not questioning this usage per se (although it is quite confusing as well); I have the problem mostly with ca.value() = bla; as a standalone statement that doesn't do what it looks.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • 1
    "_As a last resort, maybe something can be done to the returned type_": Just `&`-qualify `operator=`. I think that is also the only sensible approach. – user17732522 May 20 '22 at 07:03
  • @user17732522, that works! What also works is to add `[[nodicard]]` for the unqualified assignment. In both cases I couldn't use `= default`, which makes me very sad :(. See here https://godbolt.org/z/cfq84Gq3K. Although it is even sadder that I have to change underlying classes that I cannot control. – alfC May 20 '22 at 07:27
  • 1
    I don't see any reason that `= default` shouldn't work. I think that is just a bug. The latest GCC compiles it fine. Right, `[[nodiscard]]` should also at least give a warning. It will not affect overload resolution in contrast to my suggestion and still allow for the use case you mentioned at the end. On the other hand it will not detect an unintentional `=` instead of `==` e.g. in `if(ca.value() = bla)`, although compilers tend to have extra warnings for that. – user17732522 May 20 '22 at 07:32
  • 1
    The bug is actually not related to the `&` qualifier, but the `auto` return type. Give it the idiomatic one and the older GCC also compiles it. – user17732522 May 20 '22 at 07:36
  • 1
    There is still a `&` missing on the return type. – user17732522 May 20 '22 at 07:46
  • 1
    Third time is a charm: https://godbolt.org/z/qqhYv9Y1Y – alfC May 20 '22 at 07:47
  • 1
    I would have thought gcc would already warn about assigning to a temporary. – Goswin von Brederlow May 20 '22 at 16:03

1 Answers1

5

Ordinarily we can call a member function on an object regardless of whether that object's value category is an lvalue or rvalue.

What can be done to the class definition to prevent this behavior?

Prior to modern C++ there was no way prevent this usage. But since C++11 we can ref-qualify a member function to do what you ask as shown below.

From member functions:

During overload resolution, non-static member function with a cv-qualifier sequence of class X is treated as follows:

  • no ref-qualifier: the implicit object parameter has type lvalue reference to cv-qualified X and is additionally allowed to bind rvalue implied object argument

  • lvalue ref-qualifier: the implicit object parameter has type lvalue reference to cv-qualified X

  • rvalue ref-qualifier: the implicit object parameter has type rvalue reference to cv-qualified X


This allows us to do what you ask for a custom managed class. In particular, we can & qualify the copy assignment operator.

struct C
{
    C(int)
    {
      std::cout<<"converting ctor called"<<std::endl;
    }
    C(){
        std::cout<<"default ctor called"<<std::endl;
    }
    C(const C&)
    {
     std::cout<<"copy ctor called"<<std::endl;
    }
//-------------------------v------>added this ref-qualifier
    C& operator=(const C&) &
    {
    std::cout<<"copy assignment operator called"<<std::endl;;
    return *this;
    }

};
C func()
{
    C temp;
    return temp;
}

int main()
{
//---------v---------> won't work because assignment operator is & qualified
    func() = 4;
}
Jason
  • 36,170
  • 5
  • 26
  • 60
  • Thank you! So, no hope to do it without modifying the returned class? I guess I will use `const` returned type as long as the type is a POD. – alfC May 20 '22 at 07:55
  • If you are curious why I need this, look how I define "`conj`" in the second code block in this section https://gitlab.com/correaa/boost-multi#special-memory-allocators-and-fancy-pointers (note the return type). If I don't put that `const` I get that the "view" array can be assigned although it is not mutable (the code `Ah[1][0] = ...;` would be allowed which doesn't do anything). – alfC May 20 '22 at 07:55
  • @alfC Unfortunately no.(not without modifying the returned class). But hey atleast we now have a way! – Jason May 20 '22 at 08:02