In this post I want to describe a small language feature added in C++11 that, although essential for full value semantics support, is often neglected in tutorials and in compiler implementations.
The problem
Do you know Fernando Cacciola’s Boost.Optional library? It is used for representing ‘nullable’ or ‘optional’ objects that may have not been assigned a value. A C++14-compatible variant thereof is now being added to Library Fundamentals TS. One of the goals is that optional<T>
works smoothly with moveable but non-copyable types; and even for copyable types, it should not make any copies unless absolutely necessary. This is a very basic usage scenario:
optional<T> read(); // may return T or not-a-T if (optional<T> v = read()) // has value? use(*v); // get the value
In line 3 we move-construct an optional<T>
which internally move constructs a T
— no copying. But the library offers more ways to access the potential value:
T t = read().value(); // (1) T u = read().value_or(0); // (2) T v = *read(); // (3)
Function value()
either returns a contained value (if one is present) or throws an exception. Function value_or()
either returns the contained value or returns the provided default. operator*
either returns the contained value or renders an undefined behaviour, if no value is present. It is “unsafe” but the fastest alternative at the same point. It is to be used when we are convinced by other means that the value is there.
Let’s pick line 1. Is any copying of T
involved? Obviously, we do not need to have two copies of optional<T>
at any time; but we have a “syntactical” problem in reflecting this. What is the signature of function(s) value
? In C++03 the best we can do is this:
template <typename T> class optional { // ... T& value(); T const& value() const; };
This is a fairly familiar construct. “If the container is const
, give me const
access; if the container is mutable, give me a mutable access. But in both cases we are returning an lvalue reference. Whichever overload is picked, it will trigger the copy construction of the T
object subsequently. There is no way from the member function to tell that it is called for an rvalue. The only thing we could do to prevent copying is this:
T t = std::move(read().value());
But this is just repulsive, and likely forgotten.
The solution
Even if one knows rvalue references and how to apply them for function arguments, one may not know how to apply them to the ‘implied object’ (*this
). Had value()
been a non-member function, we would know what to do:
T & value(optional<T> & this_); T && value(optional<T> && this_); T const& value(optional<T> const& this_);
But what to do for member functions? This is solved by the c++11 feature that we can call “ref-qualifiers for member functions”. The original proposal that describes it is N2439. It enables us to write:
template <typename T> class optional { // ... T& value() &; T&& value() &&; T const& value() const&; };
The syntax may be confusing at first, but it becomes logical when we imagine that each (non-static) member function can be thought of as a free function with one additional special parameter in front: the implied *this
object. Then these references apply to this implied parameter.
The *this
pointer
But this way of reasoning with the implied object cannot be pushed too far. For instance, if we applied it to a good old const
member function, it would mean that we are passing the implied object by value, which is not true.
The root of the problem here boils down to the fact that this
is a pointer rather than a reference. There is a historical justification for this choice. At the point OO support was added to C++, the language did not have references yet, so this
had to be a pointer. And later, when references have been added… you know… backwards compatibility. But this worked fine until rvalue references were introduced. At that time you could say that the const
/mutable
qualifiers on member functions said how type T
— which this
points to — is qualified:
struct Type { void mem(); void mem() const; }; // equivalent to: void mem(Type * this_); void mem(Type const* this_);
But in C++11 we need to be able to say that a member function is invoked on a temporary. For references, it is easily denoted by rvalue references, but there is no such thing for pointers. Therefore in C++ we have these two incompatible ways of looking at the implied object: sometimes as reference and sometimes as pointer. This incompatibility is also visible at C++ syntax level:
struct Type { void mem() const; void mem() &; // ERROR };
You have to decide whether you are using the ref or non-ref qualifiers and you cannot mix them within one overload set. But keyword this
remains a pointer in either case.
Yes, I know what you are saying at this point. The backwards compatibility is often a PITA, but it is also one of C++’s strengths.
Const-ness is not a function’s property
I would like to make one remark now that we are discussing the const
and ref-qualifiers on member functions. Note that unlike other ‘modifiers’ like static
, constexpr
, noexcept
, [[noreturn]]
, these two do not constrain the function, but only the implied *this
parameter. You can freely call non-const
member function from a const
function. The only thing you cannot do is to call a non-const
function on *this
object. See this example:
struct Type { int _i = 0; void modify(Type & t) const { t._i = 1; } }; int main() { Type t; t.modify(t); assert (t._i == 1); }
An object changed its own value from within a const
-qualified member function! Everything is const
-correct!
Ref-qualifiers in your compiler
Does your compiler support ref-qualifiers? The first proposal for rvalue references was submitted on 2002 (N1377), whereas the first proposal for member function ref-qualifiers was submitted 3 years later, on 2005 (N1784). This lag is also reflected in compiler support of these features. The following table summarizes the state as I know it:
Compiler | rvalue refs | ref-qualifiers | source |
---|---|---|---|
GCC | 4.3 | 4.8.1 | here |
Clang | 2.9 | 2.9 | here |
Intel | 12.0 | 14.0 | here |
Visual C++ | VC10 | Nov 2013 CTP | here |
BTW, you can find the reference implementation of std::experimental::optional
library, which exploits ref-qualifiers, here.
Great article! (as always)
Thank you for giving this feature a clear explanation!
IMHO the
“`
T && value(optional && this_);
“`
overload continues to be dubious, though widely used in the standard library. It leads very easily to dangling references when you do things like
“`
const auto& x = make_an_optional().value();
“`
A rvalue ref qualified overload should not be handing out references to its internal members, except in the particular case where the reference it’s handing out is precisely a sub-object, in which case the lifetime extension mechanism applies (although this is quite a subtle point and is bugged in even fairly recent versions of gcc).
Forgot to add (and can’t edit here), it should return by value. In common use cases because of move elision there is zero performance difference. It’s only once you start chaining these things that you see differences, and even then it’s only moves which tend to be cheaps.
Pingback: Does std::optional forwards rvalueness when contained object functions are called? – Windows Questions
Pingback: C++23’s Deducing this: what it is, why it is, how to use it – IT GEEK NEWS