Since many software developers these days start their programming journey with managed languages like Java, C++ code they write often reflects their idioms and patterns. A few days ago, I was looking for ways to speed up a performance critical section of the code and noticed that a significant portion of CPU cycles were spent on copy constructor and destructor of an expensive Thrift object.
To illustrate the problem, let’s take a look at
Even though we are passing an rvalue of the builder to use
function, the build
method returns a reference to a val
, which means that consume
receives a copy-constructed instance of the val
and the value it contains goes to waste. In this particular case, it’s not a big deal, since Val
is super trivial, but the Thrift class I was dealing with had multiple expensive members, including strings, maps and other Thrift objects.
It seems very wasteful to copy all of these fields and destroy the originals. Why don’t we take advantage of a magical std::move? In theory we can modify build method to return std::move(val) and change its type to Val, but
this would be bad news to someone who calls the build method more than once and there is nothing in the API that would make this hard/impossible
we no longer have a way to build the same instances multiple times
In Rust we can use almighty borrow checker to support distinguish these cases - self type consumes builder instance and can move its internals out and &self only borrows builder instance, so a copy or reference can be returned to clients.
Fortunately, C++ also has something for this case - meet ref-qualified member functions. We can create a member function that can be invoked only on rvalues, in which case it’s safe to return moved internals.
Now, if we don’t plan to reuse builder instance, we can std::move it, which in turn allows us to invoke build_once method
Note that it’s a compile-time error to call build_once on builder instance directly, so our API is both safe and efficient.
As always, you can play with this code using godbolt compiler.
It's a good point and unfortunately very little known feature. Meyers book also talks about it. BTW, you could overload build() with & and &&. i.e. qualify & and && at the end of the decl. so && will be used with an rvalue and & will be used with an lvalue. I think in the case above we might need to std::forward since inside the function, holder is lvalue.