nikitautiu
nikitautiu

Reputation: 982

Template overload resolution and implicit conversions

Wanting to try my hand, I recently decided to try and implement a templated complex number class in C++. As a reference I used the C++ 11 standard library implementation, and I came across, a difference between that implementation and mine. It refers to how they overloaded the += operator in their class.

In mine I basically have a single += method, that is able to handle both Complex<T> += Complex<U> and Complex<T> += whatever other type that implicitly converts to T and thus Complex<T>. For clarification here is the class declaration:

template <typename T>
class Complex {
    public:
        constexpr Complex (T=T(), T=T()); // added for clarity

        template <typename U>
        constexpr Complex (const Complex <U>&);
        template <typename U>
        constexpr Complex (Complex <U>&&);
        template <typename U>
        Complex <T>& operator= (const Complex <U>&);
        template <typename U>
        Complex <T>& operator= (Complex <U>&&);

        Complex& operator+= (const Complex<T>&);
        Complex& operator-= (const Complex<T>&);
        Complex& operator*= (const Complex<T>&); 
        Complex& operator/= (const Complex<T>&); 
        Complex& operator++ (void);
        Complex operator++ (int);
        Complex& operator-- (void);
        Complex operator-- (int);

        std::string to_string (void) const;

        constexpr T real (void);
        constexpr T img (void);
        void real (const T&);
        void img (const T&);

    private:
        T _m_real, _m_img;
        template <typename U>
        friend class Complex;
};

However in the standard library implementation, they use 2 overloads for operator+=, one that takes a Complex<U> and another one that takes T. Both implementations seem to yield the same behaviour as far as I tested: addition between any two complex types or a complex and another type that implicitly converts to that of the complex's internal.

So, my question is: Is there any reason for a separate operator+=(T) other than optimizing away a temporary complex and why use a nestde template <typename U> if all other complex types implicitly convert to Complex?

Upvotes: 1

Views: 249

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275966

struct Foo {
  operator int()const {
    return 7;
  }
};`

You will have problems incrementing by Foo, as only one user defined conversion will be called.

As an example:

Complex<double> d;
Foo f;
d += f; // fails to compile with your version, compiles with C++ standard one

There is another difference. If we have a class Bar that has an operator Complex<double>() const, it will fail to work with the C++ standard version.

With your version, it works iff T is exactly double.

In short, your arguments being implicitly convertible to/from a particular type isn't the same as taking arguments of that type. Only one user defined conversion can be tried, so if you take int types that can convert to int are acceptable, and if you take a type that can be made from int, not all types that can convert to int are acceptable.

Non-user defined conversions are not restricted in the same way.

Upvotes: 1

The two interfaces that you are comparing are really:

// 1
Complex<T>& operator+=(const Complex<T>&);

// 2
template <typename X>
complex<T>& operator+=(const complex<X>&);
complex<T>& operator+=(const T&);

The two differences are that in 2, the overload taking a complex is templated on the type of the argument, and that there is an explicit overload that takes the T directly. Due to the existing (assuming that you added the missing constructor [*]) conversions both approaches will basically allow the same code to compile, but at an extra cost.

If you only want to add a value to the real part of the complex number, the first design requires the creation of a Complex<T> (assuming that you had that constructor, which is not shown), while in the second case no temporary object needs to be created.

Similarly, given as argument a different instantiation of the complex type, the implicit conversion would be used in 1 to create a temporary that would then be added, while in the second approach the argument would be bound directly without the need of a temporary.

While this is not important with fundamental types, it can be if the type used to instantiate the template is user defined and expensive to construct/copy. Consider a BigNum type that dynamically allocated memory, for example. The temporary would incur a memory allocation and deallocation.

[*] You don't allow default construction or construction with a single argument.

Upvotes: 1

Related Questions