views:

118

answers:

1

Lets say I've this exception class:

struct MyException : public std::exception
{
    MyException(const std::exception &exc) : std::exception(exc)
    {
        cout << "lval\n";
    }
    MyException(std::exception &&exc) : std::exception(std::forward<std::exception>(exc))
    {
        cout << "rval\n";
    }
};

...
...

try
{
    throw std::exception("Oh no!");
    // above is rvalue since it's got no name, what if the throw is made as
    // std::exception lvalExc("Oh wierd!");
    // throw lvalExc;
    // if the throw is made thus, how can it be caught by catch(std::exception &&exc)?
}
catch(std::exception &&rValRef)
{
    cout << "rValRef!\n";
    throw MyException(std::forward<std::exception>(rValRef));
}

When I tried to catch by value or by (const) lvalue ref. the compiler says these cases are already handled by the rvalue ref catch clause, which is understandable, as an exception is an xvalue and perhaps the best way to catch an xvalue is an rvalue ref (correct me if I'm wrong). But can someone explain about the perfect forwarding in the above case of exception creation? Is it correct? Even though it compiles, is it meaningful or useful? Should the C++ library I use have a move constructor implemented for its std::exception for this kind of usage to be truly meaningful? I tried searching for articles and SO questions on rvalue references with respect to exceptions, couldn't find any.

+6  A: 

Actually, exception handling has special rules with respect to lvalues and rvalues. The temporary exception object is an lvalue, see 15.1/3 of the current draft:

A throw-expression initializes a temporary object, called the exception object, the type of which is determined by removing any top-level cv-qualifiers from the static type of the operand of throw and adjusting the type from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T”, respectively. The temporary is an lvalue and is used to initialize the variable named in the matching handler (15.3). If the type of the exception object would be an incomplete type or a pointer to an incomplete type other than (possibly cv-qualified) void the program is ill-formed. Except for these restrictions and the restrictions on type matching mentioned in 15.3, the operand of throw is treated exactly as a function argument in a call (5.2.2) or the operand of a return statement.

And catching by rvalue reference is illegal, too, see 15.3/1:

The exception-declaration in a handler describes the type(s) of exceptions that can cause that handler to be entered. The exception-declaration shall not denote an incomplete type or an rvalue reference type. The exception-declaration shall not denote a pointer or reference to an incomplete type, other than void*, const void*, volatile void*, or const volatile void*.

Also, you don't seem to understand perfect forwarding. Your forward invocation is no better than a move. The idea of perfect forwarding is to encode the value category of the argument as part of the type and let template argument deduction figure it out. But your exception handler is not and cannot be a function template.

Basically, perfect forwarding relies on template argument deduction and rvalue references:

void inner(const int&);  // #1 takes only lvalues or const rvalues
void inner(int&&);       // #2 takes non-const rvalues only

template<class T>
void outer(T && x) {
    inner(forward<T>(x));
}

int main() {
   int k = 23;
   outer(k);   // outer<T=int&> --> forward<int&> --> #1
   outer(k+2); // outer<T=int>  --> forward<int>  --> #2
}

Depending on the value category of the argument, template argumend deduction deduces T to be either an lvalue reference or a normal value type. Due to reference collapsing, T&& is also an lvalue reference in the first case, or an rvalue reference in the second case. If you see T&& and T is a template parameter which can be deduced, it's basically a "catch everything". std::forward restores the original value category (encoded in T) so we can perfectly forward the argument to the overloaded inner functions and select the correct one. But this only works because outer is a template and because there are special rules for determining T with respect to its value category. If you use an rvalue references without templates/template argument deduction (like in #2), the function will only accept rvalues.

sellibitze
+1: You say what I said, but more and better. Also, just to make the advice explicit, a plain non-const reference will match and be able to modify the exception object.
Potatoswatter
@Potatoswatter: Sorry for repeating half of your answer. I actually missed your reference to 15.1/3 when I skimmed your response.
sellibitze
@sellibitze: No point in making them mutually exclusive. You're free to borrow anything as long as your answer is correct :v) .
Potatoswatter
@sellibitze: Are you talking about MyException class? If yes, it doesn't have any function template. Also could you please explain what is _perfect forwarding is to encode the value category of the argument as part of the type and let template argument deduction figure it out_.
legends2k
@sellibitze: Thanks for the answer guys - both Potatoswatter and you. But IMHO, eventhough quoting the standard is perfect and to the point, the whole point of coming to SO is to get better clarifications or explanations on the standard futher so that it's easy to grasp for a n00b. Any one can _read_ the standard but understanding it clearly is a totally different story :(
legends2k
@legends2k: I updated the answer to shed some more light on how perfect forwarding works.
sellibitze
@sellibitze: Yeah, that makes it clear for me. Now I understand why you were saying _move_ is what I was trying to do and not perfect forwarding since the function has only a rvalue reference. Thanks for the explanation :)
legends2k