Author: Bronek Kozicki <brok@rubikon.pl>
Doc. no.: N1784=05-0044
Date: 2005-04-15
Working Group: Evolution
This proposal supersedes document N1676. In N1676 I proposed fix for certain asymmetry in the language that makes it impossible to disallow call of copy assignment operator on r-value of user defined type (e.g. "A() = A();"). After discussion with members of BSI panel I decided that proposed resolution (i.e. allowing copy assignment operator to be declared as non-member function) would add significant complexity into language without reasonable gain, given obvious and less obvious issues with name hiding, inheritance, etc. that are partly discussed in section 4 of N1676. There was however alternative solution mentioned in section 4.1 that I'm going to elaborate in following proposal.
1. Motivation
2. Proposal
3. Overloading
4. Ending note
Currently C++ does not have any means to prevent member functions from being called on r-value of user defined type. Typical workaround for this problem (often applied when overloading operators) is to declare function as a non-member function taking non-const reference to user-defined type. Although this is frequently valid solution, it may not always be applied, or is sometimes sub-optimal:
There is obviously some asymmetry in the language that does allow one to define function parameter as non-const reference, thus preventing r-value from being passed to function or allowing separate overloads for l-value and r-value argument, but there are no means to prevent member function from being called on r-value. Here is sample program that would actually benefit from ability to declare overloaded assignment operator that may be called only on l-value:
#include <cstdio> template <typename T, typename V> void f(T t, V v) { // perform some calculation and // store result into reference returned by t() t() = v * 2; } class M { struct K { int i; K(int i) : i (i) {} }; // unable to disable assignment to rvalue of type K static K k; public: static K& g1() { return k; } static K g2() { return k; // returning copy! } static int& g3() { return k.i; } static float g4() { return k.i; // returning copy! } static void show() { std::printf("K::k.i == %i\n", k.i); } }; M::K M::k(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 // f(M::g4, 3.14); // error, as it should be }
Following proposal is an attempt to remove demonstrated problem from C++ .
Proposed resolution is to introduce additional qualifier that could be applied to non-static member function (clause 9.3.1/3). This qualifier would mean "member function may be called on l-value only" and will be further called l-value qualifier. Proposed actual syntax is to reuse ampersand character in location reserved for cv-qualifiers of non-static member function. In motivation example it could be used to define overloaded copy assignment operator, as demonstrated below:
// ... as in motivation example class M { struct K { int i; K(int i) : i (i) {} K& operator=(const K& rh) & // l-value qualifier { i = rh.i; return *this; } }; // ... as in motivation example int main() { f(M::g2, 31); // compilation error }
Here is proposed semantics of l-value qualifier:
Proposed qualifier will allow class author to mark any member function as "to be called on l-values only"; this in effect will give him power to decide "how powerful temporary value may be", thus limiting number of temporary values created by class user. This ability should not be overused. Rule of thumb is : use l-value qualifier only for function whose sole purpose if to modify state of the object or overloads of selected operators. For example: l-value qualifier could be used to denote non-static member function returning void that does not take non-const reference or non-const pointer as its argument and that modifies state of the object without any side effects. It could be also used to declare overload of assignment operator, compound assignment (i.e. "+=", "*=" etc.) or take address operator (i.e. "&"), thus providing class behaviour closer to that of built-in types. I believe this has important implications for generic programming.
Addition of another qualifier raises questions about possible complication of function overloading rules. There could be two solutions:
struct A { void f(const int&) & {} void f(int&) {} // called in line denoted // 1 below void g(int&) & {} // void g(int&) {} - compiler error }; int main() { int i; A a; a.f(i); // 1 }
struct A { A(char * = 0); ~A(); void copy(A&) &; void copy(A&); // performs swap }; int main() { A a; A(new char[10000000]).copy(a); // fast! }