1. Introduction
C++ allows overloading the prefix and postfix
and
operators with the following syntax:
struct S { auto operator ++ () { ... } // ++s auto operator ++ ( int ) { ... } // s++ auto operator -- () { ... } // --s auto operator -- ( int ) { ... } // s-- };
This syntax is a vestige of a historical design decision that, while pragmatic at the time, isn’t good language design by today’s standard (and wasn’t then either). It presents three main problems:
-
It’s subtle
-
It’s not expressive
-
It’s not intuitive
-
It’s not conducive to education
The current syntax was chosen for two main reasons: It was a minimal change to the language and it didn’t add new keywords. Today, tools such as contextual keywords allow us to solve this problem better.
Overloading increment and decrement operators is not super common: Plenty of C++ programs are written without ever
overloading these operators, with their predominant use being iterator implementations. However, it’s also not an
uncommon syntax to need to use. To put the scale of this topic into perspective: According to GitHub code search,
appears about 416k times in open source C++ code at the time of writing. This is the same order of magnitude as
, which
appears about 647k times at the time of writing.
While this isn’t a problem most developers run into daily, and certainly developers are able to memorize which signature is which or look up the syntax, developers should not have to use memorization tricks in order to overload these operators or remember what the syntax means when reading code. Today, we can do better. This paper proposes a modern syntax for these operators, which significantly improves the expressiveness, intuitiveness, readability, and teachability of code using these overloads and largely corrects a historical wart of the language.
2. History
Originally, C++ only had
and
, which were used for both prefix and postfix variations.
Postfix-specific overloads were added later. According to The Design and Evolution of C++ 11.5.3, a driving motivator for the selected
syntax was to find a "minimal change to express the prefix/postfix distinction." To further quote Stroustrup:
I considered the obvious solution, adding the keywords
and
prefix to C++:
postfix class Ptr_to_X { // ... X operator prefix ++ (); // prefix ++ X & operator postfix ++ (); // postfix ++ }; or
class Ptr_to_X { // ... X prefix operator ++ (); // prefix ++ X & postfix operator ++ (); // postfix ++ }; However, I received the usual howl of outrage from people who dislike new keywords. Several alternatives that did not involve new keywords were suggested. For example:
class Ptr_to_X { // ... X ++ operator (); // prefix ++ X & operator ++ (); // postfix ++ }; or
class Ptr_to_X { // ... X & operator ++ (); // postfix because it // returns a reference X operator ++ (); // prefix because it // doesn't return a reference }; I considered the former too cute and the latter too subtle. Finally, I settled on:
class Ptr_to_X { // ... X operator ++ (); // prefix: no argument X & operator ++ ( int ); // postfix: because of // the argument }; This may be both too cute and too subtle, but it works, requires no new syntax, and has a logic to the madness. Other unary operators are prefix and take no arguments when defined as member functions. The "odd" and unused dummy
argument is used to indicate the odd postfix operators. ...
int
The syntax, while pragmatic at the time, and while it does get the job done, is certainly subtle and unintuitive. Even at the time of writing [DNE], Stroustrup described it as "a bit of a wart."
Today, this syntax is a very notable wart, especially in light of syntax improvements and pushes for more modern syntax in the language. Today, a much different design decision would likely have been made, especially given tools like contextual keywords, which didn’t exist at the time.
3. Proposal
This paper proposes the following syntax with contextual keywords:
struct S { auto operator ++ prefix () { ... } // ++s auto operator ++ postfix () { ... } // s++ };
This greatly improves the clarity of code using these operator overloads.
Due to ABI, these would not be new signatures, but rather, they would be syntax sugar for the existing overload signatures:
New syntax | Same as |
---|---|
|
|
|
|
In addition to usage as
and
, it could be used as follows:
S s ; s . operator ++ prefix (); s . operator ++ postfix (); // expands to: // s.operator++(); // s.operator++(0);
One shortcoming of this approach, however, is that the underlying signature would still manifest when working with member function pointers:
auto ( S ::* pre )() = & S :: operator ++ ; auto ( S ::* post )( int ) = & S :: operator ++ ;
Passing an extra
parameter would also be required when using the postfix member function pointer.
This could be partially alleviated with the following semantics, at the cost of some added complexity:
auto pre = & S :: operator ++ prefix ; auto post = & S :: operator ++ postfix ; // works as if by: // auto(S::* tmp1)() = &S::operator++; // auto pre = tmp1; // auto(S::* tmp2)(int) = &S::operator++; // auto pre = tmp2;
While it is unfortunate that the signature wart could still manifest, making member function pointers to increment/decrement operator overloads is likely rare.
4. Alternatives
In a discussion on the #include<c++> discord, Janet Cobb suggested the following utility and workaround:
namespace std { using prefix = void ; using postfix = int ; } struct S { auto operator ++ ( std :: prefix ) { ... } // ++s auto operator ++ ( std :: postfix ) { ... } // s++ };
While this is only a facade over the problem (not entirely dissimilar to the proposed change, which is merely syntactic
sugar), it would be a very low-complexity library change that would greatly improve the status quo without adding
language complexity. In addition to usage as
and
, it could be used as follows:
S s ; s . operator ++ (); s . operator ++ ( std :: postfix ()); auto ( S ::* pre )( std :: prefix ) = & S :: operator ++ ; auto ( S ::* post )( std :: postfix ) = & S :: operator ++ ;
Unfortunately,
would be an invalid use of a void expression, and similar to the proposed
solution, an extra int parameter would have to be passed when using the postfix member function pointer.
5. Questions for EWGI
-
Would this be better solved as a core change or a library change?
6. Proposed Wording
Wording pending guidance and interest.