Non-member overloaded copy assignment operator

Doc. no.: J16/04-0116= WG21/N1676
Date:     9 September 2004
Project:  Programming Language C++
Reply To: Bronek Kozicki <brok@rubikon.pl>

Contents

1. Motivation
2. Design and requirements
3. Proposal
  3.1 Only explicit classes
  3.2 Only classes where implicit copy assignment operator is not available
  3.3 Declaration only within class definition
4. Discussion
  4.1 Alternative solution

1. Motivation

Currently C++ Standard does not allow overloading of copy assignment operator as non-member function; overloaded assignment operator must be member function of user defined class. For this reason it's currently impossible to overload assignment operator to make it behave like assignment operator for built-in type. Overloaded assignment operator, like any other member function, may be called on rvalue (if accessible in place of call), while assignment operator for built-in type may not be called on rvalue. Ability to use only lvalue on left side of assignment operator is considered important feature of language, however for many programmers it's not obvious that this feature is provided only for built-in types. Following code demonstrates situation where this discrepancy is disturbing - program behaves against programmer's expectations:

#include <cstdio>

// some calculation goes here
int calc(int v)
{
  return v * 2;
}

template <typename T, typename V>
void f(T t, V v)
{
  // perform some calculation and
  // store result into reference returned by t()
  t() = calc(v);
}

class M
{
  struct K
  {
    int i;
    K(int i) : i (i) {}
  };
  // unable to disable assignment to rvalue of type K

  static K i;

public:
  static K& g1()
  {
    return i;
  }

  static K g2()
  {
    return i; // returning copy!
  }

  static int& g3()
  {
    return i.i;
  }

  static float g4()
  {
    return i.i; // returning copy!
  }

  static void show()
  {
    std::printf("K::i.i == %i\n", i.i);
  }
};

M::K M::i(1);

int main()
{
  f(M::g1, 30);    // OK
  M::show();       // 60

  f(M::g2, 31);    // should be an error, but compiles fine
  M::show();       // 60, which is unexpected output

  f(M::g3, 32);    // OK
  M::show();       // 64
  // n.f(M::g4, 34.43);    // error, as it should be
}

Following proposal is an attempt to remove this inconsistency in the C++ programming language.

2. Design and requirements

Proposed solution is to allow declaration of overloaded copy assignment operator as a non-member function, with certain limitations. Such overloaded assignment operator must accept two formal parameters. If left parameter is non-const reference, only lvalue may be passed as left operand of assignment expression. Declaration of overloaded assignment operator being non-member function might be template function, there might be also many declarations with the same type of left operand (i.e. set of overloaded functions). Following sections of this document will discuss proposed changes in the C++ Standard, limitations required to make these changes cooperate well with existing C++ features and impact of proposed solution. Different solution is presented at the end of this document.

3. Proposal

Copy assignment operator for user defined class is always declared if object of given class is being assigned some value - overloaded by programmer, or implicitly declared by compiler (unless proposal N1582 [1] is accepted). If programmer is allowed to declare overloaded assignment operator as non-member function, program might have many assignment operators for given class declared in different places:

  1.   member functions declared inside class definition (overloaded or declared implicitly by compiler), as currently allowed by the C++ Standard
  2.   non-member functions declared inside class definition (friend functions)
  3.   non-member functions declared outside class definition

We should remember that non-member function can be introduced at any place of program, which means that if above is accepted as it is, actual user of class might be able to declare copy assignment operator. This could introduce problems that are hard to find and diagnose. Author presumes that members of the C++ Standard Committee would be reluctant to allow it. Following sections will discuss limitations that are proposed to remove this problem.

3.1 Only explicit classes

If proposal N1582 is accepted, programmers will be able to define class without copy assignment operator implicitly declared by compiler. It seems reasonable to allow declaration of copy assignment operator for such class as desired by programmer, be it member or non-member function. Thus one proposed limitation is to allow overloading of copy assignment operator in form of non-member function only for explicit classes, as proposed in N1582. If type of left parameter of overloaded non-member copy assignment is not reference to explicit class, program is ill-formed. This limitation will not disallow declaration of overloaded copy assignment operator being non-member function of class at any point of program (also by class user).

If proposed change with this limitation is accepted, following code will be well formed:

explicit struct K
{
  int i;
  K& operator= (double);
};

K& operator=(K&, const K&);
K& operator=(K&, int);

3.2 Only classes where implicit copy assignment operator is not available

