incorrect function call on overload operator
Asked Answered
G

2

7

In the code, why is (10 != i) calling == instead of !=? The other two call !=

#include <iostream>

class Integer
{
  int x;

public:
  bool
  operator== (const Integer &i)
  {
    std::cout << "==";
    return x == i.x;
  }
  bool
  operator!= (const Integer &i)
  {
    std::cout << "!=";
    return x != i.x;
  }
  Integer (int t = 0) { x = t; }
};

int
main ()
{
  Integer i;
  std::cout << (i != i) << '\n';   // calls !=
  std::cout << (i != 100) << '\n'; // calls !=
  std::cout << (10 != i) << '\n';  // calls ==
}
Germayne answered 7/7, 2022 at 9:41 Comment(7)
You have not defined an operator for int != IntegerExact
well, how is my == working for the same then?Germayne
If there is no operator !=, the compiler might instead try !(a == b). A recent addition to the language.Jhelum
Added C++20 tag since it only compiles in that language version (although I've no idea why change of a != b to !(a == b) enables implicit conversion in this case)Stavropol
@Stavropol - The converting constructorSutherland
In C++20, 10 != i will invoke the operator!= synthesized by operator==, not the one you defined. see cppinsights.Poundal
In C++20 or later you don't need to define any operator!=. Just having operator== is enough to do the right thing (assuming you don't want any unusual semantics for !=). Simply don't declare a operator!=.Babushka
S
4

There are two new additions to C++20 that made this possible (note that your code doesn't compile in earlier standard versions).

  1. Compiler will attempt to replace a != b with !(a == b) if there is no suitable != for these arguments.

  2. If there is no suitable a == b, compiler will attempt b == a as well.

So, what happens - compiler first notices that it doesn't know how to compare 10 != i, so it tries !(10 == i). There is still no suitable comparison, so it tries !(i == 10) and it can finally be done using your implicit constructor to convert 10 to Integer.
It can be easily verified by adding more info to debug print:

  bool
  operator== (const Integer &i) const
  {
    std::cout << x << "==" << i.x << ' ';
    return x == i.x;
  }

will print 0==10 1 in the last line (see it online).


As noticed in comments, you don't even need operator !=, due to aforementioned behaviour C++20 compiler will automatically convert any such call to operator ==.

Stavropol answered 7/7, 2022 at 10:6 Comment(12)
I'm not exactly sure why implicit conversion to Integer is not considered by the compiler in case of 10 != i tho.Stavropol
It's not considered because to do so would be putting the cart before the horses. First we build a candidate set (including re-written ones), then do overload resolution, not the other way around. There is no operator!=(int, Integer) present to be a candidate.Sutherland
@StoryTeller-UnslanderMonica The question of Yksisarvinen is (I think), why operator!=(Integer, Integer) together with implicit conversion is not considered or chosen.Niemeyer
@Niemeyer - Which is addressed just the same by my comment, all the way back to C++98.Sutherland
@StoryTeller-UnslanderMonica There is no operator!=(Integer, int) either, and yet Integer::operator!=(const Integer&) is considered as candidate in this case. I explained in the answer why operator== could be used in 10 != i instead of operator != (which was the original question), but I don't know why operator!= couldn't be used in the first place.Stavropol
I have decided to accept this as this explains the order in which the compiler evaluates which is what I wanted to know, the other answer shows a different simplified approachGermayne
@Stavropol why does not !(10 == i) trigger the implicit conversion? what makes it different from !(i == 10), why does the order matter hereGermayne
@Germayne - It's not how compilers do it. Which is why the conversion seems odd in this model.Sutherland
@StoryTeller-UnslanderMonica I am just confused now, is the reasoning in this answer correct? as I do not understand the concept this answer seems to clarify the gaps, but if the explanation is wrong I would be happy to pick the other oneGermayne
@Germayne - This answer presents a mental model for the subject. Some find it helpful, but I'm of the opinion it just muddles one's understanding. It's not a case of "try this, now this, now that". Compilers look at 10 != i and try to find function candidates to call. Then they look at implicit conversion sequences for ranking the candidates (and some tie breakers). A member function is only a candidate when the lhs is of class type. That's how C++20's rewritten candidates bring members into the candidate set, and why a conversion won't happen to the lhs here.Sutherland
The phrasing here ("if there is no suitable") suggests that the compiler tries X and then, if that fails, tries Y. That's not what happens - the compiler tries X and Y simultaneously and picks the best one. There may not be an X or a Y (in which case it doesn't matter), but if both exist - they could still be ambiguous.Singletree
@StoryTeller-UnslanderMonica "A member function is only a candidate when the lhs is of class type.", so for choosing a member function, the lhs is never implicitly converted? But it could be for a free function?Niemeyer
O
10

Prior to C++20, you'd need to add two free functions for the comparison where the int is on the left-hand side:

bool operator==(int lhs, const Integer& rhs) {
    return rhs == lhs;
}

bool operator!=(int lhs, const Integer& rhs) {
    return rhs != lhs;
}

You should also make the member comparison operators const qualified:

class Integer {
public:
    //...
    bool operator==(const Integer &i) const { // note const
        std::cout << "==";
        return x == i.x;
    }
    bool operator!=(const Integer &i) const { // note const
        std::cout << "!=";
        return x != i.x;
    }
    //...
};

You could also remove the member operators to simplify things. Now the left-hand side int will be implicitly converted to Integer and then compared with the right-hand side:

