util: don't require T::Error in io::load and io::save

If T::Error is declared, it must inherit from ndn::tlv::Error.

io::load now requires WireDecodable<T>;
io::save now requires WireEncodable<T>.

refs #3741

Change-Id: Iddfd2febc9c509c3e61cc9ad5da9d8694a302420
diff --git a/src/util/io.hpp b/src/util/io.hpp
index ea093d5..7da812e 100644
--- a/src/util/io.hpp
+++ b/src/util/io.hpp
@@ -22,6 +22,7 @@
 #ifndef NDN_UTIL_IO_HPP
 #define NDN_UTIL_IO_HPP
 
+#include "concepts.hpp"
 #include "../encoding/block.hpp"
 
 #include <iostream>
@@ -63,6 +64,25 @@
 
 constexpr IoEncoding DEPRECATED(BASE_64) = BASE64;
 
+namespace detail {
+
+template<typename T>
+static void
+checkInnerError(typename T::Error*)
+{
+  static_assert(std::is_base_of<tlv::Error, typename T::Error>::value,
+                "T::Error, if declared, must inherit from ndn::tlv::Error");
+}
+
+template<typename T>
+static void
+checkInnerError(...)
+{
+  // T::Error is not declared
+}
+
+} // namespace detail
+
 /** \brief loads a TLV block from a stream
  *  \return if success, the Block and true
  *          otherwise, a default-constructed Block and false
@@ -79,20 +99,18 @@
 shared_ptr<T>
 load(std::istream& is, IoEncoding encoding = BASE64)
 {
+  BOOST_CONCEPT_ASSERT((WireDecodable<T>));
+  detail::checkInnerError<T>(nullptr);
+
   optional<Block> block = loadBlock(is, encoding);
   if (!block) {
     return nullptr;
   }
 
   try {
-    auto obj = make_shared<T>();
-    obj->wireDecode(*block);
-    return obj;
+    return make_shared<T>(*block);
   }
-  catch (const typename T::Error& e) {
-    return nullptr;
-  }
-  catch (const tlv::Error& e) {
+  catch (const tlv::Error&) {
     return nullptr;
   }
 }
@@ -122,13 +140,13 @@
 void
 save(const T& obj, std::ostream& os, IoEncoding encoding = BASE64)
 {
+  BOOST_CONCEPT_ASSERT((WireEncodable<T>));
+  detail::checkInnerError<T>(nullptr);
+
   Block block;
   try {
     block = obj.wireEncode();
   }
-  catch (const typename T::Error& e) {
-    BOOST_THROW_EXCEPTION(Error(e.what()));
-  }
   catch (const tlv::Error& e) {
     BOOST_THROW_EXCEPTION(Error(e.what()));
   }
diff --git a/tests/unit-tests/util/io.t.cpp b/tests/unit-tests/util/io.t.cpp
index fc423a1..8ffab07 100644
--- a/tests/unit-tests/util/io.t.cpp
+++ b/tests/unit-tests/util/io.t.cpp
@@ -90,20 +90,11 @@
 class EncodableType
 {
 public:
-  class Error : public tlv::Error
-  {
-  public:
-    Error()
-      : tlv::Error("encode error")
-    {
-    }
-  };
-
   Block
   wireEncode() const
   {
     if (shouldThrow) {
-      BOOST_THROW_EXCEPTION(Error());
+      BOOST_THROW_EXCEPTION(tlv::Error("encode error"));
     }
 
     // block will be 0xAA, 0x01, 0xDD
@@ -114,23 +105,23 @@
   bool shouldThrow = false;
 };
 
-class DecodableType
+template<bool SHOULD_THROW = false>
+class DecodableTypeTpl
 {
 public:
-  class Error : public tlv::Error
+  DecodableTypeTpl() = default;
+
+  explicit
+  DecodableTypeTpl(const Block& block)
   {
-  public:
-    Error()
-      : tlv::Error("decode error")
-    {
-    }
-  };
+    this->wireDecode(block);
+  }
 
   void
-  wireDecode(const Block& block) const
+  wireDecode(const Block& block)
   {
-    if (shouldThrow) {
-      BOOST_THROW_EXCEPTION(Error());
+    if (m_shouldThrow) {
+      BOOST_THROW_EXCEPTION(tlv::Error("decode error"));
     }
 
     // block must be 0xBB, 0x01, 0xEE
@@ -139,18 +130,12 @@
     BOOST_CHECK_EQUAL(block.value()[0], 0xEE);
   }
 
-public:
-  bool shouldThrow = false;
+private:
+  bool m_shouldThrow = SHOULD_THROW;
 };
 
-class DecodableTypeThrow : public DecodableType
-{
-public:
-  DecodableTypeThrow()
-  {
-    this->shouldThrow = true;
-  }
-};
+typedef DecodableTypeTpl<false> DecodableType;
+typedef DecodableTypeTpl<true> DecodableTypeThrow;
 
 BOOST_AUTO_TEST_CASE(LoadNoEncoding)
 {