face: remove deprecated PendingInterestId, InterestFilterId, RegisteredPrefixId

Plus some refactoring in Face::Impl

Refs: #4885
Change-Id: I5c46aaef35eb618d6f4934d4629c473c7fdde456
diff --git a/ndn-cxx/face.cpp b/ndn-cxx/face.cpp
index 32aa630..8b20ae3 100644
--- a/ndn-cxx/face.cpp
+++ b/ndn-cxx/face.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -173,25 +173,17 @@
   interest2->getNonce();
 
   IO_CAPTURE_WEAK_IMPL(post) {
-    impl->asyncExpressInterest(id, interest2, afterSatisfied, afterNacked, afterTimeout);
+    impl->expressInterest(id, interest2, afterSatisfied, afterNacked, afterTimeout);
   } IO_CAPTURE_WEAK_IMPL_END
 
-  return PendingInterestHandle(*this, reinterpret_cast<const PendingInterestId*>(id));
-}
-
-void
-Face::cancelPendingInterest(const PendingInterestId* pendingInterestId)
-{
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->asyncRemovePendingInterest(reinterpret_cast<RecordId>(pendingInterestId));
-  } IO_CAPTURE_WEAK_IMPL_END
+  return PendingInterestHandle(m_impl, id);
 }
 
 void
 Face::removeAllPendingInterests()
 {
   IO_CAPTURE_WEAK_IMPL(post) {
-    impl->asyncRemoveAllPendingInterests();
+    impl->removeAllPendingInterests();
   } IO_CAPTURE_WEAK_IMPL_END
 }
 
@@ -205,7 +197,7 @@
 Face::put(Data data)
 {
   IO_CAPTURE_WEAK_IMPL(post) {
-    impl->asyncPutData(data);
+    impl->putData(data);
   } IO_CAPTURE_WEAK_IMPL_END
 }
 
@@ -213,7 +205,7 @@
 Face::put(lp::Nack nack)
 {
   IO_CAPTURE_WEAK_IMPL(post) {
-    impl->asyncPutNack(nack);
+    impl->putNack(nack);
   } IO_CAPTURE_WEAK_IMPL_END
 }
 
@@ -236,7 +228,7 @@
 
   auto id = m_impl->registerPrefix(filter.getPrefix(), onSuccess, onFailure, flags, options,
                                    filter, onInterest);
-  return RegisteredPrefixHandle(*this, reinterpret_cast<const RegisteredPrefixId*>(id));
+  return RegisteredPrefixHandle(m_impl, id);
 }
 
 InterestFilterHandle
@@ -245,18 +237,10 @@
   auto id = m_impl->m_interestFilterTable.allocateId();
 
   IO_CAPTURE_WEAK_IMPL(post) {
-    impl->asyncSetInterestFilter(id, filter, onInterest);
+    impl->setInterestFilter(id, filter, onInterest);
   } IO_CAPTURE_WEAK_IMPL_END
 
-  return InterestFilterHandle(*this, reinterpret_cast<const InterestFilterId*>(id));
-}
-
-void
-Face::clearInterestFilter(const InterestFilterId* interestFilterId)
-{
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->asyncUnsetInterestFilter(reinterpret_cast<RecordId>(interestFilterId));
-  } IO_CAPTURE_WEAK_IMPL_END
+  return InterestFilterHandle(m_impl, id);
 }
 
 RegisteredPrefixHandle
@@ -270,18 +254,7 @@
   options.setSigningInfo(signingInfo);
 
   auto id = m_impl->registerPrefix(prefix, onSuccess, onFailure, flags, options, nullopt, nullptr);
-  return RegisteredPrefixHandle(*this, reinterpret_cast<const RegisteredPrefixId*>(id));
-}
-
-void
-Face::unregisterPrefixImpl(const RegisteredPrefixId* registeredPrefixId,
-                           const UnregisterPrefixSuccessCallback& onSuccess,
-                           const UnregisterPrefixFailureCallback& onFailure)
-{
-  IO_CAPTURE_WEAK_IMPL(post) {
-    impl->asyncUnregisterPrefix(reinterpret_cast<RecordId>(registeredPrefixId),
-                                onSuccess, onFailure);
-  } IO_CAPTURE_WEAK_IMPL_END
+  return RegisteredPrefixHandle(m_impl, id);
 }
 
 void
@@ -376,38 +349,61 @@
   }
 }
 
-PendingInterestHandle::PendingInterestHandle(Face& face, const PendingInterestId* id)
-  : CancelHandle([&face, id] { face.cancelPendingInterest(id); })
+PendingInterestHandle::PendingInterestHandle(weak_ptr<Face::Impl> weakImpl, detail::RecordId id)
+  : CancelHandle([w = std::move(weakImpl), id] {
+      auto impl = w.lock();
+      if (impl != nullptr) {
+        impl->asyncRemovePendingInterest(id);
+      }
+    })
 {
 }
 
-RegisteredPrefixHandle::RegisteredPrefixHandle(Face& face, const RegisteredPrefixId* id)
-  : CancelHandle([&face, id] { face.unregisterPrefixImpl(id, nullptr, nullptr); })
-  , m_face(&face)
+RegisteredPrefixHandle::RegisteredPrefixHandle(weak_ptr<Face::Impl> weakImpl, detail::RecordId id)
+  : CancelHandle([=] { unregister(weakImpl, id, nullptr, nullptr); })
+  , m_weakImpl(std::move(weakImpl))
   , m_id(id)
 {
-  // The lambda passed to CancelHandle constructor cannot call this->unregister,
-  // because base class destructor cannot access the member fields of this subclass.
+  // The lambda passed to CancelHandle constructor cannot call the non-static unregister(),
+  // because the base class destructor cannot access the member fields of this subclass.
 }
 
 void
 RegisteredPrefixHandle::unregister(const UnregisterPrefixSuccessCallback& onSuccess,
                                    const UnregisterPrefixFailureCallback& onFailure)
 {
-  if (m_id == nullptr) {
-    if (onFailure != nullptr) {
+  if (m_id == 0) {
+    if (onFailure) {
       onFailure("RegisteredPrefixHandle is empty");
     }
     return;
   }
 
-  m_face->unregisterPrefixImpl(m_id, onSuccess, onFailure);
-  m_face = nullptr;
-  m_id = nullptr;
+  unregister(m_weakImpl, m_id, onSuccess, onFailure);
+  *this = {};
 }
 
-InterestFilterHandle::InterestFilterHandle(Face& face, const InterestFilterId* id)
-  : CancelHandle([&face, id] { face.clearInterestFilter(id); })
+void
+RegisteredPrefixHandle::unregister(const weak_ptr<Face::Impl>& weakImpl, detail::RecordId id,
+                                   const UnregisterPrefixSuccessCallback& onSuccess,
+                                   const UnregisterPrefixFailureCallback& onFailure)
+{
+  auto impl = weakImpl.lock();
+  if (impl != nullptr) {
+    impl->asyncUnregisterPrefix(id, onSuccess, onFailure);
+  }
+  else if (onFailure) {
+    onFailure("Face already closed");
+  }
+}
+
+InterestFilterHandle::InterestFilterHandle(weak_ptr<Face::Impl> weakImpl, detail::RecordId id)
+  : CancelHandle([w = std::move(weakImpl), id] {
+      auto impl = w.lock();
+      if (impl != nullptr) {
+        impl->asyncUnsetInterestFilter(id);
+      }
+    })
 {
 }
 
diff --git a/ndn-cxx/face.hpp b/ndn-cxx/face.hpp
index f1b32a9..39d1d39 100644
--- a/ndn-cxx/face.hpp
+++ b/ndn-cxx/face.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -36,30 +36,31 @@
 
 class Transport;
 
-class PendingInterestId;
 class PendingInterestHandle;
-class RegisteredPrefixId;
 class RegisteredPrefixHandle;
-class InterestFilterId;
 class InterestFilterHandle;
 
+namespace detail {
+using RecordId = uint64_t;
+} // namespace detail
+
 /**
- * @brief Callback invoked when expressed Interest gets satisfied with a Data packet
+ * @brief Callback invoked when an expressed Interest is satisfied by a Data packet
  */
 typedef function<void(const Interest&, const Data&)> DataCallback;
 
 /**
- * @brief Callback invoked when Nack is sent in response to expressed Interest
+ * @brief Callback invoked when a Nack is received in response to an expressed Interest
  */
 typedef function<void(const Interest&, const lp::Nack&)> NackCallback;
 
 /**
- * @brief Callback invoked when expressed Interest times out
+ * @brief Callback invoked when an expressed Interest times out
  */
 typedef function<void(const Interest&)> TimeoutCallback;
 
 /**
- * @brief Callback invoked when incoming Interest matches the specified InterestFilter
+ * @brief Callback invoked when an incoming Interest matches the specified InterestFilter
  */
 typedef function<void(const InterestFilter&, const Interest&)> InterestCallback;
 
@@ -74,12 +75,12 @@
 typedef function<void(const Name&, const std::string&)> RegisterPrefixFailureCallback;
 
 /**
- * @brief Callback invoked when unregisterPrefix or unsetInterestFilter command succeeds
+ * @brief Callback invoked when unregistering a prefix succeeds
  */
 typedef function<void()> UnregisterPrefixSuccessCallback;
 
 /**
- * @brief Callback invoked when unregisterPrefix or unsetInterestFilter command fails
+ * @brief Callback invoked when unregistering a prefix fails
  */
 typedef function<void(const std::string&)> UnregisterPrefixFailureCallback;
 
@@ -237,16 +238,6 @@
                   const TimeoutCallback& afterTimeout);
 
   /**
-   * @deprecated use PendingInterestHandle::cancel()
-   */
-  [[deprecated]]
-  void
-  removePendingInterest(const PendingInterestId* pendingInterestId)
-  {
-    cancelPendingInterest(pendingInterestId);
-  }
-
-  /**
    * @brief Cancel all previously expressed Interests
    */
   void
@@ -312,14 +303,14 @@
                     uint64_t flags = nfd::ROUTE_FLAG_CHILD_INHERIT);
 
   /**
-   * @brief Set InterestFilter to dispatch incoming matching interest to onInterest callback
+   * @brief Set an InterestFilter to dispatch matching incoming Interests to @p onInterest callback.
    *
    * @param filter     Interest filter
    * @param onInterest A callback to be called when a matching interest is received
    *
    * This method modifies library's FIB only, and does not register the prefix with the
-   * forwarder.  It will always succeed.  To register prefix with the forwarder, use
-   * registerPrefix, or use the setInterestFilter overload taking two callbacks.
+   * forwarder. It will always succeed. To register a prefix with the forwarder, use
+   * registerPrefix() or one of the other two setInterestFilter() overloads.
    *
    * @return A handle for unsetting the Interest filter.
    */
