face: scoped pending Interest

refs #4316

Change-Id: I79f832ce599f09a4d5bbf8cd8c4656d677dd164c
diff --git a/ndn-cxx/face.cpp b/ndn-cxx/face.cpp
index bf832d6..6522e64 100644
--- a/ndn-cxx/face.cpp
+++ b/ndn-cxx/face.cpp
@@ -173,20 +173,20 @@
   return m_transport;
 }
 
-const PendingInterestId*
+PendingInterestHandle
 Face::expressInterest(const Interest& interest,
                       const DataCallback& afterSatisfied,
                       const NackCallback& afterNacked,
                       const TimeoutCallback& afterTimeout)
 {
-  shared_ptr<Interest> interest2 = make_shared<Interest>(interest);
+  auto interest2 = make_shared<Interest>(interest);
   interest2->getNonce();
 
   IO_CAPTURE_WEAK_IMPL(post) {
     impl->asyncExpressInterest(interest2, afterSatisfied, afterNacked, afterTimeout);
   } IO_CAPTURE_WEAK_IMPL_END
 
-  return reinterpret_cast<const PendingInterestId*>(interest2.get());
+  return PendingInterestHandle(*this, reinterpret_cast<const PendingInterestId*>(interest2.get()));
 }
 
 void
@@ -413,6 +413,12 @@
   }
 }
 
+PendingInterestHandle::PendingInterestHandle(Face& face, const PendingInterestId* id)
+  : CancelHandle([&face, id] { face.removePendingInterest(id); })
+  , m_id(id)
+{
+}
+
 RegisteredPrefixHandle::RegisteredPrefixHandle(Face& face, const RegisteredPrefixId* id)
   : CancelHandle([&face, id] { face.unregisterPrefix(id, nullptr, nullptr); })
   , m_face(&face)
diff --git a/ndn-cxx/face.hpp b/ndn-cxx/face.hpp
index 05454ca..4f9b4b9 100644
--- a/ndn-cxx/face.hpp
+++ b/ndn-cxx/face.hpp
@@ -37,6 +37,7 @@
 class Transport;
 
 class PendingInterestId;
+class PendingInterestHandle;
 class RegisteredPrefixId;
 class RegisteredPrefixHandle;
 class InterestFilterId;
@@ -231,8 +232,9 @@
    * @param afterTimeout function to be invoked if neither Data nor Network NACK
    *                     is returned within InterestLifetime
    * @throw OversizedPacketError encoded Interest size exceeds MAX_NDN_PACKET_SIZE
+   * @return A handle for canceling the pending Interest.
    */
-  const PendingInterestId*
+  PendingInterestHandle
   expressInterest(const Interest& interest,
                   const DataCallback& afterSatisfied,
                   const NackCallback& afterNacked,
@@ -240,8 +242,7 @@
 
   /**
    * @brief Cancel previously expressed Interest
-   *
-   * @param pendingInterestId The ID returned from expressInterest.
+   * @param pendingInterestId a handle returned by expressInterest.
    */
   void
   removePendingInterest(const PendingInterestId* pendingInterestId);
@@ -363,7 +364,7 @@
    * unsetInterestFilter will use the same credentials as original
    * setInterestFilter/registerPrefix command
    *
-   * @param registeredPrefixId a handle returned from registerPrefix
+   * @param registeredPrefixId a handle returned by registerPrefix
    */
   void
   unsetInterestFilter(const RegisteredPrefixId* registeredPrefixId);
@@ -374,7 +375,7 @@
    * This method always succeeds and will **NOT** send any request to the connected
    * forwarder.
    *
-   * @param interestFilterId a handle returned from setInterestFilter.
+   * @param interestFilterId a handle returned by setInterestFilter.
    */
   void
   unsetInterestFilter(const InterestFilterId* interestFilterId);
@@ -388,7 +389,7 @@
    * If registeredPrefixId was obtained using setInterestFilter, the corresponding
    * InterestFilter will be unset too.
    *
-   * @param registeredPrefixId a handle returned from registerPrefix
+   * @param registeredPrefixId a handle returned by registerPrefix
    * @param onSuccess          Callback to be called when operation succeeds
    * @param onFailure          Callback to be called when operation fails
    */
@@ -534,16 +535,67 @@
   shared_ptr<Impl> m_impl;
 };
 
