Document number: | N3723 |
Date: | 2013-08-23 |
Project: | Programming Language C++ |
Reply-to: | Pascal Costanza, ExaScience Lab, Intel Belgium, <pascal.costanza@intel.com> |
C++11 substantially improved performance of passing objects around through move semantics and rvalue references. However, the specification for operator-> exhibits a corner case that does not support move semantics very well.
Section 13.3.1.2, paragraph 8 [N3690] specifies that when "operator-> returns, the operator-> is applied to the value returned, with the original second operand." Further (in footnote 133): "If the value returned by the operator-> function has class type, this may result in selecting and calling another operator-> function. This process repeats until an operator-> function returns a value of non-class type." According to Section 5.2.5, paragraph 2 [N3690], the only kind of value of non-class type to which operator-> is applicable is a pointer to a complete class type. This precludes a user-provided definition for operator-> from constructing an rvalue as a result, since it is semantically not meaningful to take the address of an rvalue according to Section 5.3.1, paragraph 3 [N3690], which is what would need to be returned.
Assume you want to define an iterator class for iterating over two arrays where the result of dereferencing an iterator provides the illusion of a single object with members that refer to entries of the separate containers. The following code illustrates this idea.
constexpr auto N = 256; class coord { public: int x, y; coord(int x, int y) : x(x), y(y) {} }; class arrays_iterator { private: int *a0, *a1; size_t i; public: arrays_iterator(int* a0, int* a1, size_t i) : a0(a0), a1(a1), i(i) {} coord operator*() { return coord(a0[i], a1[i]); } coord* operator->() { return &coord(a0[0],a1[i]); // invalid!!! } arrays_iterator operator++() { ++i; return *this; } arrays_iterator operator++(int) { arrays_iterator result(a0,a1,i); i++; return result; } bool operator!=(const arrays_iterator& that) const { return (a0 != that.a0) || (a1 != that.a1) || (i != that.i); } }; arrays_iterator beginArrays(int* a0, int* a1) { return arrays_iterator(a0,a1,0); } arrays_iterator endArrays(int* a0, int* a1) { return arrays_iterator(a0,a1,N); } void test(int* x, int* y) { for (auto it = beginArrays(x,y); it != endArrays(x,y); ++it) { std::cout << it->x << ", " << it->y << std::endl; } }
The definition for operator-> in the code above is invalid because it attempts to take the address of an rvalue. However, some definition for operator-> is necessary to fulfill the requirements for input iterators (cf. Section 24.2.3, paragraph 2 [N3690]) and their supersets.
We encountered a very similar situation in a library that we plan to open source, which relies heavily on the performance offered by move semantics. The lack of support for move semantics in operator-> currently prevents us from implementing fully conforming, efficient iterator classes in that library.
A possible workaround to enable fulfilling the requirements for input iterators is to create an instance of the desired class on the heap and wrap it in a unique_ptr to manage its lifetime. This allows us to provide the following definition for operator->.
class arrays_iterator { ... std::unique_ptr<coord> operator->() { return std::unique_ptr<coord>(new coord(a0[i], a1[i])); } ... }
However, the drawback of this workaround is the added overhead of allocating and almost immediately deallocating a temporary object on the heap. As a consequence, this workaround also makes it impossible for a compiler to completely compile away the temporary object, which would be possible if operator-> would somehow support move semantics.
We propose the following extension of C++ to enable user-provided definitions for operator-> that support move semantics:
With the solution proposed above, we can now provide a valid definition for operator-> for the example given above:
class arrays_iterator { ... coord operator->(int) { return coord(a0[i], a1[i]); } ... }
This solution works because we do not need to take the address of an rvalue anymore. The additional parameter, whose value can be ignored in the implementation, gives a syntactic hint to the compiler that it should use the dot operator on the result rather than the arrow operator. This proposal does not suggest to enable users to provide definitions for some kind of operator.. Instead, a definition for operator-> with the additional parameter effectively ends the "search" for the object on which the final low-level member access using the dot operator is performed.
C++ programmers are already aware of the practice of using an additional ignorable int parameter to provide syntactic hints about the meaning of expressions in the case of operator++ and operator--, specifically to determine whether these are prefix or postfix increment or decrement operators (cf. Section 13.5.7 [N3690]). This is why I believe that this would be an acceptable way to extend C++. However, a disadvantage of this suggestion is that overload resolution would need to be changed for operator-> to ensure that only one of the possible operator-> implementations exists per class type. This may be a too invasive change in the C++ language specification.
However, there are also other ways to provide syntactic hints to the compiler. Here are some possible alternatives I can think of (there may be more):
One could give the new variant of operator-> a different name, like operator->..
coord operator->. () { return coord(a0[i], a1[i]); }
However, this still requires changes to overload resolution rules to ensure that an implementation of only either operator-> or operator->. exists.
One could indicate the variant by way of an attribute (cf. Section 7.6 [N3690]):
coord operator-> [[dotted]] () { return coord(a0[i], a1[i]); }
However, this contradicts the notion that attributes should not change the meaning of a program
According to Section 13.3.1.2, footnote 133 [N3690], if a class type is returned by operator->, then another operator-> function is applied to the result again. However, an operator-> function may not be defined on the resulting class type, which currently results in a compile-time error. One way to make this proposal for extending operator-> work would be to revert to the dot operator in this case.
coord operator-> () { return coord(a0[i], a1[i]); }
In this alternative, the compiler would revert to the dot operator when accessing the member in the return value, because coord does not implement operator->. This effectively turns what used to be a compile-time error into meaningful semantics, which may be the least invasive change to the C++ language specification, but which potentially breaks programmers' existing intuitions what does and does not constitute a compile-time error.
Furthermore, this alternative does not allow the programmer to choose how the result is accessed: There may or may not be situations where sometimes a programmer wants the result to be accessed with another call to operator->, and sometimes by the dot operator, when both options are available. All the other options discussed above allow programmers to make that distinction when implementing operator->, while this alternative does not.