In some cases (specified in clause 12.8/12 of the C++ Standard) program is ill-formed if copy assignment operator implicitly declared by compiler is being used. In other words, for some user defined classes implicitly declared copy assignment is not available. This is actually similar to explicit class, as specified in N1582. Thus alternatively to limitation described above (section 3.1), it might be possible to allow declaration of overloaded copy assignment as non-member for all classes where implicitly declared copy assignment is not available - per clause of 12.8/12 the C++ Standard or due to explicit class qualifier proposed in N1582. This limitation will not disallow declaration of overloaded copy assignment operator being non-member function of class at any point of program.

If proposed change with this limitation is accepted, following code will be well formed:

struct K
{
  const int i;
  K() : i(0) {}
};

K& operator=(K&, const K&);

3.3 Declaration only within class definition

Another proposed limitation is to allow declaration of overloaded non-member copy assignment operator only inside definition of class. Currently only one form of declaration of non-member function inside definition of class is allowed, that is friend function. If type of left parameter of overloaded non-member copy assignment operator is not reference to class where overloaded operator is declared, program is ill-formed. This will disallow declaration of non-member copy assignment at any point of program outside class definition; it will also retain one important feature of overloaded copy assignment - currently it is part of class interface and implementation. Author believes that this limitation alone (without need of introducing into C++ language any of two limitations presented above) will make proposed change cooperate well with existing C++ features.

If proposed change with this is accepted, following code will be well formed:

struct K
{
  int i;
  K(int i) : i (i) {}

  friend K& operator=(K& lh, const K& rh)
  {
    lh.i = rh.i;
    return lh;
  }
};

Sample code presented in above two sections (3.1 and 3.2) would be ill-formed, because overloaded copy assignment in these samples is declared outside of function definition. Author is in favour of allowing overloading of copy assignment as non-member function with only this limitation (provided that problem described in section 4 is resolved), or allowing alternative solution described in section 4.1.

4. Discussion

Currently copy assignment operator declared in base class is always hidden by copy assignment operator declared in derived classes (overloaded or declared implicitly by compiler). Author of this proposal is convinced that this is important feature of the C++ language, and no changes in the C++ Standard should harm it. However this proposal and N1582 might actually introduce changes that may affect this feature:

  1.   if derived class is defined explicit (as proposed in N1582), it does not have assignment operator implicitly declared by compiler. If there's also no overloaded copy assignment, there's actually no declaration of copy assignment that would hide the one declared in its base class. Following code demonstrates the problem:
    struct Base
    {
      Base& operator=(const Base&);
    };
    
    explicit struct Derived : public Base // as proposed in N1582
    {
      default Derived();
      default ~Derived();
    };
    
    int main()
    {
      Derived d1;
      Derived d2;
      d1 = d2;          // Base::operator= is not hidden
    }
    
  2.   if copy assignment has been declared as non-member function of some class and it's first parameter is reference to said class (as proposed in this document), then value of class derived from given class may be bound to such parameter. Because non-member function may not be hidden, such overloaded copy assignment would always be present in set of available overloads and selected if it is best match. Under some scenarios such overloaded non-member copy assignment operator will be best, or even only one, match. Following code demonstrates this problem:
    struct Base
    {
      friend Base& operator=(Base &lh, int);
      friend Base& operator=(Base &lh, const Base&);
    };
    
    struct Derived : public Base
    {
      Derived(void * = 0);
      Derived& operator=(const Derived&);
    };
    
    int main()
    {
      Derived d;
      d = 1;       // operator=(Base &lh, int) is best match
      d = Base();  // operator=(Base &lh, const Base&) is the only match
      d = 0;       // unexpected ambiguity
    }
    

Resolution for both these problems is needed. Author does not have solution for the first problem stated above. Second problem might be fixed if the C++ Standard is supplemented with rule stating that left operand of the copy assignment operator is not bound to reference to its base class.

4.1 Alternative solution

Author believes that instead of allowing another form of copy assignment operator, it might be possible to introduce additional qualifier for member function declaration. This qualifier would identify functions that may be called only on lvalue. Example below:

struct K
{
  int i;
  K(int i) : i (i) {}

  K& operator=(const K& rh) &; // "&" is member function qualifier here
};

int main()
{
  K() = 1;    // ill-formed
}

Such solution will introduce new, useful feature into core C++ language. It would possibly not conflict with some C++ features stated above, however it remains to be verified if it would cooperate well with other features of the C++ language.

5. References

[1] http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1582.pdf