Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What type does the conversion logic target?

I don't understand why in the following code the expression C c3 = 5 + c; doesn't get compiled although 5 could be converted to type C as in the previous statement.

#include <iostream>

class C 
{
    int m_value;
public:
    C(int value): m_value(value) {} ;

    int get_value() { return m_value; } ; 

    C operator+(C rhs) { return C(rhs.m_value+m_value); }
};

int main()
{
    C c = 10;
    C c2 = c + 5; // Works fine. 5 is converted to type C and the operator + is called
    C c3 = 5 + c; // Not working: compiler error. Question: Why is 5 not converted to type C??

    std::cout << c.get_value() << std::endl; // output 10
    std::cout << c2.get_value() << std::endl; // output 15

}



like image 416
Soulimane Mammar Avatar asked Feb 13 '19 09:02

Soulimane Mammar


2 Answers

Because if overload operator as member function of the class, it could only be called when the object of that class is used as left operand. (And the left operand becomes the implicit *this object for the member function to be called.)

Binary operators are typically implemented as non-members to maintain symmetry (for example, when adding a complex number and an integer, if operator+ is a member function of the complex type, then only complex+integer would compile, and not integer+complex).

From the standard, [over.match.oper]/3

(emphasis mine)

For a unary operator @ with an operand of a type whose cv-unqualified version is T1, and for a binary operator @ with a left operand of a type whose cv-unqualified version is T1 and a right operand of a type whose cv-unqualified version is T2, four sets of candidate functions, designated member candidates, non-member candidates, built-in candidates, and rewritten candidates, are constructed as follows:

  • (3.1) If T1 is a complete class type or a class currently being defined, the set of member candidates is the result of the qualified lookup of T1::operator@ ([over.call.func]); otherwise, the set of member candidates is empty.

That means if the type of left operand is not a class type, the set of member candidates is empty; the overloaded operator (as member function) won't be considered.

You can overload it as a non-member function to allow the implicit conversion for both left and right operands.

C operator+(C lhs, C rhs) { return C(lhs.get_value() + rhs.get_value()); }

then both c + 5 or 5 + c would work fine.

LIVE

BTW: This will cause one temporaray object being constructed (from int to C) for the non-member function to be called; if you care about that, you can add all the three possible overloads as follows. Also note that this is a trade-off issue.

C operator+(C lhs, C rhs) { return C(lhs.get_value() + rhs.get_value()); }
C operator+(C lhs, int rhs) { return C(lhs.get_value() + rhs); }
C operator+(int lhs, C rhs) { return C(lhs + rhs.get_value()); }

And here're some suggestions about when to use a normal, friend, or member function overload.

In most cases, the language leaves it up to you to determine whether you want to use the normal/friend or member function version of the overload. However, one of the two is usually a better choice than the other.

When dealing with binary operators that don’t modify the left operand (e.g. operator+), the normal or friend function version is typically preferred, because it works for all parameter types (even when the left operand isn’t a class object, or is a class that is not modifiable). The normal or friend function version has the added benefit of “symmetry”, as all operands become explicit parameters (instead of the left operand becoming *this and the right operand becoming an explicit parameter).

When dealing with binary operators that do modify the left operand (e.g. operator+=), the member function version is typically preferred. In these cases, the leftmost operand will always be a class type, and having the object being modified become the one pointed to by *this is natural. Because the rightmost operand becomes an explicit parameter, there’s no confusion over who is getting modified and who is getting evaluated.

like image 146
songyuanyao Avatar answered Oct 13 '22 15:10

songyuanyao


You are facing the reason to define certain operator overloads as free functions, i.e., when implicit conversions are desired. To see what's going on under the hood, consider the verbose form of operator overload invocations:

C c2 = c.operator+(5); // Ok, c has this member function
C c3 = 5.operator+(c); // No way, this is an integer without members

You can obviously do is an explicit C construction as in

C c3 = C{5} + c;

but this is not intended for an arithmetic value type like C. To make the implicit construction possible, define the overload as a free function

auto operator + (C lhs, const C& rhs)
{
    lhs += rhs;
    return lhs;
}

Now, there is no restriction of the left hand side operand. Note that the operator is implemented in terms of += (you would have to implement it to make the above code compile), which is good practice as pointed out in this thread: when you provide a binary operator + for a custom type, users of that type will also expected operator += to be available. Hence, to reduce code duplication, it's usually good to implement + in terms of += (same for all other arithmetic operands).

Further note that these operands often require a substantial amount of boilerplate code. To reduce this, consider e.g. the Boost operators library. To generate all standard arithmetic operators based on the minimal amount of actual hand-written code:

#include <boost/operators.hpp>

class C : private boost::arithmetic<C>
//                ^^^^^^^^^^^^^^^^^^^^
//                where the magic happens (Barton-Nackmann trick)
{
   int m_value ;

   public:
     C(int value): m_value(value) {} ;

     C& operator+=(const C& rhs) { m_value += rhs.m_value; return *this; }
     C& operator-=(const C& rhs) { m_value -= rhs.m_value; return *this; }
     C& operator*=(const C& rhs) { m_value *= rhs.m_value; return *this; }
     C& operator/=(const C& rhs) { m_value /= rhs.m_value; return *this; }
     const C& operator+() const { return *this; }
     C operator-() const { return {-m_value}; }

     int get_value() { return m_value; } ;
};
like image 30
lubgr Avatar answered Oct 13 '22 13:10

lubgr