3

I have the following code, which is a wrapper for POD into a template class Foo<T>, where T is the wrapped type (can be int, double etc). I define a templated conversion operator, and also the friend addition operator+, see code below. In the last line of main(), I define Foo<int> a = 10 then compute

cout << 2.1 + a << endl; // outputs 12

What is the rule here? It looks like the expression is being translated as

operator+(2.1, a)

which then becomes operator+(Foo<int>(2.1), a). Why doesn't the compiler try to first convert a into a double then perform the addition? I.e. why don't we have the expression evaluated as

2.1 + a.operator double()

Thanks!

PS: Just realized that clang++ fails to compile the code, saying that overloaded call to operator+ is ambiguous. However g++4.9 compiles it without problems, even with all warning flags turned on.

Code snippet below:

#include <iostream>
using namespace std;

template <typename T>
class Foo // wrapper class for a POD
{
    T val_; // this is the wrapped value
public:
    Foo(T val = {}): val_(val) {};

    template<typename S> // conversion operator
    operator S ()
    {
        std::cout << "Calling conversion operator" << std::endl;
        return val_;
    }

    // the += operator
    Foo& operator+=(const Foo& rhs)
    {
        val_ += rhs.val_; 
        return *this;
    }

    // the + operator
    friend Foo operator+(Foo lhs, const Foo& rhs)
    {
        cout << "Calling operator+" << endl;
        return lhs += rhs;
    }

    // stream operator
    friend std::ostream &operator<<(std::ostream &os, const Foo &rhs)
    {
        return os << rhs.val_;
    }
};

