Search code examples
c++c++11templatesoperator-overloadingassignment-operator

Checking for self-assignment when overloading operator= for template class of generic type


Usually, when overloading and assign operator, one should check for self-assignment. In a simple non-templated class it would like the following:

MyClass& MyClass::operator=(const MyClass& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

However, suppose that MyClass is templated, and that we want to generalize the assignment operator overloading for generic types that that the class can have:

template<class T>
class MyClass
{
    template<class U>
    friend class MyClass;
    T value;

    template<class U>
    MyClass<T>& operator=(const MyClass<U>& rhs)

    //... other stuff
}

template<class T>
template<class U>
MyClass<T>& MyClass<T>::operator=(const MyClass<U>& rhs)
{
    if (this != &rhs) { //this line gives an error
        value = (T)rhs.value;
    }
}

In the above case, the line if (this != &rhs) would give a compiler error. In MS Visual Studio 2015, it is the error C2446:

'==': no conversion from 'const MyClas< T > *' to 'MyClass< U > *const '

Therefore, how could I implement the checking for self-assignment when working with an assignment operator that can take at the right hand side an instance of MyClass of generic templated type?


Solution

  • same_object is a function that takes two references, and returns true if they both refer to the same object; not the same address, but the same object.

    template<class T, class U, class=void>
    struct same_object_t {
      constexpr bool operator()(T const volatile&, U const volatile&)const{return false;}
    };
    template<class T>
    struct same_object_t<T,T,void> {
      bool operator()(T const volatile& lhs, T const volatile& rhs)const{
        return std::addressof(lhs) == std::addressof(rhs);
      }
    };
    template<class T, class U>
    struct same_object_t<T,U,
      typename std::enable_if<
        std::is_base_of<T, U>::value && !std::is_same<T,U>::value
      >::type
    >:
      same_object_t<T,T>
    {};
    template<class T, class U>
    struct same_object_t<T,U,
      typename std::enable_if<
        std::is_base_of<U, T>::value && !std::is_same<T,U>::value
      >::type
    >:
      same_object_t<U,U>
    {};
    template<class T, class U>
    constexpr bool same_object(T const volatile& t, U const volatile& u) {
      return same_object_t<T,U>{}(t, u);
    }
    
    template<class T>
    template<class U>
    MyClass<T>& MyClass<T>::operator=(const MyClass<U>& rhs)
    {
      if (!same_object(*this, rhs)) {
        value = static_cast<T>(rhs.value);
      }
      return *this;
    }
    

    Live example.

    Two distinct objects can share an address due to unions and standard layout "first member" address sharing, as well as an array and the first element of the array. Those cases return false from same_object.

    private/protected inheritance can break this, as can a type U that inherits from a type T through more than one path.