Commit 924520ac authored by Marc Horowitz's avatar Marc Horowitz Committed by Sara Golemon

Make it work more generically

Summary:
Change the strategy here: provide a more generic interface
which can be used safely with any exception types, but operates more
efficiently for explicitly named types.  Update the callers to use the
new pattern, which mostly means get() and operator* are replaced with
other more generic methods.  (This diff was constructed by merging
D1490304, D1501644, and D1500861.)

Test Plan:
run tests.  Run something which generates an exception
message, and make sure that message isn't "std::exception"

Reviewed By: marccelani@fb.com

Subscribers: ruibalp, mcduff, marccelani, hitesh, mshneer, rtgw-diffs@, alandau, bmatheny, adityab, wormhole-diffs@, bwester, njormrod

FB internal diff: D1517701
parent c715a583
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
#include <cassert> #include <cassert>
#include <exception> #include <exception>
#include <memory> #include <memory>
#include <folly/String.h>
#include <folly/detail/ExceptionWrapper.h> #include <folly/detail/ExceptionWrapper.h>
namespace folly { namespace folly {
...@@ -50,11 +51,13 @@ namespace folly { ...@@ -50,11 +51,13 @@ namespace folly {
* exception_wrapper is designed to handle exception management for both * exception_wrapper is designed to handle exception management for both
* convenience and high performance use cases. make_exception_wrapper is * convenience and high performance use cases. make_exception_wrapper is
* templated on derived type, allowing us to rethrow the exception properly for * templated on derived type, allowing us to rethrow the exception properly for
* users that prefer convenience. exception_wrapper is flexible enough to accept * users that prefer convenience. These explicitly named exception types can
* any std::exception. For performance sensitive applications, exception_wrapper * therefore be handled without any peformance penalty. exception_wrapper is
* exposes a get() function. These users can use dynamic_cast to retrieve * also flexible enough to accept any type. If a caught exception is not of an
* desired derived types (hence the decision to limit usage to just * explicitly named type, then std::exception_ptr is used to preserve the
* std::exception instead of void*). * exception state. For performance sensitive applications, the accessor methods
* can test or extract a pointer to a specific exception type with very little
* overhead.
* *
* Example usage: * Example usage:
* *
...@@ -86,16 +89,14 @@ namespace folly { ...@@ -86,16 +89,14 @@ namespace folly {
* // Thread2: Exceptions are bad! * // Thread2: Exceptions are bad!
* void processResult() { * void processResult() {
* auto ep = globalExceptionWrapper.get(); * auto ep = globalExceptionWrapper.get();
* if (ep) { * if (!ep.with_exception<FacePlantException>([&](
* auto faceplant = dynamic_cast<FacePlantException*>(ep); * FacePlantException& faceplant) {
* if (faceplant) {
* LOG(ERROR) << "FACEPLANT"; * LOG(ERROR) << "FACEPLANT";
* } else { * })) {
* auto failwhale = dynamic_cast<FailWhaleException*>(ep); * ep.with_exception<FailWhaleException>([&](
* if (failwhale) { * FailWhaleException& failwhale) {
* LOG(ERROR) << "FAILWHALE!"; * LOG(ERROR) << "FAILWHALE!";
* } * });
* }
* } * }
* } * }
* *
...@@ -107,21 +108,80 @@ class exception_wrapper { ...@@ -107,21 +108,80 @@ class exception_wrapper {
void throwException() const { void throwException() const {
if (throwfn_) { if (throwfn_) {
throwfn_(item_.get()); throwfn_(item_.get());
} else if (eptr_) {
std::rethrow_exception(eptr_);
} }
} }
std::exception* get() { return item_.get(); } explicit operator bool() const {
const std::exception* get() const { return item_.get(); } return item_ || eptr_;
}
std::exception* operator->() { return get(); } // This will return a non-nullptr only if the exception is held as a
const std::exception* operator->() const { return get(); } // copy. It is the only interface which will distinguish between an
// exception held this way, and by exception_ptr. You probably
// shouldn't use it at all.
std::exception* getCopied() { return item_.get(); }
const std::exception* getCopied() const { return item_.get(); }
std::exception& operator*() { assert(get()); return *get(); } fbstring what() const {
const std::exception& operator*() const { assert(get()); return *get(); } if (item_) {
return exceptionStr(*item_.get());
} else if (eptr_) {
return estr_;
} else {
return fbstring();
}
}
explicit operator bool() const { return get(); } template <class Ex>
bool is_compatible_with() const {
if (item_) {
return dynamic_cast<const Ex*>(getCopied());
} else if (eptr_) {
try {
std::rethrow_exception(eptr_);
} catch (std::exception& e) {
return dynamic_cast<const Ex*>(&e);
} catch (...) {
// fall through
}
}
return false;
}
template <class Ex, class F>
bool with_exception(F f) {
if (item_) {
if (auto ex = dynamic_cast<Ex*>(getCopied())) {
f(*ex);
return true;
}
} else if (eptr_) {
try {
std::rethrow_exception(eptr_);
} catch (std::exception& e) {
if (auto ex = dynamic_cast<Ex*>(&e)) {
f(*ex);
return true;
}
} catch (...) {
// fall through
}
}
return false;
}
template <class Ex, class F>
bool with_exception(F f) const {
return with_exception<const Ex>(f);
}
std::exception_ptr getExceptionPtr() const { std::exception_ptr getExceptionPtr() const {
if (eptr_) {
return eptr_;
}
try { try {
throwException(); throwException();
} catch (...) { } catch (...) {
...@@ -131,8 +191,16 @@ class exception_wrapper { ...@@ -131,8 +191,16 @@ class exception_wrapper {
} }
protected: protected:
// Optimized case: if we know what type the exception is, we can
// store a copy of the concrete type, and a helper function so we
// can rethrow it.
std::shared_ptr<std::exception> item_; std::shared_ptr<std::exception> item_;
void (*throwfn_)(std::exception*); void (*throwfn_)(std::exception*);
// Fallback case: store the library wrapper, which is less efficient
// but gets the job done. Also store the the what() string, so we
// can at least get it back out without having to rethrow.
std::exception_ptr eptr_;
std::string estr_;
template <class T, class... Args> template <class T, class... Args>
friend exception_wrapper make_exception_wrapper(Args&&... args); friend exception_wrapper make_exception_wrapper(Args&&... args);
...@@ -203,13 +271,52 @@ class try_and_catch<LastException, Exceptions...> : ...@@ -203,13 +271,52 @@ class try_and_catch<LastException, Exceptions...> :
try_and_catch() : Base() {} try_and_catch() : Base() {}
template <typename Ex>
typename std::enable_if<std::is_base_of<std::exception, Ex>::value>::type
assign_eptr(Ex& e) {
this->eptr_ = std::current_exception();
// The cast is needed so we get the desired overload of exceptionStr()
this->estr_ = exceptionStr(static_cast<std::exception&>(e)).toStdString();
}
template <typename Ex>
typename std::enable_if<!std::is_base_of<std::exception, Ex>::value>::type
assign_eptr(Ex& e) {
this->eptr_ = std::current_exception();
this->estr_ = exceptionStr(e).toStdString();
}
template <typename Ex>
struct optimize {
static const bool value =
std::is_base_of<std::exception, Ex>::value &&
std::is_copy_assignable<Ex>::value &&
!std::is_abstract<Ex>::value;
};
template <typename Ex>
typename std::enable_if<!optimize<Ex>::value>::type
assign_exception(Ex& e) {
assign_eptr(e);
}
template <typename Ex>
typename std::enable_if<optimize<Ex>::value>::type
assign_exception(Ex& e) {
this->item_ = std::make_shared<Ex>(e);
this->throwfn_ = folly::detail::Thrower<Ex>::doThrow;
}
template <typename F> template <typename F>
void call_fn(F&& fn) { void call_fn(F&& fn) {
try { try {
Base::call_fn(std::move(fn)); Base::call_fn(std::move(fn));
} catch (const LastException& e) { } catch (LastException& e) {
this->item_ = std::make_shared<LastException>(e); if (typeid(e) == typeid(LastException&)) {
this->throwfn_ = folly::detail::Thrower<LastException>::doThrow; assign_exception(e);
} else {
assign_eptr(e);
}
} }
} }
}; };
......
...@@ -349,8 +349,15 @@ std::string hexDump(const void* ptr, size_t size); ...@@ -349,8 +349,15 @@ std::string hexDump(const void* ptr, size_t size);
fbstring errnoStr(int err); fbstring errnoStr(int err);
/** /**
* Debug string for an exception: include type and what(). * Debug string for an exception: include type and what(). Note that
* the non-templated function overloads will be used in preference to
* the template.
*/ */
template<typename T>
fbstring exceptionStr(const T& e) {
return folly::to<fbstring>(demangle(typeid(e)));
}
inline fbstring exceptionStr(const std::exception& e) { inline fbstring exceptionStr(const std::exception& e) {
return folly::to<fbstring>(demangle(typeid(e)), ": ", e.what()); return folly::to<fbstring>(demangle(typeid(e)), ": ", e.what());
} }
......
...@@ -44,7 +44,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_test, iters) { ...@@ -44,7 +44,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_test, iters) {
std::runtime_error e("payload"); std::runtime_error e("payload");
for (int i = 0; i < iters; ++i) { for (int i = 0; i < iters; ++i) {
auto ew = folly::make_exception_wrapper<std::runtime_error>(e); auto ew = folly::make_exception_wrapper<std::runtime_error>(e);
assert(ew.get()); assert(ew);
} }
} }
...@@ -81,7 +81,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_test_concurrent, iters) { ...@@ -81,7 +81,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_test_concurrent, iters) {
std::runtime_error e("payload"); std::runtime_error e("payload");
for (int i = 0; i < iters; ++i) { for (int i = 0; i < iters; ++i) {
auto ew = folly::make_exception_wrapper<std::runtime_error>(e); auto ew = folly::make_exception_wrapper<std::runtime_error>(e);
assert(ew.get()); assert(ew);
} }
}); });
} }
...@@ -127,9 +127,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_cast, iters) { ...@@ -127,9 +127,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_cast, iters) {
std::runtime_error e("payload"); std::runtime_error e("payload");
for (int i = 0; i < iters; ++i) { for (int i = 0; i < iters; ++i) {
auto ew = folly::make_exception_wrapper<std::runtime_error>(e); auto ew = folly::make_exception_wrapper<std::runtime_error>(e);
std::exception* basePtr = static_cast<std::exception*>(ew.get()); assert(ew.is_compatible_with<std::runtime_error>());
auto ep = dynamic_cast<std::runtime_error*>(basePtr);
assert(ep);
} }
} }
...@@ -196,9 +194,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_cast_concurrent, iters) { ...@@ -196,9 +194,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_cast_concurrent, iters) {
std::runtime_error e("payload"); std::runtime_error e("payload");
for (int i = 0; i < iters; ++i) { for (int i = 0; i < iters; ++i) {
auto ew = folly::make_exception_wrapper<std::runtime_error>(e); auto ew = folly::make_exception_wrapper<std::runtime_error>(e);
std::exception* basePtr = static_cast<std::exception*>(ew.get()); assert(ew.is_compatible_with<std::runtime_error>());
auto ep = dynamic_cast<std::runtime_error*>(basePtr);
assert(ep);
} }
}); });
} }
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <stdexcept> #include <stdexcept>
#include <folly/ExceptionWrapper.h> #include <folly/ExceptionWrapper.h>
#include <folly/Conv.h>
using namespace folly; using namespace folly;
...@@ -52,9 +53,10 @@ TEST(ExceptionWrapper, try_and_catch_test) { ...@@ -52,9 +53,10 @@ TEST(ExceptionWrapper, try_and_catch_test) {
[=]() { [=]() {
throw std::runtime_error(expected); throw std::runtime_error(expected);
}); });
EXPECT_TRUE(ew.get()); EXPECT_TRUE(bool(ew));
EXPECT_EQ(ew.get()->what(), expected); EXPECT_TRUE(ew.getCopied());
auto rep = dynamic_cast<std::runtime_error*>(ew.get()); EXPECT_EQ(ew.what(), "std::runtime_error: payload");
auto rep = ew.is_compatible_with<std::runtime_error>();
EXPECT_TRUE(rep); EXPECT_TRUE(rep);
// Changing order is like catching in wrong order. Beware of this in your // Changing order is like catching in wrong order. Beware of this in your
...@@ -62,19 +64,20 @@ TEST(ExceptionWrapper, try_and_catch_test) { ...@@ -62,19 +64,20 @@ TEST(ExceptionWrapper, try_and_catch_test) {
auto ew2 = try_and_catch<std::runtime_error, std::exception>([=]() { auto ew2 = try_and_catch<std::runtime_error, std::exception>([=]() {
throw std::runtime_error(expected); throw std::runtime_error(expected);
}); });
EXPECT_TRUE(ew2.get()); EXPECT_TRUE(bool(ew2));
// We are catching a std::exception, not std::runtime_error. // We are catching a std::exception, not std::runtime_error.
EXPECT_NE(ew2.get()->what(), expected); EXPECT_FALSE(ew2.getCopied());
rep = dynamic_cast<std::runtime_error*>(ew2.get()); // But, we can still get the actual type if we want it.
EXPECT_FALSE(rep); rep = ew2.is_compatible_with<std::runtime_error>();
EXPECT_TRUE(rep);
// Catches even if not rightmost. // Catches even if not rightmost.
auto ew3 = try_and_catch<std::exception, std::runtime_error>([]() { auto ew3 = try_and_catch<std::exception, std::runtime_error>([]() {
throw std::exception(); throw std::exception();
}); });
EXPECT_TRUE(ew3.get()); EXPECT_TRUE(bool(ew3));
EXPECT_NE(ew3.get()->what(), expected); EXPECT_NE(ew3.what(), expected);
rep = dynamic_cast<std::runtime_error*>(ew3.get()); rep = ew3.is_compatible_with<std::runtime_error>();
EXPECT_FALSE(rep); EXPECT_FALSE(rep);
// If does not catch, throws. // If does not catch, throws.
...@@ -84,3 +87,71 @@ TEST(ExceptionWrapper, try_and_catch_test) { ...@@ -84,3 +87,71 @@ TEST(ExceptionWrapper, try_and_catch_test) {
}), }),
std::exception); std::exception);
} }
class AbstractIntException : public std::exception {
public:
virtual int getInt() const = 0;
};
class IntException : public AbstractIntException {
public:
explicit IntException(int i)
: i_(i) {}
virtual int getInt() const { return i_; }
virtual const char* what() const noexcept override {
what_ = folly::to<std::string>("int == ", i_);
return what_.c_str();
}
private:
int i_;
mutable std::string what_;
};
TEST(ExceptionWrapper, with_exception_test) {
int expected = 23;
// This works, and doesn't slice.
exception_wrapper ew = try_and_catch<std::exception, std::runtime_error>(
[=]() {
throw IntException(expected);
});
EXPECT_TRUE(bool(ew));
EXPECT_EQ(ew.what(), "IntException: int == 23");
ew.with_exception<IntException>([&](const IntException& ie) {
EXPECT_EQ(ie.getInt(), expected);
});
// I can try_and_catch a non-copyable base class. This will use
// std::exception_ptr internally.
exception_wrapper ew2 = try_and_catch<AbstractIntException>(
[=]() {
throw IntException(expected);
});
EXPECT_TRUE(bool(ew2));
EXPECT_EQ(ew2.what(), "IntException: int == 23");
ew2.with_exception<AbstractIntException>([&](AbstractIntException& ie) {
EXPECT_EQ(ie.getInt(), expected);
EXPECT_EQ(typeid(ie), typeid(IntException));
});
}
TEST(ExceptionWrapper, non_std_exception_test) {
int expected = 17;
exception_wrapper ew = try_and_catch<std::exception, int>(
[=]() {
throw expected;
});
EXPECT_TRUE(bool(ew));
EXPECT_FALSE(ew.is_compatible_with<std::exception>());
EXPECT_EQ(ew.what(), "int");
// non-std::exception types are supported, but the only way to
// access their value is to explicity rethrow and catch it.
try {
ew.throwException();
} catch (int& i) {
EXPECT_EQ(i, expected);
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment