A proposal to add l-value member function qualifier

Author:         Bronek Kozicki <brok@rubikon.pl>
Doc. no.:       N1784=05-0044
Date:           2005-04-15
Working Group:  Evolution

Abstract

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.

Contents

1. Motivation
2. Proposal
3. Overloading
4. Ending note

1. Motivation

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:

  1. there is different syntax to call member and non-member function, unless it is overloaded operator
  2. member functions have different properties than non-member functions (e.g. member hiding)
  3. some operators may not be overloaded as a non-member functions (e.g. assignment, parentheses)

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++ .

2. Proposal

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:

  1. its primary meaning is to disable member function call on object that is an r-value
  2. it does not change type of this expression used inside member function denoted with this qualifier
  3. l-value qualifier can be freely mixed with cv-qualifiers; const, volatile and l-value properties of type are orthogonal thus there's no reason to introduce any special limitations.

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.

3. Overloading

Addition of another qualifier raises questions about possible complication of function overloading rules. There could be two solutions:

  1. do not allow overloading. This could be easily achieved adding another bullet to clause 13.1 : member functions that differ only by presence or absence of lvalue qualifier are equivalent. Sample code using l-value qualifier would be as follows:
    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
    }
    
  2. allow overloading. As there are no conversions from rvalue to lvalue, overloading rule would not need to take conversion into account. It should also have very low priority in order not to change meaning of programs when l-value qualifier is added to non-static class member functions used by such programs. This would allow following program to perform special optimization when data are copied from temporary value:
    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!
    }
    
    		

4. Ending note

Although problem that this proposal strive to solve is small, it is real and surprising, especially for novice programmers. Proposed solution is still work in progress. Even if it might seem complicated, I believe that simple solution is possible.