Search code examples
c++move

Why move-assignment is called?


I have some class with copy and move assignment, but move seems to be wrong in my example and leads to unecpected behavior. Why is move called and how can I avoid this? C1 is assigned to C2 and used afterwards, but move is called and C1 is empty then.

#include <iostream>

class CSomeClass
{
protected:
   size_t m_uiSize = 0u;

public:
   CSomeClass() {}

   ~CSomeClass() {}

   size_t size() const { return m_uiSize; }

   void resize( size_t p_uiNewSize ) { m_uiSize = p_uiNewSize; }

   /* This operator I was expected to be called in all cases. */
   CSomeClass& operator=( const CSomeClass& p_rzOther )
   {
      std::wcout << "Copy explicit" << std::endl;
      m_uiSize = p_rzOther.size();
      return *this;
   }

   CSomeClass& operator=( CSomeClass&& p_rzOther )
   {
      std::wcout << "Move explicit" << std::endl;
      m_uiSize = p_rzOther.size();
      p_rzOther.resize( 0u );
      return *this;
   }

#if 1
   template<typename M> CSomeClass& operator=( const M& p_rzOther )
   {
      std::wcout << "Copy UNDEF" << std::endl;
      m_uiSize = p_rzOther.size();
      return *this;
   }

   template<typename M> CSomeClass& operator=( M&& p_rzOther )
   {
      std::wcout << "Move UNDEF" << std::endl;
      p_rzOther.resize( 0u );
      return *this;
   }
#endif
};


int main()
{
   CSomeClass C1;
   CSomeClass C2;

   C1.resize( 1u );

   std::wcout << L"C1 size before: " << C2.size() << std::endl;

   C2 = C1;

   std::wcout << L"C1 size after: " << C2.size() << std::endl;

   return 0;
}

This results in the following output:

C1 size before: 1
Move UNDEF
C1 size after: 0

My real problem is a bit more complicated (with more templates and a large range of assignment variants).

If the #if 1 is changed to #if 0, the correct copy assignment operator is called, but in my real code, there are cases where non of the assignment operators are called (instead there is done a plain copy which is wrong, too).

I hope you can explain the mechanism to me. What am I missing?


Solution

  • template<typename M> CSomeClass& operator=( M&& p_rzOther )
    

    Here, M&& p_rzOther is a forwarding reference. You can pass both lvalues and rvalues to it, both const and non-const.

    In your case, M gets deduced as CSomeClass &, which, due to the reference collapsing turns the assignment operator into:

    CSomeClass &operator=(CSomeClass &p_rzOther)
    

    Because in C2 = C1;, C1 is not const, the operator above is a better match than two other assignment operators that take a const CSomeClass &.

    You can solve this with SFINAE, by preventing M from being CSomeClass (possibly cv-qualified, possibly a reference to one):

    template <
        typename M,
        std::enable_if_t<
            !std::is_same_v<
                CSomeClass,
                std::remove_cv_t<std::remove_reference_t<M>>
            >,
            decltype(nullptr)
        > = nullptr
    >
    CSomeClass &operator=(M &&p_rzOther)
    

    And since this operator= can handle both value categories with and without const, you don't need the other one. I suggest removing

    template<typename M> CSomeClass& operator=( const M& p_rzOther )
    

    to prevent it from conflicting with the other operators.