class Integer {
    int x;

public:
    Integer(int t = 0) : x{t} {}

    friend bool operator==(const Integer& lhs, const Integer& rhs) {
        return rhs.x == lhs.x;
    }

    friend bool operator!=(const Integer& lhs, const Integer& rhs) {
        return !(rhs == lhs);
    }    
};

Since you've tagged this C++20, you can let operator== do all the work. See Default comparisons. It'll be used for operator!= too.

class Integer {
    int x;

public:
    bool operator==(const Integer &i) const {
        std::cout << "==";
        return x == i.x;
    }
    
    Integer(int t = 0) : x{t} {}
};

... and it'll correctly show that it used operator== for all your != comparisons (and negated it).

More from Defaulted equality comparison:

A class can define operator== as defaulted, with a return value of bool. This will generate an equality comparison of each base class and member subobject, in their declaration order. Two objects are equal if the values of their base classes and members are equal. The test will short-circuit if an inequality is found in members or base classes earlier in declaration order.

Per the rules for operator==, this will also allow inequality testing

This means that you will in fact get away with the below only since C++20:

class Integer {
    int x;

public:
    bool operator==(const Integer &i) const = default;
    
    Integer(int t = 0) : x{t} {}
};

or even better, get all the comparison operators for free by defaulting the spaceship operator <=>:

class Integer {
    int x;

public:
    auto operator<=>(const Integer &i) const = default;
    
    Integer(int t = 0) : x{t} {}
};
Obituary answered 7/7, 2022 at 9:57 Comment(4)
thanks for the answer, do you mind explaining why you used auto instead of bool for <=>?Germayne
@Germayne You're welcome! I'm using auto because its return type is "tricky". See: Three-way_comparisonObituary
I am sorry I had to not choose this as an answer, but I did upvote and for those who do not care about the internals, your solution is what makes sense to me, the other answer goes into details I was expecting as a replyGermayne
@Germayne Then I think you did absolutely right in accepting that answer. No need to feel bad about it! :)Obituary
S
4

There are two new additions to C++20 that made this possible (note that your code doesn't compile in earlier standard versions).

  1. Compiler will attempt to replace a != b with !(a == b) if there is no suitable != for these arguments.

  2. If there is no suitable a == b, compiler will attempt b == a as well.

So, what happens - compiler first notices that it doesn't know how to compare 10 != i, so it tries !(10 == i). There is still no suitable comparison, so it tries !(i == 10) and it can finally be done using your implicit constructor to convert 10 to Integer.
It can be easily verified by adding more info to debug print:

  bool
  operator== (const Integer &i) const
  {
    std::cout << x << "==" << i.x << ' ';
    return x == i.x;
  }

will print 0==10 1 in the last line (see it online).


As noticed in comments, you don't even need operator !=, due to aforementioned behaviour C++20 compiler will automatically convert any such call to operator ==.

Stavropol answered 7/7, 2022 at 10:6 Comment(12)
I'm not exactly sure why implicit conversion to Integer is not considered by the compiler in case of 10 != i tho.Stavropol
It's not considered because to do so would be putting the cart before the horses. First we build a candidate set (including re-written ones), then do overload resolution, not the other way around. There is no operator!=(int, Integer) present to be a candidate.Sutherland
@StoryTeller-UnslanderMonica The question of Yksisarvinen is (I think), why operator!=(Integer, Integer) together with implicit conversion is not considered or chosen.Niemeyer
@Niemeyer - Which is addressed just the same by my comment, all the way back to C++98.Sutherland
@StoryTeller-UnslanderMonica There is no operator!=(Integer, int) either, and yet Integer::operator!=(const Integer&) is considered as candidate in this case. I explained in the answer why operator== could be used in 10 != i instead of operator != (which was the original question), but I don't know why operator!= couldn't be used in the first place.Stavropol
I have decided to accept this as this explains the order in which the compiler evaluates which is what I wanted to know, the other answer shows a different simplified approachGermayne
@Stavropol why does not !(10 == i) trigger the implicit conversion? what makes it different from !(i == 10), why does the order matter hereGermayne
@Germayne - It's not how compilers do it. Which is why the conversion seems odd in this model.Sutherland
@StoryTeller-UnslanderMonica I am just confused now, is the reasoning in this answer correct? as I do not understand the concept this answer seems to clarify the gaps, but if the explanation is wrong I would be happy to pick the other oneGermayne
@Germayne - This answer presents a mental model for the subject. Some find it helpful, but I'm of the opinion it just muddles one's understanding. It's not a case of "try this, now this, now that". Compilers look at 10 != i and try to find function candidates to call. Then they look at implicit conversion sequences for ranking the candidates (and some tie breakers). A member function is only a candidate when the lhs is of class type. That's how C++20's rewritten candidates bring members into the candidate set, and why a conversion won't happen to the lhs here.Sutherland
The phrasing here ("if there is no suitable") suggests that the compiler tries X and then, if that fails, tries Y. That's not what happens - the compiler tries X and Y simultaneously and picks the best one. There may not be an X or a Y (in which case it doesn't matter), but if both exist - they could still be ambiguous.Singletree
@StoryTeller-UnslanderMonica "A member function is only a candidate when the lhs is of class type.", so for choosing a member function, the lhs is never implicitly converted? But it could be for a free function?Niemeyer

© 2022 - 2024 — McMap. All rights reserved.