ISO/IEC JTC1 SC22 WG21
P0135R0
Richard Smith
richard@metafoo.co.uk
2015-09-27

Guaranteed copy elision through simplified value categories

This paper presents a case for guaranteeing that certain forms of copy elision always occur (in particular, when the source object is a temporary), and removing the semantic checks on the copy or move constructor in the case where the guaranteed elision occurs. The approach described herein achieves this not by eliding a copy, but by reworking the definition of the value categories (glvalue versus prvalue) such that it is unnatural to perform the copy in the first place.

Copy elision

ISO C++ permits copies to be elided in a number of cases: Whether copies are elided in the above cases is up to the whim of the implementation. In practice, implementations always elide copies in the first case, but source code cannot rely on this, and must provide a copy or move operation for such cases, even when they know (or believe) it will never be called.

This paper addresses only the first case. While we believe that reliable NRVO ("named return value optimization", the second bullet) is an important feature to allow reasoning about performance, the cases where NRVO is possible are subtle and a simple guarantee is difficult to give.

Why should copy elision be mandatory?

A recent thread on the std-proposals mailing list provides a long list of reasons why the current approach to copy elision is problematic. Here are some highlights:

What would mandatory copy elision look like?

struct NonMoveable {
  NonMoveable(int);
  NonMoveable(NonMoveable&) = delete;
  void NonMoveable(NonMoveable&) = delete;
  std::array<int, 1024> arr;
};
NonMoveable make() {
  return NonMoveable(42); // ok, directly constructs returned object
}
auto nm = make(); // ok, directly constructs 'nm'

Value categories

The approach we take to provide guaranteed copy elision is to tweak the definition of C++'s 'glvalue' and 'prvalue' value categories (which, counterintuitively, categorize expressions, not values). C++ currently specifies the value categories as follows:
However, these rules are hard to internalize and confusing -- for instance, an expression that creates a temporary object designates an object, so why is it not an lvalue? Why is NonMoveable().arr an xvalue rather than a prvalue? This paper suggests a rewording of these rules to clarify their intent. In particular, we suggest the following definitions for glvalue and prvalue: That is: prvalues perform initialization, glvalues produce locations.

Denotationally, we have:
 glvalue :: Environment -> (Environment, Location)
 prvalue :: (Environment, Location) -> Environment

So far, this is not a functional change to C++; it does not change the classification of any existing expression. However, it makes it simpler to reason about why expressions are classified as they are:

struct X { int n; };
extern X x;
X{4};   // prvalue: represents initialization of an X object
x.n;    // glvalue: represents the location of x's member n
X{4}.n; // glvalue: represents the location of X{4}'s member n;
        //          in particular, xvalue, as member is expiring

using T = X[2];
T{{5}, {6}};    // prvalue: represents initialization of an array of 2 X's
T{{5}, {6}}[0]; // xvalue: represents location of expiring array element

Implications of refined value categories

Now we have a simple description of value categories, we can reconsider how expressions in those categories should behave. In particular, given a class type A, the expression A() is currently specified as creating a temporary object, but this is not necessary: because the purpose of a prvalue is to performs initialization, it should not be the responsibility of the A() expression to create a temporary object. That should instead be performed by the context in which the expression appears, if necessary. However, in many contexts, it is not necessary to create this temporary object. For instance:

// make() is a prvalue (it returns "by value"). Therefore, it models the
// initialization of an object of type NonMoveable.
NonMoveable make() {
  // The object initialized by 'make()' is initialized by the following
  // constructor call.
  return NonMoveable(42);
}
// Use 'make()' to directly initialize 'nm'. No temporary objects are created.
auto nm = make();

NonMoveable x = {5}; // ok today
NonMoveable x = 5; // equivalent to NonMoveable x = NonMoveable(5),
                   // ill-formed today (creates a temporary but can't move it),
                   // ok under this proposal (does not create a temporary object)

We conclude that a prvalue expression of class or array type should not create a temporary object. Instead, the temporary object is created by the context where the expression appears, if it is necessary. The contexts that require a temporary object to be created ("materialized") are as follows:

Note that the first rule here already exists in the standard, to support prvalues of non-class, non-array type. The difference is that, with the proposed change, the language rules are now uniform for class and non-class types.

Alternatives

There appear to be two main alternatives to this proposal: The main advantages of those approaches over this proposal is that they retain the conceptual model that Type(...) creates a temporary object. However, that model is partially an illusion: it only explains the behavior when Type is a class type.

Acknowledgements

The author wishes to thank David Krauss and Jonathan Coe for their feedback on this proposal, and all the contributors to the std-proposals discussions on this topic for their ideas.