@@ -351,38 +342,6 @@
                  uint64_t flags = nfd::ROUTE_FLAG_CHILD_INHERIT);
 
   /**
-   * @deprecated use RegisteredPrefixHandle::unregister()
-   */
-  [[deprecated]]
-  void
-  unsetInterestFilter(const RegisteredPrefixId* registeredPrefixId)
-  {
-    unregisterPrefixImpl(registeredPrefixId, nullptr, nullptr);
-  }
-
-  /**
-   * @deprecated use InterestFilterHandle::cancel()
-   */
-  [[deprecated]]
-  void
-  unsetInterestFilter(const InterestFilterId* interestFilterId)
-  {
-    clearInterestFilter(interestFilterId);
-  }
-
-  /**
-   * @deprecated use RegisteredPrefixHandle::unregister()
-   */
-  [[deprecated]]
-  void
-  unregisterPrefix(const RegisteredPrefixId* registeredPrefixId,
-                   const UnregisterPrefixSuccessCallback& onSuccess,
-                   const UnregisterPrefixFailureCallback& onFailure)
-  {
-    unregisterPrefixImpl(registeredPrefixId, onSuccess, onFailure);
-  }
-
-  /**
    * @brief Publish data packet
    * @param data the Data; a copy will be made, so that the caller is not required to
    *             maintain the argument unchanged
@@ -408,9 +367,9 @@
   /**
    * @brief Process any data to receive or call timeout callbacks.
    *
-   * This call will block forever (default timeout == 0) to process IO on the face.
-   * To exit cleanly on a producer, unset any Interest filters with unsetInterestFilter() and wait
-   * for processEvents() to return. To exit after an error, one can call shutdown().
+   * This call will block forever (with the default timeout of 0) to process I/O on the face.
+   * To exit cleanly on a producer, clear any Interest filters and wait for processEvents() to
+   * return. To exit after an error, one can call shutdown().
    * In consumer applications, processEvents() will return when all expressed Interests have been
    * satisfied, Nacked, or timed out. To terminate earlier, a consumer application should cancel
    * all previously expressed and still-pending Interests.
@@ -442,16 +401,17 @@
   }
 
   /**
-   * @brief Shutdown face operations
+   * @brief Shutdown face operations.
    *
-   * This method cancels all pending operations and closes connection to NDN Forwarder.
+   * This method cancels all pending operations and closes the connection to the NDN forwarder.
    *
    * Note that this method does not stop the io_service if it is shared between multiple Faces or
    * with other IO objects (e.g., Scheduler).
    *
-   * @warning Calling this method could cause outgoing packets to be lost. Producers that shut down
-   *          immediately after sending a Data packet should instead use unsetInterestFilter() to
+   * @warning Calling this method may cause outgoing packets to be lost. Producers that shut down
+   *          immediately after sending a Data packet should instead clear all Interest filters to
    *          shut down cleanly.
+   * @sa processEvents()
    */
   void
   shutdown();
@@ -488,7 +448,6 @@
 
   /**
    * @throw Face::Error on unsupported protocol
-   * @note shared_ptr is passed by value because ownership is transferred to this function
    */
   void
   construct(shared_ptr<Transport> transport, KeyChain& keyChain);
@@ -496,17 +455,6 @@
   void
   onReceiveElement(const Block& blockFromDaemon);
 
-  void
-  cancelPendingInterest(const PendingInterestId* pendingInterestId);
-
-  void
-  clearInterestFilter(const InterestFilterId* interestFilterId);
-
-  void
-  unregisterPrefixImpl(const RegisteredPrefixId* registeredPrefixId,
-                       const UnregisterPrefixSuccessCallback& onSuccess,
-                       const UnregisterPrefixFailureCallback& onFailure);
-
 private:
   /// the io_service owned by this Face, may be null
   unique_ptr<boost::asio::io_service> m_internalIoService;
@@ -532,25 +480,25 @@
   friend InterestFilterHandle;
 };
 
-/** \brief A handle of pending Interest.
+/** \brief Handle for a pending Interest.
  *
  *  \code
  *  PendingInterestHandle hdl = face.expressInterest(interest, satisfyCb, nackCb, timeoutCb);
  *  hdl.cancel(); // cancel the pending Interest
  *  \endcode
- *
- *  \warning Canceling a pending Interest after the face has been destructed may trigger undefined
- *           behavior.
  */
 class PendingInterestHandle : public detail::CancelHandle
 {
 public:
   PendingInterestHandle() noexcept = default;
 
-  PendingInterestHandle(Face& face, const PendingInterestId* id);
+private:
+  PendingInterestHandle(weak_ptr<Face::Impl> impl, detail::RecordId id);
+
+  friend Face;
 };
 
-/** \brief A scoped handle of pending Interest.
+/** \brief Scoped handle for a pending Interest.
  *
  *  Upon destruction of this handle, the pending Interest is canceled automatically.
  *  Most commonly, the application keeps a ScopedPendingInterestHandle as a class member field,
@@ -561,13 +509,10 @@
  *    ScopedPendingInterestHandle hdl = face.expressInterest(interest, satisfyCb, nackCb, timeoutCb);
  *  } // hdl goes out of scope, canceling the pending Interest
  *  \endcode
- *
- *  \warning Canceling a pending Interest after the face has been destructed may trigger undefined
- *           behavior.
  */
 using ScopedPendingInterestHandle = detail::ScopedCancelHandle<PendingInterestHandle>;
 
