I made a custom stream type, call it error_stream
, which derives from std::ostringstream
. I also made a custom manipulator for the stream called throw_cpp_class
(throw_cpp
is an instance of throw_cpp_class
). My goal was to have this syntax:
error_stream s;
s << "some error " << message() << throw_cpp; // throw_cpp throws an exception containing contents of the stream.
I discovered that by defining an insertion operator which takes an rvalue reference to the stream as the first operand, I can now do this:
error_stream() << "some error " << message() << throw_cpp;
The insertion operator looks like this:
error_stream& operator<<(error_stream&& s, const throw_cpp_class&)
{
throw s.str();
return s;
}
What's going on here? Why can I return a value of type error_stream&&
where an error_stream&
is required? (Does this invoke the move constructor?). Is this horribly inefficient? (Not that I really care, given that the exception should be rare).
Q: When should it be used? A: You should use std::move if you want to call functions that support move semantics with an argument which is not an rvalue (temporary expression).
Reference collapsing is the mechanism that leads to universal references (which are really just rvalue references in situations where reference-collapsing takes place) sometimes resolving to lvalue references and sometimes to rvalue references.
That's what rvalue references and move semantics are for! Move semantics allows you to avoid unnecessary copies when working with temporary objects that are about to evaporate, and whose resources can safely be taken from that temporary object and used by another.
rvalue and lvalue are categories of expressions. rvalue reference and lvalue reference are categories of references. Inside a declaration, T x&& = <initializer expression> , the variable x has type T&&, and it can be bound to an expression (the ) which is an rvalue expression.
With this code:
error_stream& operator<<(error_stream&& s, const throw_cpp_class&)
{
throw s.str();
return s;
}
You can return error_stream&& s
as a error_stream&
, because s
is an lvalue, and it is NOT an rvalue.
"What?" you ask? "But I see &&
right there!". This part of C++ is tricky. When you see type&& s
(and type
is not a template), then that means the variable is an rvalue reference, which is a reference that is "constructed from" an rvalue. But it has a name: s
. And everything with a name is an lvalue. That's why you have to call std::move
sometimes, because you have to let the compiler know that you want it to treat that variable as an rvalue again.
Does this invoke the move constructor?).
Nope, it simply returns a reference to the lvalue s
.
Is this horribly inefficient? (Not that I really care, given that the exception should be rare).
Nope, since there's no copy nor even a move happening.
ostream& operator<<(ostream&& s, const T&)
then that means that unless throw_cpp
is the first thing streamed, your overload won't be called, because the previous thing streamed will return an ostream&
, not an error_stream&&
. (Note they ought to be templates, but many aren't, and it's irrelevant to the point) You'll have to cast it back to a error_stream
.
Also, that's not how manipulators work. Manipulators are functions, and when you stream those functions to a stream, the stream calls the function and passes itself as a parameter, so you'd want something more like so:
template <class exception, class charT, class traits>
std::basic_ostream<charT,traits>& throw_cpp(std::basic_ostream<charT,traits>& os)
{
error_stream& self = dynamic_cast<error_stream&>(os); //maybe throws std::bad_cast
throw exception(self.str());
return os; //redundant, but the compiler might not know that.
}
Here it is at work (with stringstream)
T&&
is an rvalue-reference, but its value category is that of a reference (an lvalue) so it can be stored inside an lvalue-reference. No move/copy constructors are called in this case either, because it is taken/returned by reference.
I wouldn't say this is inefficient in the least bit, but rather a common and idiomatic use of rvalue-references.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With