Make it work more generically
authorMarc Horowitz <mhorowitz@fb.com>
Mon, 11 Aug 2014 22:40:23 +0000 (15:40 -0700)
committerSara Golemon <sgolemon@fb.com>
Tue, 9 Sep 2014 21:22:23 +0000 (14:22 -0700)
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
D1490304D1501644, 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

folly/ExceptionWrapper.h
folly/String.h
folly/test/ExceptionWrapperBenchmark.cpp
folly/test/ExceptionWrapperTest.cpp

index 726a27fc810ee367db88946ad035414264f067af..14f2b71bcf70bbf4a118e84879c9c053e5f8d796 100644 (file)
@@ -20,6 +20,7 @@
 #include <cassert>
 #include <exception>
 #include <memory>
+#include <folly/String.h>
 #include <folly/detail/ExceptionWrapper.h>
 
 namespace folly {
@@ -50,11 +51,13 @@ namespace folly {
  * exception_wrapper is designed to handle exception management for both
  * convenience and high performance use cases. make_exception_wrapper is
  * templated on derived type, allowing us to rethrow the exception properly for
- * users that prefer convenience. exception_wrapper is flexible enough to accept
- * any std::exception. For performance sensitive applications, exception_wrapper
- * exposes a get() function. These users can use dynamic_cast to retrieve
- * desired derived types (hence the decision to limit usage to just
- * std::exception instead of void*).
+ * users that prefer convenience. These explicitly named exception types can
+ * therefore be handled without any peformance penalty.  exception_wrapper is
+ * also flexible enough to accept any type. If a caught exception is not of an
+ * explicitly named type, then std::exception_ptr is used to preserve the
+ * 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:
  *
@@ -86,16 +89,14 @@ namespace folly {
  * // Thread2: Exceptions are bad!
  * void processResult() {
  *   auto ep = globalExceptionWrapper.get();
- *   if (ep) {
- *     auto faceplant = dynamic_cast<FacePlantException*>(ep);
- *     if (faceplant) {
+ *   if (!ep.with_exception<FacePlantException>([&](
+ *     FacePlantException& faceplant) {
  *       LOG(ERROR) << "FACEPLANT";
- *     } else {
- *       auto failwhale = dynamic_cast<FailWhaleException*>(ep);
- *       if (failwhale) {
+ *     })) {
+ *     ep.with_exception<FailWhaleException>([&](
+ *       FailWhaleException& failwhale) {
  *         LOG(ERROR) << "FAILWHALE!";
- *       }
- *     }
+ *       });
  *   }
  * }
  *
@@ -107,21 +108,80 @@ class exception_wrapper {
   void throwException() const {
     if (throwfn_) {
       throwfn_(item_.get());
+    } else if (eptr_) {
+      std::rethrow_exception(eptr_);
     }
   }
 
-  std::exception* get() { return item_.get(); }
-  const std::exception* get() const { return item_.get(); }
+  explicit operator bool() const {
+    return item_ || eptr_;
+  }
 
-  std::exception* operator->() { return get(); }
-  const std::exception* operator->() const { return get(); }
+  // This will return a non-nullptr only if the exception is held as a
+  // 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(); }
-  const std::exception& operator*() const { assert(get()); return *get(); }
+  fbstring what() const {
+    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 {
+    if (eptr_) {
+      return eptr_;
+    }
+
     try {
       throwException();
     } catch (...) {
@@ -131,8 +191,16 @@ class exception_wrapper {
   }
 
  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_;
   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>
   friend exception_wrapper make_exception_wrapper(Args&&... args);
@@ -203,13 +271,52 @@ class try_and_catch<LastException, Exceptions...> :
 
   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>
   void call_fn(F&& fn) {
     try {
       Base::call_fn(std::move(fn));
-    } catch (const LastException& e) {
-      this->item_ = std::make_shared<LastException>(e);
-      this->throwfn_ = folly::detail::Thrower<LastException>::doThrow;
+    } catch (LastException& e) {
+      if (typeid(e) == typeid(LastException&)) {
+        assign_exception(e);
+      } else {
+        assign_eptr(e);
+      }
     }
   }
 };
index 0b2b8646822131f44ac3a9d54ddcf176498b997d..223c49073ca6be1278a67cde04d565921c0258c4 100644 (file)
@@ -349,8 +349,15 @@ std::string hexDump(const void* ptr, size_t size);
 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) {
   return folly::to<fbstring>(demangle(typeid(e)), ": ", e.what());
 }
index e78951988084723179412a3dbe8e667024bafcba..e384be65947b03d04cddd8c4feefe31ef6175aac 100644 (file)
@@ -44,7 +44,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_test, iters) {
   std::runtime_error e("payload");
   for (int i = 0; i < iters; ++i) {
     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) {
         std::runtime_error e("payload");
         for (int i = 0; i < iters; ++i) {
           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) {
   std::runtime_error e("payload");
   for (int i = 0; i < iters; ++i) {
     auto ew = folly::make_exception_wrapper<std::runtime_error>(e);
-    std::exception* basePtr = static_cast<std::exception*>(ew.get());
-    auto ep = dynamic_cast<std::runtime_error*>(basePtr);
-    assert(ep);
+    assert(ew.is_compatible_with<std::runtime_error>());
   }
 }
 
@@ -196,9 +194,7 @@ BENCHMARK_RELATIVE(exception_wrapper_create_and_cast_concurrent, iters) {
         std::runtime_error e("payload");
         for (int i = 0; i < iters; ++i) {
           auto ew = folly::make_exception_wrapper<std::runtime_error>(e);
-          std::exception* basePtr = static_cast<std::exception*>(ew.get());
-          auto ep = dynamic_cast<std::runtime_error*>(basePtr);
-          assert(ep);
+          assert(ew.is_compatible_with<std::runtime_error>());
         }
       });
     }
index 8683e89f94798737eee4fc635156b7a2eebfe1dc..e04b9f47d649e39cda355d2b0e083b2b37ca6d1a 100644 (file)
@@ -17,6 +17,7 @@
 #include <gtest/gtest.h>
 #include <stdexcept>
 #include <folly/ExceptionWrapper.h>
+#include <folly/Conv.h>
 
 using namespace folly;
 
@@ -52,9 +53,10 @@ TEST(ExceptionWrapper, try_and_catch_test) {
     [=]() {
       throw std::runtime_error(expected);
     });
-  EXPECT_TRUE(ew.get());
-  EXPECT_EQ(ew.get()->what(), expected);
-  auto rep = dynamic_cast<std::runtime_error*>(ew.get());
+  EXPECT_TRUE(bool(ew));
+  EXPECT_TRUE(ew.getCopied());
+  EXPECT_EQ(ew.what(), "std::runtime_error: payload");
+  auto rep = ew.is_compatible_with<std::runtime_error>();
   EXPECT_TRUE(rep);
 
   // Changing order is like catching in wrong order. Beware of this in your
@@ -62,19 +64,20 @@ TEST(ExceptionWrapper, try_and_catch_test) {
   auto ew2 = try_and_catch<std::runtime_error, std::exception>([=]() {
     throw std::runtime_error(expected);
   });
-  EXPECT_TRUE(ew2.get());
+  EXPECT_TRUE(bool(ew2));
   // We are catching a std::exception, not std::runtime_error.
-  EXPECT_NE(ew2.get()->what(), expected);
-  rep = dynamic_cast<std::runtime_error*>(ew2.get());
-  EXPECT_FALSE(rep);
+  EXPECT_FALSE(ew2.getCopied());
+  // But, we can still get the actual type if we want it.
+  rep = ew2.is_compatible_with<std::runtime_error>();
+  EXPECT_TRUE(rep);
 
   // Catches even if not rightmost.
   auto ew3 = try_and_catch<std::exception, std::runtime_error>([]() {
     throw std::exception();
   });
-  EXPECT_TRUE(ew3.get());
-  EXPECT_NE(ew3.get()->what(), expected);
-  rep = dynamic_cast<std::runtime_error*>(ew3.get());
+  EXPECT_TRUE(bool(ew3));
+  EXPECT_NE(ew3.what(), expected);
+  rep = ew3.is_compatible_with<std::runtime_error>();
   EXPECT_FALSE(rep);
 
   // If does not catch, throws.
@@ -84,3 +87,71 @@ TEST(ExceptionWrapper, try_and_catch_test) {
     }),
     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);
+  }
+}