-/** \brief A handle of registered prefix.
+/** \brief Handle for a registered prefix.
  */
 class RegisteredPrefixHandle : public detail::CancelHandle
 {
@@ -578,22 +523,28 @@
     // see https://stackoverflow.com/a/44693603
   }
 
-  RegisteredPrefixHandle(Face& face, const RegisteredPrefixId* id);
-
   /** \brief Unregister the prefix.
-   *  \warning Unregistering a prefix after the face has been destructed may trigger undefined
-   *           behavior.
    */
   void
   unregister(const UnregisterPrefixSuccessCallback& onSuccess = nullptr,
              const UnregisterPrefixFailureCallback& onFailure = nullptr);
 
 private:
-  Face* m_face = nullptr;
-  const RegisteredPrefixId* m_id = nullptr;
+  RegisteredPrefixHandle(weak_ptr<Face::Impl> impl, detail::RecordId id);
+
+  static void
+  unregister(const weak_ptr<Face::Impl>& impl, detail::RecordId id,
+             const UnregisterPrefixSuccessCallback& onSuccess,
+             const UnregisterPrefixFailureCallback& onFailure);
+
+private:
+  weak_ptr<Face::Impl> m_weakImpl;
+  detail::RecordId m_id = 0;
+
+  friend Face;
 };
 
-/** \brief A scoped handle of registered prefix.
+/** \brief Scoped handle for a registered prefix.
  *
  *  Upon destruction of this handle, the prefix is unregistered automatically.
  *  Most commonly, the application keeps a ScopedRegisteredPrefixHandle as a class member field,
@@ -605,33 +556,30 @@
  *    ScopedRegisteredPrefixHandle hdl = face.registerPrefix(prefix, onSuccess, onFailure);
  *  } // hdl goes out of scope, unregistering the prefix
  *  \endcode
- *
- *  \warning Unregistering a prefix after the face has been destructed may trigger undefined
- *           behavior.
  */
 using ScopedRegisteredPrefixHandle = detail::ScopedCancelHandle<RegisteredPrefixHandle>;
 
-/** \brief A handle of registered Interest filter.
+/** \brief Handle for a registered Interest filter.
  *
  *  \code
  *  InterestFilterHandle hdl = face.setInterestFilter(prefix, onInterest);
  *  hdl.cancel(); // unset the Interest filter
  *  \endcode
- *
- *  \warning Unsetting an Interest filter after the face has been destructed may trigger
- *           undefined behavior.
  */
 class InterestFilterHandle : public detail::CancelHandle
 {
 public:
   InterestFilterHandle() noexcept = default;
 
-  InterestFilterHandle(Face& face, const InterestFilterId* id);
+private:
+  InterestFilterHandle(weak_ptr<Face::Impl> impl, detail::RecordId id);
+
+  friend Face;
 };
 
-/** \brief A scoped handle of registered Interest filter.
+/** \brief Scoped handle for a registered Interest filter.
  *
- *  Upon destruction of this handle, the Interest filter is unset automatically.
+ *  Upon destruction of this handle, the Interest filter is canceled automatically.
  *  Most commonly, the application keeps a ScopedInterestFilterHandle as a class member field,
  *  so that it can cleanup its Interest filter when the class instance is destructed.
  *
@@ -640,9 +588,6 @@
  *    ScopedInterestFilterHandle hdl = face.setInterestFilter(prefix, onInterest);
  *  } // hdl goes out of scope, unsetting the Interest filter
  *  \endcode
- *
- *  \warning Unsetting an Interest filter after the face has been destructed may trigger
- *           undefined behavior.
  */
 using ScopedInterestFilterHandle = detail::ScopedCancelHandle<InterestFilterHandle>;
 
diff --git a/ndn-cxx/impl/face-impl.hpp b/ndn-cxx/impl/face-impl.hpp
index 2e98467..97736e8 100644
--- a/ndn-cxx/impl/face-impl.hpp
+++ b/ndn-cxx/impl/face-impl.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -23,6 +23,7 @@
 #define NDN_IMPL_FACE_IMPL_HPP
 
 #include "ndn-cxx/face.hpp"
+#include "ndn-cxx/impl/interest-filter-record.hpp"
 #include "ndn-cxx/impl/lp-field-tag.hpp"
 #include "ndn-cxx/impl/pending-interest.hpp"
 #include "ndn-cxx/impl/registered-prefix.hpp"
@@ -55,36 +56,39 @@
 
 /** @brief implementation detail of Face
  */
