0xbe5077ed
0xbe5077ed

Reputation: 4765

Is it reasonably efficient to insert into an rvalue reference to a stream?

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).

Upvotes: 5

Views: 316

Answers (2)

Mooing Duck
Mooing Duck

Reputation: 66961

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.


Unrelated to your actual question, most overloads for streams are:

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)

Upvotes: 15

David G
David G

Reputation: 96845

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.

Upvotes: 3

Related Questions