int main()
{
    Foo<int> a = 10;

    // operator+(2.1, a), why not
    // 2.1 + a. operator int() ?
    cout << 2.1 + a << endl; // outputs 12
}
vsoftco
  • 55,410
  • 12
  • 139
  • 252
  • [I'm not getting the same output.](http://rextester.com/FOVV97252) – David G Aug 03 '14 at 01:54
  • @0x499602D2 this becomes super interesting, I now realized that `clang++` fails to compare this code, saying that overloaded `operator+` is ambiguos: cannot differentiate between build-in `operator+` and `Foo::operator+`. On `g++4.9` it compiles and spits out 12. Just saw that you were using `clang++`, try `g++` and it will compile without any complain. – vsoftco Aug 03 '14 at 01:58
  • 1
    You got your terminology mixed up, [operator precedence](http://en.cppreference.com/w/cpp/language/operator_precedence) is not involved with user-defined conversion operators. Instead it's done with all other [implicit conversions](http://en.cppreference.com/w/cpp/language/implicit_cast) which is done independently from expression evaluation. – Some programmer dude Aug 03 '14 at 02:09
  • @JoachimPileborg ok, thanks, technically is not correct, however I really meant precedence, i.e. which operator is invoked first. Because here it looks like there is no conversion being performed, although the types are not compatible. So `operator+` seems to take precedence over a possible conversion from `Foo` to `double`. – vsoftco Aug 03 '14 at 02:12
  • g++'s treatment of templated conversion operators [is rather messed up](http://coliru.stacked-crooked.com/a/af523508d843ae2f). – T.C. Aug 03 '14 at 02:23
  • @T.C. cannot really understand what is `g++` trying to say (btw you have a typo in the code, `std::enable_if_t` should be `std::enable_if` – vsoftco Aug 03 '14 at 02:28
  • @vsoftco That's not a typo. g++ basically doesn't use the templated conversion operator, at all. – T.C. Aug 03 '14 at 02:29
  • @T.C. I see, that's why you deleted the `friend operator+`, to force a conversion. It is strange that it does use the conversion operator in other cases, like `double a = Foo(10)` for example. PS: didn't know about `enable_if_t`, what does it do? Cannot find it anywhere. – vsoftco Aug 03 '14 at 02:34
  • 1
    @vsoftco It's an alias for `typename std::enable_if*...*/>::type` added in C++14. – T.C. Aug 03 '14 at 02:35
  • 1
    @T.C. http://en.cppreference.com/w/cpp/types/enable_if – David G Aug 03 '14 at 02:35

2 Answers2

4

Let's start from §13.3.1.2 [over.match.oper]/p2-3:

If either operand has a type that is a class or an enumeration, a user-defined operator function might be declared that implements this operator or a user-defined conversion can be necessary to convert the operand to a type that is appropriate for a built-in operator. In this case, overload resolution is used to determine which operator function or built-in operator is to be invoked to implement the operator.

[...]

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, three sets of candidate functions, designated member candidates, nonmember candidates and built-in candidates, are constructed as follows:

  • 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@ (13.3.1.1.1); otherwise, the set of member candidates is empty.
  • The set of non-member candidates is the result of the unqualified lookup of operator@ in the context of the expression according to the usual rules for name lookup in unqualified function calls (3.4.2) except that all member functions are ignored. [...]
  • For the operator ,, the unary operator &, or the operator ->, the built-in candidates set is empty. For all other operators, the built-in candidates include all of the candidate operator functions defined in 13.6 that, compared to the given operator,
    • have the same operator name, and
    • accept the same number of operands, and
    • accept operand types to which the given operand or operands can be converted according to 13.3.3.1, and
    • do not have the same parameter-type-list as any non-member candidate that is not a function template specialization.

So, given the expression 2.1 + a, let's construct the candidate sets:

  • T1 is double, not a class type, so the set of member candidates is empty.
  • The non-member candidate set consists of:

    Foo<int> operator+(Foo<int> lhs, const Foo<int>& rhs);
    

    (plus lots of other overloads for different instantiations of Foo that's obviously a worse match than this one.)

  • The built-in candidate set consists of a long list of functions you can see in clang's output on your code, specified in §13.6 [over.built]/p12:

    For every pair of promoted arithmetic types L and R, there exist candidate operator functions of the form [...] LR operator+(L , R ); [...] where LR is the result of the usual arithmetic conversions between types L and R.

Overload resolution can succeed only if a unique best match can be found among this pile of candidates.

First, note that of the numerous possible built-in operators below, none can possibly be the unique best match, because Foo<int> is convertible to every possible type of the right operand:

operator+(double, unsigned long long)
operator+(double, unsigned long)
operator+(double, unsigned int)
operator+(double, __int128)
operator+(double, long long)
operator+(double, long)
operator+(double, float)
operator+(double, double)
operator+(double, long double)
operator+(double, int)

(I only listed ones whose first argument is of type double, since that's the type of the first argument, and hence the other built-ins can't possibly be better than any of those.)

Thus, overload resolution can succeed if and only if your overload of operator + is a better match than every one of them. Without loss of generality, we consider the following two functions:

    operator+(double, int);  // built-in
    Foo<int> operator+(Foo<int> lhs, const Foo<int>& rhs); // overload

given an argument list of (double, Foo<int>). For the first candidate, the first argument is an exact match, the second one requires a user-defined conversion. For the second candidate, the first argument requires a user-defined conversion, and the second one is an exact match.

We thus have a criss-cross situation, which means that neither candidate function is better than the other. (The first requirement of one function F1 being better than the other F2 is that for each argument the conversion required for F1 is not worse than F2 - §13.3.3 [over.match.best]/p2.)

As a result, there's no unique best overload, overload resolution fails, and the program is ill-formed. Clang is correct in rejecting this code, and g++ is broken in failing to reject it.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • great! thanks for pointing out the reference from the standard. Funny that a relatively simple code can cause so much headache to a compiler like g++ – vsoftco Aug 03 '14 at 03:44
  • Implicit typecasting is a source of many, many headaches, especially for compilers. – Mike DeSimone Aug 03 '14 at 04:07
1

I don't have the manual handy, but C++ prefers to implicitly typecast to objects over typecasting from objects. In other words, it'll interpret double + Foo<int> as Foo<int>(double) + Foo<int> if Foo<int> has a constructor that can take a double. ("Can take a double" allows implicit typecasting of the double to other things like int unless the constructor in question is declared explicit.)

If Foo<int> doesn't have a suitable constructor, only then would it consider calling Foo<int>::operator double() to degrade the object to a double... and I'm not even sure the language will even try that implicitly!

If you really want to make double + Foo<int> convert the Foo<int> to a double first, then add, you'll need to write:

double operator +(double a, const Foo<int>& b)
{
    return a + double(b);
}

or some kind of template equivalent. No friend declaration needed so long as Foo<int>::operator double() exists.

Mike DeSimone
  • 41,631
  • 10
  • 72
  • 96
  • Thanks! It doesn't implicitly trying to convert when I declare the ctor as explicit. You may be right, but then why does `clang++` fail to compile the code if this should be standard behaviour? – vsoftco Aug 03 '14 at 02:58
  • Thinking about it, I'm wondering if the preference for constructors over typecasts was some compilers' inference from the standard, assuming that typecast operators were second-class citizens. The standard itself probably didn't spell it out. So the clang programmers decided to force the programmer to be explicit, which is really the best course of action. – Mike DeSimone Aug 03 '14 at 03:07
  • Thanks, the point of friend templated conversion operator was so that one can easily convert among compatible types, without having to define `operator int`, `operator double` and so on. I agree that an explicit conversion is the way to go, however the code I posted is not so un-natural, and something very similar can be used for example to implement strong types (basically you add one more template parameter for the type tag). The fact that a `double` + `Foo` is then converted to `int` becomes a bit bothersome. – vsoftco Aug 03 '14 at 03:15
  • This is incorrect, as you can see by using a [non-template conversion operator in g++](http://coliru.stacked-crooked.com/a/65aa44e98d265f78), which correctly reports an ambiguity. It's just their handling of template conversion operators that is broken. – T.C. Aug 03 '14 at 03:53
  • @T.C. what exactly is incorrect? At the end of my previous comment I meant "...`double + Foo` is then converted to `Foo`..." – vsoftco Aug 03 '14 at 04:00
  • @vsoftco I was talking about the claim in the answer that "C++ prefers to implicitly typecast to objects over typecasting from objects". There's no such preference. – T.C. Aug 03 '14 at 04:01