+/** \brief A handle of pending Interest.
+ *
+ *  \code
+ *  PendingInterestHandle hdl = face.expressInterest(interest, satisfyCb, nackCb, timeoutCb);
+ *  hdl.cancel(); // cancel the pending Interest
+ *  \endcode
+ *
+ *  \warning Canceling the same pending Interest more than once, using same or different
+ *           PendingInterestHandle or ScopedPendingInterestHandle, may trigger undefined behavior.
+ *  \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);
+
+  operator const PendingInterestId*() const noexcept
+  {
+    return m_id;
+  }
+
+private:
+  const PendingInterestId* m_id = nullptr;
+};
+
+/** \brief A scoped handle of 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,
+ *  so that it can cleanup its pending Interest when the class instance is destructed.
+ *
+ *  \code
+ *  {
+ *    ScopedPendingInterestHandle hdl = face.expressInterest(interest, satisfyCb, nackCb, timeoutCb);
+ *  } // hdl goes out of scope, canceling the pending Interest
+ *  \endcode
+ *
+ *  \warning Canceling the same pending Interest more than once, using same or different
+ *           PendingInterestHandle or ScopedPendingInterestHandle, may trigger undefined behavior.
+ *  \warning Canceling a pending Interest after the face has been destructed may trigger undefined
+ *           behavior.
+ */
+using ScopedPendingInterestHandle = detail::ScopedCancelHandle;
+
 /** \brief A handle of registered prefix.
  */
 class RegisteredPrefixHandle : public detail::CancelHandle
 {
 public:
-  RegisteredPrefixHandle() = default;
+  RegisteredPrefixHandle() noexcept
+  {
+    // This could have been '= default', but there's compiler bug in Apple clang 9.0.0,
+    // see https://stackoverflow.com/a/44693603
+  }
 
   RegisteredPrefixHandle(Face& face, const RegisteredPrefixId* id);
 
-  operator const RegisteredPrefixId*() const
+  operator const RegisteredPrefixId*() const noexcept
   {
     return m_id;
   }
@@ -600,11 +652,11 @@
 class InterestFilterHandle : public detail::CancelHandle
 {
 public:
-  InterestFilterHandle() = default;
+  InterestFilterHandle() noexcept = default;
 
   InterestFilterHandle(Face& face, const InterestFilterId* id);
 
-  operator const InterestFilterId*() const
+  operator const InterestFilterId*() const noexcept
   {
     return m_id;
   }
diff --git a/tests/unit/face.t.cpp b/tests/unit/face.t.cpp
index 7f4ba07..43b4db5 100644
--- a/tests/unit/face.t.cpp
+++ b/tests/unit/face.t.cpp
@@ -267,7 +267,7 @@
   } while (false));
 }
 
-BOOST_AUTO_TEST_CASE(RemovePendingInterest)
+BOOST_AUTO_TEST_CASE(RemovePendingInterestId)
 {
   const PendingInterestId* interestId =
     face.expressInterest(*makeInterest("/Hello/World", true, 50_ms),
@@ -286,6 +286,24 @@
   BOOST_CHECK(true);
 }
 
+BOOST_AUTO_TEST_CASE(CancelPendingInterestHandle)
+{
+  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);
+
+  hdl.cancel();
+  advanceClocks(10_ms);
+
+  face.receive(*makeData("/Hello/World/%21"));
+  advanceClocks(200_ms, 5);
+
+  // avoid "test case [...] did not check any assertions" message from Boost.Test
+  BOOST_CHECK(true);
+}
+
 BOOST_AUTO_TEST_CASE(RemoveAllPendingInterests)
 {
   face.expressInterest(*makeInterest("/Hello/World/0", false, 50_ms),