-class Face::Impl : noncopyable
+class Face::Impl : public std::enable_shared_from_this<Face::Impl>
 {
 public:
-  using PendingInterestTable = RecordContainer<PendingInterest>;
-  using InterestFilterTable = RecordContainer<InterestFilterRecord>;
-  using RegisteredPrefixTable = RecordContainer<RegisteredPrefix>;
-
   Impl(Face& face, KeyChain& keyChain)
     : m_face(face)
     , m_scheduler(m_face.getIoService())
     , m_nfdController(m_face, keyChain)
   {
-    auto postOnEmptyPitOrNoRegisteredPrefixes = [this] {
-      this->m_face.getIoService().post([this] { this->onEmptyPitOrNoRegisteredPrefixes(); });
-      // without this extra "post", transport can get paused (-async_read) and then resumed
+    auto onEmptyPitOrNoRegisteredPrefixes = [this] {
+      // Without this extra "post", transport can get paused (-async_read) and then resumed
       // (+async_read) from within onInterest/onData callback.  After onInterest/onData
       // finishes, there is another +async_read with the same memory block.  A few of such
       // async_read duplications can cause various effects and result in segfault.
+      m_face.getIoService().post([this] {
+        if (m_pendingInterestTable.empty() && m_registeredPrefixTable.empty()) {
+          m_face.m_transport->pause();
+          if (!m_ioServiceWork) {
+            m_processEventsTimeoutEvent.cancel();
+          }
+        }
+      });
     };
 
-    m_pendingInterestTable.onEmpty.connect(postOnEmptyPitOrNoRegisteredPrefixes);
-    m_registeredPrefixTable.onEmpty.connect(postOnEmptyPitOrNoRegisteredPrefixes);
+    m_pendingInterestTable.onEmpty.connect(onEmptyPitOrNoRegisteredPrefixes);
+    m_registeredPrefixTable.onEmpty.connect(onEmptyPitOrNoRegisteredPrefixes);
   }
 
 public: // consumer
   void
-  asyncExpressInterest(RecordId id, shared_ptr<const Interest> interest,
-                       const DataCallback& afterSatisfied,
-                       const NackCallback& afterNacked,
-                       const TimeoutCallback& afterTimeout)
+  expressInterest(detail::RecordId id, shared_ptr<const Interest> interest,
+                  const DataCallback& afterSatisfied,
+                  const NackCallback& afterNacked,
+                  const TimeoutCallback& afterTimeout)
   {
     NDN_LOG_DEBUG("<I " << *interest);
     this->ensureConnected(true);
@@ -104,13 +108,18 @@
   }
 
   void
-  asyncRemovePendingInterest(RecordId id)
+  asyncRemovePendingInterest(detail::RecordId id)
   {
-    m_pendingInterestTable.erase(id);
+    m_face.getIoService().post([id, w = weak_ptr<Impl>{shared_from_this()}] { // use weak_from_this() in C++17
+      auto impl = w.lock();
+      if (impl != nullptr) {
+        impl->m_pendingInterestTable.erase(id);
+      }
+    });
   }
 
   void
-  asyncRemoveAllPendingInterests()
+  removeAllPendingInterests()
   {
     m_pendingInterestTable.clear();
   }
@@ -176,21 +185,21 @@
 
 public: // producer
   void
-  asyncSetInterestFilter(RecordId id, const InterestFilter& filter,
-                         const InterestCallback& onInterest)
+  setInterestFilter(detail::RecordId id, const InterestFilter& filter, const InterestCallback& onInterest)
   {
     NDN_LOG_INFO("setting InterestFilter: " << filter);
     m_interestFilterTable.put(id, filter, onInterest);
   }
 
   void
-  asyncUnsetInterestFilter(RecordId id)
+  asyncUnsetInterestFilter(detail::RecordId id)
   {
-    const InterestFilterRecord* record = m_interestFilterTable.get(id);
-    if (record != nullptr) {
-      NDN_LOG_INFO("unsetting InterestFilter: " << record->getFilter());
-      m_interestFilterTable.erase(id);
-    }
+    m_face.getIoService().post([id, w = weak_ptr<Impl>{shared_from_this()}] { // use weak_from_this() in C++17
+      auto impl = w.lock();
+      if (impl != nullptr) {
+        impl->unsetInterestFilter(id);
+      }
+    });
   }
 
   void
@@ -202,20 +211,7 @@
   }
 
   void
-  dispatchInterest(PendingInterest& entry, const Interest& interest)
-  {
-    m_interestFilterTable.forEach([&] (const InterestFilterRecord& filter) {
-      if (!filter.doesMatch(entry)) {
-        return;
-      }
-      NDN_LOG_DEBUG("   matches " << filter.getFilter());
-      entry.recordForwarding();
-      filter.invokeInterestCallback(interest);
-    });
-  }
-
-  void
-  asyncPutData(const Data& data)
+  putData(const Data& data)
   {
     NDN_LOG_DEBUG("<D " << data.getName());
     bool shouldSendToForwarder = satisfyPendingInterests(data);
@@ -234,7 +230,7 @@
   }
 
   void
-  asyncPutNack(const lp::Nack& nack)
+  putNack(const lp::Nack& nack)
   {
     NDN_LOG_DEBUG("<N " << nack.getInterest() << '~' << nack.getHeader().getReason());
     optional<lp::Nack> outNack = nackPendingInterests(nack);
@@ -254,7 +250,7 @@
   }
 
 public: // prefix registration
-  RecordId
+  detail::RecordId
   registerPrefix(const Name& prefix,
                  const RegisterPrefixSuccessCallback& onSuccess,
                  const RegisterPrefixFailureCallback& onFailure,
@@ -269,16 +265,15 @@
       [=] (const nfd::ControlParameters&) {
         NDN_LOG_INFO("registered prefix: " << prefix);
 
-        RecordId filterId = 0;
+        detail::RecordId filterId = 0;
         if (filter) {
           NDN_LOG_INFO("setting InterestFilter: " << *filter);
-          InterestFilterRecord& filterRecord = m_interestFilterTable.insert(*filter, onInterest);
+          auto& filterRecord = m_interestFilterTable.insert(*filter, onInterest);
           filterId = filterRecord.getId();
         }
-
         m_registeredPrefixTable.put(id, prefix, options, filterId);
 
-        if (onSuccess != nullptr) {
+        if (onSuccess) {
           onSuccess(prefix);
         }
       },
@@ -292,39 +287,16 @@
   }
 
   void
-  asyncUnregisterPrefix(RecordId id,
+  asyncUnregisterPrefix(detail::RecordId id,
                         const UnregisterPrefixSuccessCallback& onSuccess,
                         const UnregisterPrefixFailureCallback& onFailure)
   {
-    const RegisteredPrefix* record = m_registeredPrefixTable.get(id);
-    if (record == nullptr) {
-      if (onFailure != nullptr) {
-        onFailure("Unrecognized RegisteredPrefixHandle");
+    m_face.getIoService().post([=, w = weak_ptr<Impl>{shared_from_this()}] { // use weak_from_this() in C++17
+      auto impl = w.lock();
+      if (impl != nullptr) {
+        impl->unregisterPrefix(id, onSuccess, onFailure);
       }
-      return;
-    }
-
-    if (record->getFilterId() != 0) {
-      asyncUnsetInterestFilter(record->getFilterId());
-    }
-
-    NDN_LOG_INFO("unregistering prefix: " << record->getPrefix());
-
-    m_nfdController.start<nfd::RibUnregisterCommand>(
-      nfd::ControlParameters().setName(record->getPrefix()),
-      [=] (const nfd::ControlParameters&) {
-        NDN_LOG_INFO("unregistered prefix: " << record->getPrefix());
-        m_registeredPrefixTable.erase(id);
-
-        if (onSuccess != nullptr) {
-          onSuccess();
-        }
-      },
-      [=] (const nfd::ControlResponse& resp) {
-        NDN_LOG_INFO("unregister prefix failed: " << record->getPrefix());
-        onFailure(resp.getText());
-      },
-      record->getCommandOptions());
+    });
   }
 
 public: // IO routine
@@ -342,17 +314,6 @@
   }
 
   void
-  onEmptyPitOrNoRegisteredPrefixes()
-  {
-    if (m_pendingInterestTable.empty() && m_registeredPrefixTable.empty()) {
-      m_face.m_transport->pause();
-      if (!m_ioServiceWork) {
-        m_processEventsTimeoutEvent.cancel();
-      }
-    }
-  }
-
-  void
   shutdown()
   {
     m_ioServiceWork.reset();
@@ -384,15 +345,75 @@
     return wire;
   }
 
+  void
+  dispatchInterest(PendingInterest& entry, const Interest& interest)
+  {
+    m_interestFilterTable.forEach([&] (const InterestFilterRecord& filter) {
+      if (!filter.doesMatch(entry)) {
+        return;
+      }
+      NDN_LOG_DEBUG("   matches " << filter.getFilter());
+      entry.recordForwarding();
+      filter.invokeInterestCallback(interest);
+    });
+  }
+
+  void
+  unsetInterestFilter(detail::RecordId id)
+  {
+    const auto* record = m_interestFilterTable.get(id);
+    if (record != nullptr) {
+      NDN_LOG_INFO("unsetting InterestFilter: " << record->getFilter());
+      m_interestFilterTable.erase(id);
+    }
+  }
+
+  void
+  unregisterPrefix(detail::RecordId id,
+                   const UnregisterPrefixSuccessCallback& onSuccess,
+                   const UnregisterPrefixFailureCallback& onFailure)
+  {
+    const auto* record = m_registeredPrefixTable.get(id);
+    if (record == nullptr) {
+      if (onFailure) {
+        onFailure("Unrecognized RegisteredPrefixHandle");
+      }
+      return;
+    }
+
+    if (record->getFilterId() != 0) {
+      unsetInterestFilter(record->getFilterId());
+    }
+
+    NDN_LOG_INFO("unregistering prefix: " << record->getPrefix());
+
+    m_nfdController.start<nfd::RibUnregisterCommand>(
+      nfd::ControlParameters().setName(record->getPrefix()),
+      [=] (const nfd::ControlParameters&) {
+        NDN_LOG_INFO("unregistered prefix: " << record->getPrefix());
+        m_registeredPrefixTable.erase(id);
+        if (onSuccess) {
+          onSuccess();
+        }
+      },
+      [=] (const nfd::ControlResponse& resp) {
+        NDN_LOG_INFO("unregister prefix failed: " << record->getPrefix());
+        if (onFailure) {
+          onFailure(resp.getText());
+        }
+      },
+      record->getCommandOptions());
+  }
+
 private:
   Face& m_face;
   Scheduler m_scheduler;
   scheduler::ScopedEventId m_processEventsTimeoutEvent;
   nfd::Controller m_nfdController;
 
-  PendingInterestTable m_pendingInterestTable;
-  InterestFilterTable m_interestFilterTable;
-  RegisteredPrefixTable m_registeredPrefixTable;
+  detail::RecordContainer<PendingInterest> m_pendingInterestTable;
+  detail::RecordContainer<InterestFilterRecord> m_interestFilterTable;
+  detail::RecordContainer<RegisteredPrefix> m_registeredPrefixTable;
 
   unique_ptr<boost::asio::io_service::work> m_ioServiceWork; // if thread needs to be preserved
 
diff --git a/ndn-cxx/impl/interest-filter-record.hpp b/ndn-cxx/impl/interest-filter-record.hpp
index e3c7ea6..3dc40fe 100644
--- a/ndn-cxx/impl/interest-filter-record.hpp
+++ b/ndn-cxx/impl/interest-filter-record.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -23,32 +23,25 @@
 #define NDN_IMPL_INTEREST_FILTER_RECORD_HPP
 
 #include "ndn-cxx/impl/pending-interest.hpp"
+#include "ndn-cxx/impl/record-container.hpp"
 
 namespace ndn {
 
 /**
- * @brief Opaque type to identify an InterestFilterRecord
+ * @brief Associates an InterestFilter with an Interest callback.
  */
-class InterestFilterId;
-
-static_assert(sizeof(const InterestFilterId*) == sizeof(RecordId), "");
-
-/**
- * @brief associates an InterestFilter with Interest callback
- */
-class InterestFilterRecord : public RecordBase<InterestFilterRecord>
+class InterestFilterRecord : public detail::RecordBase<InterestFilterRecord>
 {
 public:
   /**
    * @brief Construct an Interest filter record
    *
    * @param filter an InterestFilter that represents what Interest should invoke the callback
-   * @param interestCallback invoked when matching Interest is received
+   * @param callback invoked when matching Interest is received
    */
-  InterestFilterRecord(const InterestFilter& filter,
-                       const InterestCallback& interestCallback)
+  InterestFilterRecord(const InterestFilter& filter, const InterestCallback& callback)
     : m_filter(filter)
-    , m_interestCallback(interestCallback)
+    , m_interestCallback(callback)
   {
   }
 
diff --git a/ndn-cxx/impl/pending-interest.hpp b/ndn-cxx/impl/pending-interest.hpp
index a766056..7ace1dc 100644
--- a/ndn-cxx/impl/pending-interest.hpp
+++ b/ndn-cxx/impl/pending-interest.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -32,14 +32,7 @@
 namespace ndn {
 
 /**
- * @brief Opaque type to identify a PendingInterest
- */
-class PendingInterestId;
-
-static_assert(sizeof(const PendingInterestId*) == sizeof(RecordId), "");
-
-/**
- * @brief Indicates where a pending Interest came from
+ * @brief Indicates where a pending Interest came from.
  */
 enum class PendingInterestOrigin
 {
@@ -60,9 +53,9 @@
 }
 
 /**
- * @brief Stores a pending Interest and associated callbacks
+ * @brief Stores a pending Interest and associated callbacks.
  */
-class PendingInterest : public RecordBase<PendingInterest>
+class PendingInterest : public detail::RecordBase<PendingInterest>
 {
 public:
   /**
@@ -79,18 +72,16 @@
     , m_dataCallback(dataCallback)
     , m_nackCallback(nackCallback)
     , m_timeoutCallback(timeoutCallback)
-    , m_nNotNacked(0)
   {
     scheduleTimeoutEvent(scheduler);
   }
 
   /**
-   * @brief Construct a pending Interest record for an Interest from NFD
+   * @brief Construct a pending Interest record for an Interest from the forwarder
    */
   PendingInterest(shared_ptr<const Interest> interest, Scheduler& scheduler)
     : m_interest(std::move(interest))
     , m_origin(PendingInterestOrigin::FORWARDER)
-    , m_nNotNacked(0)
   {
     scheduleTimeoutEvent(scheduler);
   }
@@ -121,7 +112,7 @@
   /**
    * @brief Record an incoming Nack against a forwarded Interest
    * @return least severe Nack if all destinations where the Interest was forwarded have Nacked;
-   *          otherwise, nullopt
+   *         otherwise, nullopt
    */
   optional<lp::Nack>
   recordNack(const lp::Nack& nack)
@@ -143,7 +134,7 @@
   void
   invokeDataCallback(const Data& data)
   {
-    if (m_dataCallback != nullptr) {
+    if (m_dataCallback) {
       m_dataCallback(*m_interest, data);
     }
   }
@@ -155,7 +146,7 @@
   void
   invokeNackCallback(const lp::Nack& nack)
   {
-    if (m_nackCallback != nullptr) {
+    if (m_nackCallback) {
       m_nackCallback(*m_interest, nack);
     }
   }
@@ -188,9 +179,8 @@
   NackCallback m_nackCallback;
   TimeoutCallback m_timeoutCallback;
   scheduler::ScopedEventId m_timeoutEvent;
-  int m_nNotNacked; ///< number of Interest destinations that have not Nacked
+  int m_nNotNacked = 0; ///< number of Interest destinations that have not Nacked
   optional<lp::Nack> m_leastSevereNack;
-  std::function<void()> m_deleter;
 };
 
 } // namespace ndn
diff --git a/ndn-cxx/impl/record-container.hpp b/ndn-cxx/impl/record-container.hpp
index 94b2f5e..efbb8f5 100644
--- a/ndn-cxx/impl/record-container.hpp
+++ b/ndn-cxx/impl/record-container.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -28,8 +28,9 @@
 #include <atomic>
 
 namespace ndn {
+namespace detail {
 
-using RecordId = uintptr_t;
+using RecordId = uint64_t;
 
 template<typename T>
 class RecordContainer;
@@ -194,6 +195,7 @@
   std::atomic<RecordId> m_lastId{0};
 };
 
+} // namespace detail
 } // namespace ndn
 
 #endif // NDN_IMPL_RECORD_CONTAINER_HPP
diff --git a/ndn-cxx/impl/registered-prefix.hpp b/ndn-cxx/impl/registered-prefix.hpp
index 423aec1..37fe179 100644
--- a/ndn-cxx/impl/registered-prefix.hpp
+++ b/ndn-cxx/impl/registered-prefix.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -22,27 +22,19 @@
 #ifndef NDN_IMPL_REGISTERED_PREFIX_HPP
 #define NDN_IMPL_REGISTERED_PREFIX_HPP
 
-#include "ndn-cxx/impl/interest-filter-record.hpp"
+#include "ndn-cxx/impl/record-container.hpp"
 #include "ndn-cxx/mgmt/nfd/command-options.hpp"
-#include "ndn-cxx/mgmt/nfd/control-parameters.hpp"
 
 namespace ndn {
 
 /**
- * @brief Opaque type to identify a RegisteredPrefix
+ * @brief Stores information about a prefix registered in an NDN forwarder.
  */
-class RegisteredPrefixId;
-
-static_assert(sizeof(const RegisteredPrefixId*) == sizeof(RecordId), "");
-
-/**
- * @brief stores information about a prefix registered in NDN forwarder
- */
-class RegisteredPrefix : public RecordBase<RegisteredPrefix>
+class RegisteredPrefix : public detail::RecordBase<RegisteredPrefix>
 {
 public:
   RegisteredPrefix(const Name& prefix, const nfd::CommandOptions& options,
-                   RecordId filterId = 0)
+                   detail::RecordId filterId = 0)
     : m_prefix(prefix)
     , m_options(options)
     , m_filterId(filterId)
@@ -61,7 +53,7 @@
     return m_options;
   }
 
-  RecordId
+  detail::RecordId
   getFilterId() const
   {
     return m_filterId;
@@ -70,7 +62,7 @@
 private:
   Name m_prefix;
   nfd::CommandOptions m_options;
-  RecordId m_filterId;
+  detail::RecordId m_filterId;
 };
 
 } // namespace ndn
diff --git a/ndn-cxx/mgmt/dispatcher.hpp b/ndn-cxx/mgmt/dispatcher.hpp
index 308e89a..d41f415 100644
--- a/ndn-cxx/mgmt/dispatcher.hpp
+++ b/ndn-cxx/mgmt/dispatcher.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -151,17 +151,16 @@
    *
    *  Procedure for adding a top-level prefix:
    *  1. if the new top-level prefix overlaps with an existing top-level prefix
-   *     (one top-level prefix is a prefix of another top-level prefix), throw std::domain_error
-   *  2. if wantRegister is true, invoke face.registerPrefix for the top-level prefix;
+   *     (one top-level prefix is a prefix of another top-level prefix), throw std::domain_error.
+   *  2. if \p wantRegister is true, invoke Face::registerPrefix for the top-level prefix;
    *     the returned RegisteredPrefixHandle shall be recorded internally, indexed by the top-level
-   *     prefix
-   *  3. foreach relPrefix from ControlCommands and StatusDatasets,
-   *     join the top-level prefix with the relPrefix to obtain the full prefix,
-   *     and invoke non-registering overload of face.setInterestFilter,
+   *     prefix.
+   *  3. for each `relPrefix` from ControlCommands and StatusDatasets,
+   *     join the top-level prefix with `relPrefix` to obtain the full prefix,
+   *     and invoke non-registering overload of Face::setInterestFilter,
    *     with the InterestHandler set to an appropriate private method to handle incoming Interests
-   *     for the ControlCommand or StatusDataset;
-   *     the returned InterestFilterHandle shall be recorded internally, indexed by the top-level
-   *     prefix
+   *     for the ControlCommand or StatusDataset; the returned InterestFilterHandle shall be
+   *     recorded internally, indexed by the top-level prefix.
    */
   void
   addTopPrefix(const Name& prefix, bool wantRegister = true,
@@ -171,9 +170,9 @@
    *  \param prefix a top-level prefix, e.g., "/localhost/nfd"
    *
    *  Procedure for removing a top-level prefix:
-   *  1. if the top-level prefix has not been added, abort these steps
-   *  2. if the top-level prefix has been added with wantRegister, unregister the prefix
-   *  3. unset each Interest filter recorded during addTopPrefix,
+   *  1. if the top-level prefix has not been added, abort these steps.
+   *  2. if the top-level prefix has been added with `wantRegister`, unregister the prefix.
+   *  3. clear all Interest filters set during addTopPrefix().
    */
   void
   removeTopPrefix(const Name& prefix);
diff --git a/tests/unit/face.t.cpp b/tests/unit/face.t.cpp
index eb01f8f..ef1cab3 100644
--- a/tests/unit/face.t.cpp
+++ b/tests/unit/face.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2019 Regents of the University of California.
+ * Copyright (c) 2013-2020 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -43,7 +43,7 @@
 template<typename PrefixRegReply = WantPrefixRegReply>
 class FaceFixture : public IdentityManagementTimeFixture
 {
-public:
+protected:
   FaceFixture()
     : face(io, m_keyChain, {true, !std::is_same<PrefixRegReply, NoPrefixRegReply>::value})
   {
@@ -59,8 +59,8 @@
                              const RegisterPrefixFailureCallback& failure)> f)
   {
     boost::logic::tribool result = boost::logic::indeterminate;
-    f([&] (const Name&) { result = true; },
-      [&] (const Name&, const std::string&) { result = false; });
+    f([&] (auto) { result = true; },
+      [&] (auto, auto) { result = false; });
 
     advanceClocks(1_ms);
     BOOST_REQUIRE(!boost::logic::indeterminate(result));
@@ -75,22 +75,23 @@
                                const UnregisterPrefixFailureCallback& failure)> f)
   {
     boost::logic::tribool result = boost::logic::indeterminate;
-    f([&] { result = true; }, [&] (const std::string&) { result = false; });
+    f([&] { result = true; },
+      [&] (auto) { result = false; });
 
     advanceClocks(1_ms);
     BOOST_REQUIRE(!boost::logic::indeterminate(result));
     return static_cast<bool>(result);
   }
 
-public:
+protected:
   DummyClientFace face;
 };
 
 BOOST_FIXTURE_TEST_SUITE(TestFace, FaceFixture<>)
 
-BOOST_AUTO_TEST_SUITE(Consumer)
+BOOST_AUTO_TEST_SUITE(ExpressInterest)
 
-BOOST_AUTO_TEST_CASE(ExpressInterestData)
+BOOST_AUTO_TEST_CASE(ReplyData)
 {
   size_t nData = 0;
   face.expressInterest(*makeInterest("/Hello/World", true, 50_ms),
@@ -122,21 +123,17 @@
   BOOST_CHECK_EQUAL(nTimeouts, 1);
 }
 
-BOOST_AUTO_TEST_CASE(ExpressMultipleInterestData)
+BOOST_AUTO_TEST_CASE(MultipleData)
 {
   size_t nData = 0;
 
   face.expressInterest(*makeInterest("/Hello/World", true, 50_ms),
-                       [&] (const Interest& i, const Data& d) {
-                         ++nData;
-                       },
+                       [&] (const auto&, const auto&) { ++nData; },
                        bind([] { BOOST_FAIL("Unexpected Nack"); }),
                        bind([] { BOOST_FAIL("Unexpected timeout"); }));
 
   face.expressInterest(*makeInterest("/Hello/World/a", true, 50_ms),
-                       [&] (const Interest& i, const Data& d) {
-                         ++nData;
-                       },
+                       [&] (const auto&, const auto&) { ++nData; },
                        bind([] { BOOST_FAIL("Unexpected Nack"); }),
                        bind([] { BOOST_FAIL("Unexpected timeout"); }));
 
@@ -151,7 +148,7 @@
   BOOST_CHECK_EQUAL(face.sentData.size(), 0);
 }
 
-BOOST_AUTO_TEST_CASE(ExpressInterestEmptyDataCallback)
+BOOST_AUTO_TEST_CASE(EmptyDataCallback)
 {
   face.expressInterest(*makeInterest("/Hello/World", true),
                        nullptr,
@@ -165,7 +162,7 @@
   } while (false));
 }
 
-BOOST_AUTO_TEST_CASE(ExpressInterestTimeout)
+BOOST_AUTO_TEST_CASE(Timeout)
 {
   size_t nTimeouts = 0;
   face.expressInterest(*makeInterest("/Hello/World", false, 50_ms),
@@ -184,7 +181,7 @@
   BOOST_CHECK_EQUAL(face.sentNacks.size(), 0);
 }
 
-BOOST_AUTO_TEST_CASE(ExpressInterestEmptyTimeoutCallback)
+BOOST_AUTO_TEST_CASE(EmptyTimeoutCallback)
 {
   face.expressInterest(*makeInterest("/Hello/World", false, 50_ms),
                        bind([] { BOOST_FAIL("Unexpected Data"); }),
@@ -197,7 +194,7 @@
   } while (false));
 }
 
-BOOST_AUTO_TEST_CASE(ExpressInterestNack)
+BOOST_AUTO_TEST_CASE(ReplyNack)
 {
   size_t nNacks = 0;
 
@@ -223,24 +220,20 @@
   BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
 }
 
-BOOST_AUTO_TEST_CASE(ExpressMultipleInterestNack)
+BOOST_AUTO_TEST_CASE(MultipleNacks)
 {
   size_t nNacks = 0;
 
   auto interest = makeInterest("/Hello/World", false, 50_ms, 1);
   face.expressInterest(*interest,
                        bind([] { BOOST_FAIL("Unexpected Data"); }),
-                       [&] (const Interest& i, const lp::Nack& n) {
-                         ++nNacks;
-                       },
+                       [&] (const auto&, const auto&) { ++nNacks; },
                        bind([] { BOOST_FAIL("Unexpected timeout"); }));
 
   interest->setNonce(2);
   face.expressInterest(*interest,
                        bind([] { BOOST_FAIL("Unexpected Data"); }),
-                       [&] (const Interest& i, const lp::Nack& n) {
-                         ++nNacks;
-                       },
+                       [&] (const auto&, const auto&) { ++nNacks; },
                        bind([] { BOOST_FAIL("Unexpected timeout"); }));
 
   advanceClocks(40_ms);
@@ -253,7 +246,7 @@
   BOOST_CHECK_EQUAL(face.sentInterests.size(), 2);
 }
 
-BOOST_AUTO_TEST_CASE(ExpressInterestEmptyNackCallback)
+BOOST_AUTO_TEST_CASE(EmptyNackCallback)
 {
   face.expressInterest(*makeInterest("/Hello/World"),
                        bind([] { BOOST_FAIL("Unexpected Data"); }),
@@ -267,24 +260,67 @@
   } while (false));
 }
 
-BOOST_AUTO_TEST_CASE(CancelPendingInterestHandle)
+BOOST_AUTO_TEST_CASE(PutDataFromDataCallback) // Bug 4596
+{
+  face.expressInterest(*makeInterest("/localhost/notification/1"),
+                       [&] (const auto&, const auto&) {
+                         face.put(*makeData("/chronosync/sampleDigest/1"));
+                       }, nullptr, nullptr);
+  advanceClocks(10_ms);
+  BOOST_CHECK_EQUAL(face.sentInterests.back().getName(), "/localhost/notification/1");
+
+  face.receive(*makeInterest("/chronosync/sampleDigest", true));
+  advanceClocks(10_ms);
+
+  face.put(*makeData("/localhost/notification/1"));
+  advanceClocks(10_ms);
+  BOOST_CHECK_EQUAL(face.sentData.back().getName(), "/chronosync/sampleDigest/1");
+}
+
+BOOST_AUTO_TEST_CASE(DestroyWithPendingInterest)
+{
+  auto face2 = make_unique<DummyClientFace>(io, m_keyChain);
+  face2->expressInterest(*makeInterest("/Hello/World", false, 50_ms),
+                         nullptr, nullptr, nullptr);
+  advanceClocks(50_ms, 2);
+  face2.reset();
+
+  advanceClocks(50_ms, 2); // should not crash - Bug 2518
+
+  // avoid "test case [...] did not check any assertions" message from Boost.Test
+  BOOST_CHECK(true);
+}
+
+BOOST_AUTO_TEST_CASE(Handle)
 {
   auto hdl = face.expressInterest(*makeInterest("/Hello/World", true, 50_ms),
                                   bind([] { BOOST_FAIL("Unexpected data"); }),
                                   bind([] { BOOST_FAIL("Unexpected nack"); }),
                                   bind([] { BOOST_FAIL("Unexpected timeout"); }));
-  advanceClocks(10_ms);
-
+  advanceClocks(1_ms);
   hdl.cancel();
-  advanceClocks(10_ms);
-
+  advanceClocks(1_ms);
   face.receive(*makeData("/Hello/World/%21"));
   advanceClocks(200_ms, 5);
 
+  // cancel after destructing face
+  auto face2 = make_unique<DummyClientFace>(io, m_keyChain);
+  auto hdl2 = face2->expressInterest(*makeInterest("/Hello/World", true, 50_ms),
+                                     bind([] { BOOST_FAIL("Unexpected data"); }),
+                                     bind([] { BOOST_FAIL("Unexpected nack"); }),
+                                     bind([] { BOOST_FAIL("Unexpected timeout"); }));
+  advanceClocks(1_ms);
+  face2.reset();
+  advanceClocks(1_ms);
+  hdl2.cancel(); // should not crash
+  advanceClocks(1_ms);
+
   // avoid "test case [...] did not check any assertions" message from Boost.Test
   BOOST_CHECK(true);
 }
 
+BOOST_AUTO_TEST_SUITE_END() // ExpressInterest
+
 BOOST_AUTO_TEST_CASE(RemoveAllPendingInterests)
 {
   face.expressInterest(*makeInterest("/Hello/World/0", false, 50_ms),
@@ -309,40 +345,6 @@
   advanceClocks(200_ms, 5);
 }
 
-BOOST_AUTO_TEST_CASE(DestructionWithoutCancellingPendingInterests) // Bug #2518
-{
-  {
-    DummyClientFace face2(io, m_keyChain);
-    face2.expressInterest(*makeInterest("/Hello/World", false, 50_ms),
-                          nullptr, nullptr, nullptr);
-    advanceClocks(50_ms, 2);
-  }
-
-  advanceClocks(50_ms, 2); // should not crash
-
-  // avoid "test case [...] did not check any assertions" message from Boost.Test
-  BOOST_CHECK(true);
-}
-
-BOOST_AUTO_TEST_CASE(DataCallbackPutData) // Bug 4596
-{
-  face.expressInterest(*makeInterest("/localhost/notification/1"),
-                       [&] (const Interest& i, const Data& d) {
-                         face.put(*makeData("/chronosync/sampleDigest/1"));
-                       }, nullptr, nullptr);
-  advanceClocks(10_ms);
-  BOOST_CHECK_EQUAL(face.sentInterests.back().getName(), "/localhost/notification/1");
-
-  face.receive(*makeInterest("/chronosync/sampleDigest", true));
-  advanceClocks(10_ms);
-
-  face.put(*makeData("/localhost/notification/1"));
-  advanceClocks(10_ms);
-  BOOST_CHECK_EQUAL(face.sentData.back().getName(), "/chronosync/sampleDigest/1");
-}
-
-BOOST_AUTO_TEST_SUITE_END() // Consumer
-
 BOOST_AUTO_TEST_SUITE(Producer)
 
 BOOST_AUTO_TEST_CASE(PutData)
@@ -511,7 +513,72 @@
   BOOST_CHECK_EQUAL(hasNack, true);
 }
 
-BOOST_AUTO_TEST_CASE(SetUnsetInterestFilter)
+BOOST_AUTO_TEST_SUITE_END() // Producer
+
+BOOST_AUTO_TEST_SUITE(RegisterPrefix)
+
+BOOST_FIXTURE_TEST_CASE(Failure, FaceFixture<NoPrefixRegReply>)
+{
+  BOOST_CHECK(!runPrefixReg([&] (const auto& success, const auto& failure) {
+    face.registerPrefix("/Hello/World", success, failure);
+    this->advanceClocks(5_s, 20); // wait for command timeout
+  }));
+}
+
+BOOST_AUTO_TEST_CASE(Handle)
+{
+  RegisteredPrefixHandle hdl;
+  auto doReg = [&] {
+    return runPrefixReg([&] (const auto& success, const auto& failure) {
+      hdl = face.registerPrefix("/Hello/World", success, failure);
+    });
+  };
+  auto doUnreg = [&] {
+    return runPrefixUnreg([&] (const auto& success, const auto& failure) {
+      hdl.unregister(success, failure);
+    });
+  };
+
+  // despite the "undefined behavior" warning, we try not to crash, but no API guarantee for this
+  BOOST_CHECK(!doUnreg());
+
+  // cancel after unregister
+  BOOST_CHECK(doReg());
+  BOOST_CHECK(doUnreg());
+  hdl.cancel();
+  advanceClocks(1_ms);
+
+  // unregister after cancel
+  BOOST_CHECK(doReg());
+  hdl.cancel();
+  advanceClocks(1_ms);
+  BOOST_CHECK(!doUnreg());
+
+  // cancel after destructing face
+  auto face2 = make_unique<DummyClientFace>(io, m_keyChain);
+  hdl = face2->registerPrefix("/Hello/World/2", nullptr,
+                              bind([] { BOOST_FAIL("Unexpected registerPrefix failure"); }));
+  advanceClocks(1_ms);
+  face2.reset();
+  advanceClocks(1_ms);
+  hdl.cancel(); // should not crash
+  advanceClocks(1_ms);
+
+  // unregister after destructing face
+  auto face3 = make_unique<DummyClientFace>(io, m_keyChain);
+  hdl = face3->registerPrefix("/Hello/World/3", nullptr,
+                              bind([] { BOOST_FAIL("Unexpected registerPrefix failure"); }));
+  advanceClocks(1_ms);
+  face3.reset();
+  advanceClocks(1_ms);
+  BOOST_CHECK(!doUnreg());
+}
+
+BOOST_AUTO_TEST_SUITE_END() // RegisterPrefix
+
+BOOST_AUTO_TEST_SUITE(SetInterestFilter)
+
+BOOST_AUTO_TEST_CASE(SetAndCancel)
 {
   size_t nInterests = 0;
   size_t nRegs = 0;
@@ -543,15 +610,9 @@
 
   face.receive(*makeInterest("/Hello/World/%21/3"));
   BOOST_CHECK_EQUAL(nInterests, 2);
-
-  face.unsetInterestFilter(static_cast<const RegisteredPrefixId*>(nullptr));
-  advanceClocks(25_ms, 4);
-
-  face.unsetInterestFilter(static_cast<const InterestFilterId*>(nullptr));
-  advanceClocks(25_ms, 4);
 }
 
-BOOST_AUTO_TEST_CASE(SetInterestFilterEmptyInterestCallback)
+BOOST_AUTO_TEST_CASE(EmptyInterestCallback)
 {
   face.setInterestFilter("/A", nullptr);
   advanceClocks(1_ms);
@@ -562,7 +623,7 @@
   } while (false));
 }
 
-BOOST_AUTO_TEST_CASE(SetUnsetInterestFilterWithoutSucessCallback)
+BOOST_AUTO_TEST_CASE(WithoutSuccessCallback)
 {
   size_t nInterests = 0;
   auto hdl = face.setInterestFilter("/Hello/World",
@@ -590,15 +651,9 @@
 
   face.receive(*makeInterest("/Hello/World/%21/3"));
   BOOST_CHECK_EQUAL(nInterests, 2);
-
-  face.unsetInterestFilter(static_cast<const RegisteredPrefixId*>(nullptr));
-  advanceClocks(25_ms, 4);
-
-  face.unsetInterestFilter(static_cast<const InterestFilterId*>(nullptr));
-  advanceClocks(25_ms, 4);
 }
 
-BOOST_FIXTURE_TEST_CASE(SetInterestFilterFail, FaceFixture<NoPrefixRegReply>)
+BOOST_FIXTURE_TEST_CASE(Failure, FaceFixture<NoPrefixRegReply>)
 {
   // don't enable registration reply
   size_t nRegFailed = 0;
@@ -614,7 +669,7 @@
   BOOST_CHECK_EQUAL(nRegFailed, 1);
 }
 
-BOOST_FIXTURE_TEST_CASE(SetInterestFilterFailWithoutSuccessCallback, FaceFixture<NoPrefixRegReply>)
+BOOST_FIXTURE_TEST_CASE(FailureWithoutSuccessCallback, FaceFixture<NoPrefixRegReply>)
 {
   // don't enable registration reply
   size_t nRegFailed = 0;
@@ -629,31 +684,6 @@
   BOOST_CHECK_EQUAL(nRegFailed, 1);
 }
 
-BOOST_FIXTURE_TEST_CASE(RegisterUnregisterPrefixFail, FaceFixture<NoPrefixRegReply>)
-{
-  BOOST_CHECK(!runPrefixReg([&] (const auto& success, const auto& failure) {
-    face.registerPrefix("/Hello/World", success, failure);
-    this->advanceClocks(5_s, 20); // wait for command timeout
-  }));
-}
-
-BOOST_AUTO_TEST_CASE(RegisterUnregisterPrefixHandle)
-{
-  RegisteredPrefixHandle hdl;
-  BOOST_CHECK(!runPrefixUnreg([&] (const auto& success, const auto& failure) {
-    // despite the "undefined behavior" warning, we try not to crash, but no API guarantee for this
-    hdl.unregister(success, failure);
-  }));
-
-  BOOST_CHECK(runPrefixReg([&] (const auto& success, const auto& failure) {
-    hdl = face.registerPrefix("/Hello/World", success, failure);
-  }));
-
-  BOOST_CHECK(runPrefixUnreg([&] (const auto& success, const auto& failure) {
-    hdl.unregister(success, failure);
-  }));
-}
-
 BOOST_AUTO_TEST_CASE(SimilarFilters)
 {
   size_t nInInterests1 = 0;
@@ -684,21 +714,7 @@
   BOOST_CHECK_EQUAL(nInInterests3, 0);
 }
 
-BOOST_AUTO_TEST_CASE(SetRegexFilterError)
-{
-  face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
-                         [] (const Name&, const Interest&) {
-                           BOOST_FAIL("InterestFilter::Error should have been triggered");
-                         },
-                         nullptr,
-                         bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
-
-  advanceClocks(25_ms, 4);
-
-  BOOST_REQUIRE_THROW(face.receive(*makeInterest("/Hello/World/XXX/b/c")), InterestFilter::Error);
-}
-
-BOOST_AUTO_TEST_CASE(SetRegexFilter)
+BOOST_AUTO_TEST_CASE(RegexFilter)
 {
   size_t nInInterests = 0;
   face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
@@ -721,7 +737,21 @@
   BOOST_CHECK_EQUAL(nInInterests, 2);
 }
 
-BOOST_AUTO_TEST_CASE(SetRegexFilterAndRegister)
+BOOST_AUTO_TEST_CASE(RegexFilterError)
+{
+  face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
+                         [] (const Name&, const Interest&) {
+                           BOOST_FAIL("InterestFilter::Error should have been triggered");
+                         },
+                         nullptr,
+                         bind([] { BOOST_FAIL("Unexpected setInterestFilter failure"); }));
+
+  advanceClocks(25_ms, 4);
+
+  BOOST_CHECK_THROW(face.receive(*makeInterest("/Hello/World/XXX/b/c")), InterestFilter::Error);
+}
+
+BOOST_AUTO_TEST_CASE(RegexFilterAndRegisterPrefix)
 {
   size_t nInInterests = 0;
   face.setInterestFilter(InterestFilter("/Hello/World", "<><b><c>?"),
@@ -748,7 +778,7 @@
   BOOST_CHECK_EQUAL(nInInterests, 2);
 }
 
-BOOST_FIXTURE_TEST_CASE(SetInterestFilterNoReg, FaceFixture<NoPrefixRegReply>) // Bug 2318
+BOOST_FIXTURE_TEST_CASE(WithoutRegisterPrefix, FaceFixture<NoPrefixRegReply>) // Bug 2318
 {
   // This behavior is specific to DummyClientFace.
   // Regular Face won't accept incoming packets until something is sent.
@@ -763,10 +793,10 @@
   BOOST_CHECK_EQUAL(hit, 1);
 }
 
-BOOST_AUTO_TEST_CASE(SetInterestFilterHandle)
+BOOST_AUTO_TEST_CASE(Handle)
 {
   int hit = 0;
-  auto hdl = face.setInterestFilter(Name("/"), bind([&hit] { ++hit; }));
+  InterestFilterHandle hdl = face.setInterestFilter(Name("/"), bind([&hit] { ++hit; }));
   face.processEvents(-1_ms);
 
   face.receive(*makeInterest("/A"));
@@ -779,11 +809,18 @@
   face.receive(*makeInterest("/B"));
   face.processEvents(-1_ms);
   BOOST_CHECK_EQUAL(hit, 1);
+
+  // cancel after destructing face
+  auto face2 = make_unique<DummyClientFace>(io, m_keyChain);
+  InterestFilterHandle hdl2 = face2->setInterestFilter("/Hello/World/2", nullptr);
+  advanceClocks(1_ms);
+  face2.reset();
+  advanceClocks(1_ms);
+  hdl2.cancel(); // should not crash
+  advanceClocks(1_ms);
 }
 
-BOOST_AUTO_TEST_SUITE_END() // Producer
-
-BOOST_AUTO_TEST_SUITE(IoRoutines)
+BOOST_AUTO_TEST_SUITE_END() // SetInterestFilter
 
 BOOST_AUTO_TEST_CASE(ProcessEvents)
 {
@@ -813,8 +850,6 @@
   BOOST_CHECK(true);
 }
 
-BOOST_AUTO_TEST_SUITE_END() // IoRoutines
-
 BOOST_AUTO_TEST_SUITE(Transport)
 
 using ndn::Transport;