util: reimplement scheduler::EventId with CancelHandle

<ndn-cxx/util/scheduler-scoped-event-id.hpp> header is deprecated.
It has been combined into <ndn-cxx/util/scheduler.hpp>.

Implicit conversion from nullptr to EventId is also deprecated.

refs #4698

Change-Id: I0310d41ac9638cb5be02ae61b887538f562541b2
diff --git a/ndn-cxx/detail/cancel-handle.cpp b/ndn-cxx/detail/cancel-handle.cpp
index 604522b..8057249 100644
--- a/ndn-cxx/detail/cancel-handle.cpp
+++ b/ndn-cxx/detail/cancel-handle.cpp
@@ -25,16 +25,16 @@
 namespace detail {
 
 CancelHandle::CancelHandle(function<void()> cancel)
-  : doCancel(std::move(cancel))
+  : m_cancel(std::move(cancel))
 {
 }
 
 void
-CancelHandle::cancel()
+CancelHandle::cancel() const
 {
-  if (doCancel != nullptr) {
-    doCancel();
-    doCancel = nullptr;
+  if (m_cancel != nullptr) {
+    m_cancel();
+    m_cancel = nullptr;
   }
 }
 
diff --git a/ndn-cxx/detail/cancel-handle.hpp b/ndn-cxx/detail/cancel-handle.hpp
index 11af4e4..e2cb9b5 100644
--- a/ndn-cxx/detail/cancel-handle.hpp
+++ b/ndn-cxx/detail/cancel-handle.hpp
@@ -32,18 +32,18 @@
 class CancelHandle
 {
 public:
+  CancelHandle() noexcept = default;
+
   explicit
-  CancelHandle(function<void()> cancel = nullptr);
+  CancelHandle(function<void()> cancel);
 
   /** \brief Cancel the operation.
-   *  \warning Cancelling the same operation more than once, using same or different CancelHandle or
-   *           ScopedCancelHandle, may trigger undefined behavior.
    */
   void
-  cancel();
+  cancel() const;
 
-protected:
-  function<void()> doCancel;
+private:
+  mutable function<void()> m_cancel;
 };
 
 /** \brief Cancels an operation automatically upon destruction.
@@ -51,7 +51,7 @@
 class ScopedCancelHandle
 {
 public:
-  ScopedCancelHandle() = default;
+  ScopedCancelHandle() noexcept = default;
 
   /** \brief Implicit constructor from CancelHandle.
    */
diff --git a/ndn-cxx/impl/face-impl.hpp b/ndn-cxx/impl/face-impl.hpp
index b676efb..2f68557 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-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -67,7 +67,6 @@
   Impl(Face& face)
     : m_face(face)
     , m_scheduler(m_face.getIoService())
-    , m_processEventsTimeoutEvent(m_scheduler)
   {
     auto postOnEmptyPitOrNoRegisteredPrefixes = [this] {
       this->m_face.getIoService().post([this] { this->onEmptyPitOrNoRegisteredPrefixes(); });
diff --git a/ndn-cxx/impl/pending-interest.hpp b/ndn-cxx/impl/pending-interest.hpp
index 4bed422..6495f56 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-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -26,7 +26,7 @@
 #include "ndn-cxx/face.hpp"
 #include "ndn-cxx/interest.hpp"
 #include "ndn-cxx/lp/nack.hpp"
-#include "ndn-cxx/util/scheduler-scoped-event-id.hpp"
+#include "ndn-cxx/util/scheduler.hpp"
 
 namespace ndn {
 
@@ -80,7 +80,6 @@
     , m_dataCallback(dataCallback)
     , m_nackCallback(nackCallback)
     , m_timeoutCallback(timeoutCallback)
-    , m_timeoutEvent(scheduler)
     , m_nNotNacked(0)
   {
     scheduleTimeoutEvent(scheduler);
@@ -95,7 +94,6 @@
   PendingInterest(shared_ptr<const Interest> interest, Scheduler& scheduler)
     : m_interest(std::move(interest))
     , m_origin(PendingInterestOrigin::FORWARDER)
-    , m_timeoutEvent(scheduler)
     , m_nNotNacked(0)
   {
     scheduleTimeoutEvent(scheduler);
diff --git a/ndn-cxx/ims/in-memory-storage-entry.cpp b/ndn-cxx/ims/in-memory-storage-entry.cpp
index 6b380c0..148aee1 100644
--- a/ndn-cxx/ims/in-memory-storage-entry.cpp
+++ b/ndn-cxx/ims/in-memory-storage-entry.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -32,7 +32,7 @@
 InMemoryStorageEntry::release()
 {
   m_dataPacket.reset();
-  m_markStaleEventId.reset();
+  m_markStaleEventId.cancel();
 }
 
 void
@@ -43,15 +43,9 @@
 }
 
 void
-InMemoryStorageEntry::setMarkStaleEventId(unique_ptr<util::scheduler::ScopedEventId> markStaleEventId)
+InMemoryStorageEntry::scheduleMarkStale(util::Scheduler& sched, time::nanoseconds after)
 {
-  m_markStaleEventId = std::move(markStaleEventId);
-}
-
-void
-InMemoryStorageEntry::markStale()
-{
-  m_isFresh = false;
+  m_markStaleEventId = sched.scheduleEvent(after, [this] { m_isFresh = false; });
 }
 
 } // namespace ndn
diff --git a/ndn-cxx/ims/in-memory-storage-entry.hpp b/ndn-cxx/ims/in-memory-storage-entry.hpp
index c86e038..1640c02 100644
--- a/ndn-cxx/ims/in-memory-storage-entry.hpp
+++ b/ndn-cxx/ims/in-memory-storage-entry.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -24,7 +24,7 @@
 
 #include "ndn-cxx/data.hpp"
 #include "ndn-cxx/interest.hpp"
-#include "ndn-cxx/util/scheduler-scoped-event-id.hpp"
+#include "ndn-cxx/util/scheduler.hpp"
 
 namespace ndn {
 
@@ -74,15 +74,10 @@
   void
   setData(const Data& data);
 
-  /** @brief Set eventId for the markStale event.
+  /** @brief Schedule an event to mark this entry as non-fresh.
    */
   void
-  setMarkStaleEventId(unique_ptr<util::scheduler::ScopedEventId> eventId);
-
-  /** @brief Disable the data from satisfying interest with MustBeFresh
-   */
-  void
-  markStale();
+  scheduleMarkStale(util::Scheduler& sched, time::nanoseconds after);
 
   /** @brief Check if the data can satisfy an interest with MustBeFresh
    */
@@ -96,7 +91,7 @@
   shared_ptr<const Data> m_dataPacket;
 
   bool m_isFresh;
-  unique_ptr<util::scheduler::ScopedEventId> m_markStaleEventId;
+  util::scheduler::ScopedEventId m_markStaleEventId;
 };
 
 } // namespace ndn
diff --git a/ndn-cxx/ims/in-memory-storage.cpp b/ndn-cxx/ims/in-memory-storage.cpp
index b86dd22..31ec616 100644
--- a/ndn-cxx/ims/in-memory-storage.cpp
+++ b/ndn-cxx/ims/in-memory-storage.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -186,9 +186,7 @@
   m_nPackets++;
   entry->setData(data);
   if (m_scheduler != nullptr && mustBeFreshProcessingWindow > ZERO_WINDOW) {
-    auto eventId = make_unique<util::scheduler::ScopedEventId>(*m_scheduler);
-    *eventId = m_scheduler->scheduleEvent(mustBeFreshProcessingWindow, [entry] { entry->markStale(); });
-    entry->setMarkStaleEventId(std::move(eventId));
+    entry->scheduleMarkStale(*m_scheduler, mustBeFreshProcessingWindow);
   }
   m_cache.insert(entry);
 
diff --git a/ndn-cxx/net/impl/network-monitor-impl-osx.cpp b/ndn-cxx/net/impl/network-monitor-impl-osx.cpp
index 838c630..99f606f 100644
--- a/ndn-cxx/net/impl/network-monitor-impl-osx.cpp
+++ b/ndn-cxx/net/impl/network-monitor-impl-osx.cpp
@@ -104,7 +104,6 @@
 
 NetworkMonitorImplOsx::NetworkMonitorImplOsx(boost::asio::io_service& io)
   : m_scheduler(io)
-  , m_cfLoopEvent(m_scheduler)
   , m_context{0, this, nullptr, nullptr, nullptr}
   , m_scStore(SCDynamicStoreCreate(nullptr, CFSTR("net.named-data.ndn-cxx.NetworkMonitor"),
                                    &NetworkMonitorImplOsx::onConfigChanged, &m_context))
diff --git a/ndn-cxx/net/impl/network-monitor-impl-osx.hpp b/ndn-cxx/net/impl/network-monitor-impl-osx.hpp
index 44b9af2..c287faa 100644
--- a/ndn-cxx/net/impl/network-monitor-impl-osx.hpp
+++ b/ndn-cxx/net/impl/network-monitor-impl-osx.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -31,7 +31,6 @@
 
 #include "ndn-cxx/detail/cf-releaser-osx.hpp"
 #include "ndn-cxx/util/scheduler.hpp"
-#include "ndn-cxx/util/scheduler-scoped-event-id.hpp"
 
 #include <CoreFoundation/CoreFoundation.h>
 #include <SystemConfiguration/SystemConfiguration.h>
diff --git a/ndn-cxx/util/notification-subscriber.cpp b/ndn-cxx/util/notification-subscriber.cpp
index da5089b..2ae1e52 100644
--- a/ndn-cxx/util/notification-subscriber.cpp
+++ b/ndn-cxx/util/notification-subscriber.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2018 Regents of the University of California,
+ * Copyright (c) 2014-2019 Regents of the University of California,
  *                         Arizona Board of Regents,
  *                         Colorado State University,
  *                         University Pierre & Marie Curie, Sorbonne University,
@@ -42,7 +42,6 @@
   , m_lastNackSequenceNum(std::numeric_limits<uint64_t>::max())
   , m_attempts(1)
   , m_scheduler(face.getIoService())
-  , m_nackEvent(m_scheduler)
   , m_lastInterestId(nullptr)
   , m_interestLifetime(interestLifetime)
 {
diff --git a/ndn-cxx/util/notification-subscriber.hpp b/ndn-cxx/util/notification-subscriber.hpp
index ffa37a9..d2bd2fc 100644
--- a/ndn-cxx/util/notification-subscriber.hpp
+++ b/ndn-cxx/util/notification-subscriber.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2018 Regents of the University of California,
+ * Copyright (c) 2014-2019 Regents of the University of California,
  *                         Arizona Board of Regents,
  *                         Colorado State University,
  *                         University Pierre & Marie Curie, Sorbonne University,
@@ -31,7 +31,6 @@
 #include "ndn-cxx/face.hpp"
 #include "ndn-cxx/util/concepts.hpp"
 #include "ndn-cxx/util/scheduler.hpp"
-#include "ndn-cxx/util/scheduler-scoped-event-id.hpp"
 #include "ndn-cxx/util/signal.hpp"
 #include "ndn-cxx/util/time.hpp"
 
diff --git a/ndn-cxx/util/scheduler-scoped-event-id.cpp b/ndn-cxx/util/scheduler-scoped-event-id.cpp
deleted file mode 100644
index d33678a..0000000
--- a/ndn-cxx/util/scheduler-scoped-event-id.cpp
+++ /dev/null
@@ -1,62 +0,0 @@
-/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/*
- * Copyright (c) 2013-2018 Regents of the University of California.
- *
- * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
- *
- * ndn-cxx library is free software: you can redistribute it and/or modify it under the
- * terms of the GNU Lesser General Public License as published by the Free Software
- * Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
- * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
- *
- * You should have received copies of the GNU General Public License and GNU Lesser
- * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
- * <http://www.gnu.org/licenses/>.
- *
- * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
- */
-
-#include "ndn-cxx/util/scheduler-scoped-event-id.hpp"
-
-namespace ndn {
-namespace util {
-namespace scheduler {
-
-ScopedEventId::ScopedEventId(Scheduler& scheduler) noexcept
-  : m_scheduler(&scheduler)
-{
-}
-
-ScopedEventId&
-ScopedEventId::operator=(EventId event)
-{
-  if (m_event != event) {
-    cancel();
-    m_event = std::move(event);
-  }
-  return *this;
-}
-
-ScopedEventId::~ScopedEventId()
-{
-  cancel();
-}
-
-void
-ScopedEventId::cancel()
-{
-  m_scheduler->cancelEvent(m_event);
-}
-
-void
-ScopedEventId::release() noexcept
-{
-  m_event.reset();
-}
-
-} // namespace scheduler
-} // namespace util
-} // namespace ndn
diff --git a/ndn-cxx/util/scheduler-scoped-event-id.hpp b/ndn-cxx/util/scheduler-scoped-event-id.hpp
index 87372c0..ea30af9 100644
--- a/ndn-cxx/util/scheduler-scoped-event-id.hpp
+++ b/ndn-cxx/util/scheduler-scoped-event-id.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -22,76 +22,8 @@
 #ifndef NDN_UTIL_SCHEDULER_SCOPED_EVENT_ID_HPP
 #define NDN_UTIL_SCHEDULER_SCOPED_EVENT_ID_HPP
 
+#pragma message("scheduler-scoped-event-id.hpp has been combined into scheduler.hpp")
+
 #include "ndn-cxx/util/scheduler.hpp"
 
-namespace ndn {
-namespace util {
-namespace scheduler {
-
-/** \brief Event that is automatically cancelled upon destruction.
- */
-class ScopedEventId
-{
-public:
-  /** \brief Construct ScopedEventId tied to the specified scheduler.
-   *  \param scheduler Scheduler to which the event is tied.  Behavior is undefined if
-   *                   \p scheduler is destructed before an uncanceled ScopedEventId.
-   */
-  explicit
-  ScopedEventId(Scheduler& scheduler) noexcept;
-
-  ScopedEventId(const ScopedEventId&) = delete;
-
-  ScopedEventId&
-  operator=(const ScopedEventId&) = delete;
-
-  /** \brief Move constructor.
-   */
-  ScopedEventId(ScopedEventId&&) noexcept;
-
-  /** \brief Move assignment operator.
-   */
-  ScopedEventId&
-  operator=(ScopedEventId&&) noexcept;
-
-  /** \brief Assign an event.
-   *
-   *  If a different event has been assigned to this instance previously,
-   *  that event will be cancelled immediately.
-   *
-   *  \note The caller should ensure that this ScopedEventId is tied to the correct Scheduler.
-   *        Behavior is undefined when assigning an event scheduled in another Scheduler instance.
-   */
-  ScopedEventId&
-  operator=(EventId event);
-
-  /** \brief Destructor, automatically cancels the event.
-   */
-  ~ScopedEventId();
-
-  /** \brief Manually cancel the event.
-   */
-  void
-  cancel();
-
-  /** \brief Release the event so that it won't be canceled when this ScopedEventId is destructed.
-   */
-  void
-  release() noexcept;
-
-private:
-  Scheduler* m_scheduler; // pointer to allow move semantics
-  EventId m_event;
-};
-
-inline
-ScopedEventId::ScopedEventId(ScopedEventId&&) noexcept = default;
-
-inline ScopedEventId&
-ScopedEventId::operator=(ScopedEventId&&) noexcept = default;
-
-} // namespace scheduler
-} // namespace util
-} // namespace ndn
-
 #endif // NDN_UTIL_SCHEDULER_SCOPED_EVENT_ID_HPP
diff --git a/ndn-cxx/util/scheduler.cpp b/ndn-cxx/util/scheduler.cpp
index 0cbb311..36a662c 100644
--- a/ndn-cxx/util/scheduler.cpp
+++ b/ndn-cxx/util/scheduler.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -28,6 +28,8 @@
 namespace util {
 namespace scheduler {
 
+/** \brief Stores internal information about a scheduled event
+ */
 class EventInfo : noncopyable
 {
 public:
@@ -51,6 +53,12 @@
   EventQueue::const_iterator queueIt;
 };
 
+EventId::EventId(Scheduler& sched, weak_ptr<EventInfo> info)
+  : CancelHandle([&sched, info] { sched.cancelImpl(info.lock()); })
+  , m_info(std::move(info))
+{
+}
+
 EventId::operator bool() const noexcept
 {
   auto sp = m_info.lock();
@@ -64,6 +72,12 @@
          !(m_info.owner_before(other.m_info) || other.m_info.owner_before(m_info));
 }
 
+void
+EventId::reset() noexcept
+{
+  *this = {};
+}
+
 std::ostream&
 operator<<(std::ostream& os, const EventId& eventId)
 {
@@ -97,15 +111,14 @@
     this->scheduleNext();
   }
 
-  return EventId(*i);
+  return EventId(*this, *i);
 }
 
 void
-Scheduler::cancelEvent(const EventId& eventId)
+Scheduler::cancelImpl(const shared_ptr<EventInfo>& info)
 {
-  shared_ptr<EventInfo> info = eventId.m_info.lock();
   if (info == nullptr || info->isExpired) {
-    return; // event already expired or cancelled
+    return;
   }
 
   if (info->queueIt == m_queue.begin()) {
diff --git a/ndn-cxx/util/scheduler.hpp b/ndn-cxx/util/scheduler.hpp
index d5051b7..578d8ca 100644
--- a/ndn-cxx/util/scheduler.hpp
+++ b/ndn-cxx/util/scheduler.hpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 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_UTIL_SCHEDULER_HPP
 
 #include "ndn-cxx/detail/asio-fwd.hpp"
+#include "ndn-cxx/detail/cancel-handle.hpp"
 #include "ndn-cxx/util/time.hpp"
 
 #include <boost/system/error_code.hpp>
@@ -37,40 +38,47 @@
 
 namespace scheduler {
 
-/**
- * \brief Function to be invoked when a scheduled event expires
+class Scheduler;
+class EventInfo;
+
+/** \brief Function to be invoked when a scheduled event expires
  */
 using EventCallback = std::function<void()>;
 
-/**
- * \brief Stores internal information about a scheduled event
+/** \brief A handle of scheduled event.
+ *
+ *  \code
+ *  EventId eid = scheduler.scheduleEvent(10_ms, [] { doSomething(); });
+ *  eid.cancel(); // cancel the event
+ *  \endcode
+ *
+ *  \note Canceling an expired (executed) or canceled event has no effect.
+ *  \warning Canceling an event after the scheduler has been destructed may trigger undefined
+ *           behavior.
  */
-class EventInfo;
-
-/**
- * \brief Identifies a scheduled event
- */
-class EventId
+class EventId : public ndn::detail::CancelHandle
 {
 public:
-  /**
-   * \brief Constructs an empty EventId
-   * \note EventId is implicitly convertible from nullptr.
+  /** \brief Constructs an empty EventId
    */
-  constexpr
-  EventId(std::nullptr_t = nullptr) noexcept
+  EventId() noexcept = default;
+
+  /** \brief Allow implicit conversion from nullptr.
+   */
+  [[deprecated]]
+  EventId(std::nullptr_t) noexcept
   {
   }
 
-  /**
-   * \retval true The event is valid.
-   * \retval false This EventId is empty, or the event is expired or cancelled.
+  /** \brief Determine whether the event is valid.
+   *  \retval true The event is valid.
+   *  \retval false This EventId is empty, or the event is expired or cancelled.
    */
   explicit
   operator bool() const noexcept;
 
-  /**
-   * \return whether this and other refer to the same event, or are both empty/expired/cancelled
+  /** \brief Determine whether this and other refer to the same event, or are both
+   *         empty/expired/cancelled.
    */
   bool
   operator==(const EventId& other) const noexcept;
@@ -81,23 +89,14 @@
     return !this->operator==(other);
   }
 
-  /**
-   * \brief clear this EventId
-   * \note This does not cancel the event.
-   * \post !(*this)
+  /** \brief Clear this EventId without canceling.
+   *  \post !(*this)
    */
   void
-  reset() noexcept
-  {
-    m_info.reset();
-  }
+  reset() noexcept;
 
 private:
-  explicit
-  EventId(weak_ptr<EventInfo> info) noexcept
-    : m_info(std::move(info))
-  {
-  }
+  EventId(Scheduler& sched, weak_ptr<EventInfo> info);
 
 private:
   weak_ptr<EventInfo> m_info;
@@ -109,6 +108,38 @@
 std::ostream&
 operator<<(std::ostream& os, const EventId& eventId);
 
+/** \brief A scoped handle of scheduled event.
+ *
+ *  Upon destruction of this handle, the event is canceled automatically.
+ *  Most commonly, the application keeps a ScopedEventId as a class member field, so that it can
+ *  cleanup its event when the class instance is destructed.
+ *
+ *  \code
+ *  {
+ *    ScopedEventId eid = scheduler.scheduleEvent(10_ms, [] { doSomething(); });
+ *  } // eid goes out of scope, canceling the event
+ *  \endcode
+ *
+ *  \note Canceling an expired (executed) or canceled event has no effect.
+ *  \warning Canceling an event after the scheduler has been destructed may trigger undefined
+ *           behavior.
+ */
+class ScopedEventId : public ndn::detail::ScopedCancelHandle
+{
+public:
+  using ScopedCancelHandle::ScopedCancelHandle;
+
+  ScopedEventId() noexcept = default;
+
+  /** \deprecated Scheduler argument is no longer necessary. Use default construction instead.
+   */
+  [[deprecated]]
+  explicit
+  ScopedEventId(Scheduler& scheduler) noexcept
+  {
+  }
+};
+
 class EventQueueCompare
 {
 public:
@@ -118,8 +149,7 @@
 
 using EventQueue = std::multiset<shared_ptr<EventInfo>, EventQueueCompare>;
 
-/**
- * \brief Generic scheduler
+/** \brief Generic scheduler
  */
 class Scheduler : noncopyable
 {
@@ -129,37 +159,40 @@
 
   ~Scheduler();
 
-  /**
-   * \brief Schedule a one-time event after the specified delay
-   * \return EventId that can be used to cancel the scheduled event
+  /** \brief Schedule a one-time event after the specified delay
+   *  \return EventId that can be used to cancel the scheduled event
    */
   EventId
   scheduleEvent(time::nanoseconds after, const EventCallback& callback);
 
-  /**
-   * \brief Cancel a scheduled event
+  /** \brief Cancel a scheduled event
+   *
+   *  You may also invoke `eid.cancel()`
    */
   void
-  cancelEvent(const EventId& eventId);
+  cancelEvent(const EventId& eid)
+  {
+    eid.cancel();
+  }
 
-  /**
-   * \brief Cancel all scheduled events
+  /** \brief Cancel all scheduled events
    */
   void
   cancelAllEvents();
 
 private:
-  /**
-   * \brief Schedule the next event on the deadline timer
+  void
+  cancelImpl(const shared_ptr<EventInfo>& info);
+
+  /** \brief Schedule the next event on the deadline timer
    */
   void
   scheduleNext();
 
-  /**
-   * \brief Execute expired events
-   * \note If an event callback throws, the exception is propagated to the thread running the
-   *       io_service. In case there are other expired events, they will be processed in the next
-   *       invocation of this method.
+  /** \brief Execute expired events
+   *
+   *  If an event callback throws, the exception is propagated to the thread running the io_service.
+   *  In case there are other expired events, they will be processed in the next invocation.
    */
   void
   executeEvent(const boost::system::error_code& code);
@@ -168,6 +201,8 @@
   unique_ptr<detail::SteadyTimer> m_timer;
   EventQueue m_queue;
   bool m_isEventExecuting;
+
+  friend EventId;
 };
 
 } // namespace scheduler
diff --git a/ndn-cxx/util/segment-fetcher.cpp b/ndn-cxx/util/segment-fetcher.cpp
index 6596b74..bc537a5 100644
--- a/ndn-cxx/util/segment-fetcher.cpp
+++ b/ndn-cxx/util/segment-fetcher.cpp
@@ -257,8 +257,7 @@
   afterSegmentReceived(data);
 
   // Cancel timeout event
-  m_scheduler.cancelEvent(pendingSegmentIt->second.timeoutEvent);
-  pendingSegmentIt->second.timeoutEvent = nullptr;
+  pendingSegmentIt->second.timeoutEvent.cancel();
 
   m_validator.validate(data,
                        bind(&SegmentFetcher::afterValidationSuccess, this, _1, origInterest,
@@ -403,8 +402,7 @@
   }
 
   // Cancel timeout event and set status to InRetxQueue
-  m_scheduler.cancelEvent(pendingSegmentIt->second.timeoutEvent);
-  pendingSegmentIt->second.timeoutEvent = nullptr;
+  pendingSegmentIt->second.timeoutEvent.cancel();
   pendingSegmentIt->second.state = SegmentState::InRetxQueue;
 
   m_rttEstimator.backoffRto();
diff --git a/tests/unit/util/scheduler.t.cpp b/tests/unit/util/scheduler.t.cpp
index 5cd9f89..49c90b6 100644
--- a/tests/unit/util/scheduler.t.cpp
+++ b/tests/unit/util/scheduler.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2018 Regents of the University of California.
+ * Copyright (c) 2013-2019 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -20,7 +20,6 @@
  */
 
 #include "ndn-cxx/util/scheduler.hpp"
-#include "ndn-cxx/util/scheduler-scoped-event-id.hpp"
 
 #include "tests/boost-test.hpp"
 #include "tests/unit/unit-test-time-fixture.hpp"
@@ -235,8 +234,7 @@
 BOOST_AUTO_TEST_CASE(CancelAllWithScopedEventId) // Bug 3691
 {
   Scheduler sched(io);
-  ScopedEventId eid(sched);
-  eid = sched.scheduleEvent(10_ms, []{});
+  ScopedEventId eid = sched.scheduleEvent(10_ms, []{});
   sched.cancelAllEvents();
   eid.cancel(); // should not crash
 
@@ -253,10 +251,7 @@
 BOOST_AUTO_TEST_CASE(ConstructEmpty)
 {
   EventId eid;
-  eid = nullptr;
-  EventId eid2(nullptr);
-
-  BOOST_CHECK(!eid && !eid2);
+  BOOST_CHECK(!eid);
 }
 
 BOOST_AUTO_TEST_CASE(Compare)
@@ -264,14 +259,10 @@
   EventId eid, eid2;
   BOOST_CHECK_EQUAL(eid == eid2, true);
   BOOST_CHECK_EQUAL(eid != eid2, false);
-  BOOST_CHECK_EQUAL(eid == nullptr, true);
-  BOOST_CHECK_EQUAL(eid != nullptr, false);
 
   eid = scheduler.scheduleEvent(10_ms, []{});
   BOOST_CHECK_EQUAL(eid == eid2, false);
   BOOST_CHECK_EQUAL(eid != eid2, true);
-  BOOST_CHECK_EQUAL(eid == nullptr, false);
-  BOOST_CHECK_EQUAL(eid != nullptr, true);
 
   eid2 = eid;
   BOOST_CHECK_EQUAL(eid, eid2);
@@ -309,7 +300,6 @@
 
     // eid is "expired" during callback execution
     BOOST_CHECK(!eid);
-    BOOST_CHECK(eid == nullptr);
     BOOST_CHECK_NE(eid, eid2);
 
     scheduler.cancelEvent(eid2);
@@ -326,7 +316,6 @@
   EventId eid = scheduler.scheduleEvent(10_ms, [&isCallbackInvoked] { isCallbackInvoked = true; });
   eid.reset();
   BOOST_CHECK(!eid);
-  BOOST_CHECK(eid == nullptr);
 
   this->advanceClocks(6_ms, 2);
   BOOST_CHECK(isCallbackInvoked);
@@ -354,8 +343,7 @@
 {
   int hit = 0;
   {
-    ScopedEventId se(scheduler);
-    se = scheduler.scheduleEvent(10_ms, [&] { ++hit; });
+    ScopedEventId se = scheduler.scheduleEvent(10_ms, [&] { ++hit; });
   } // se goes out of scope
   this->advanceClocks(1_ms, 15);
   BOOST_CHECK_EQUAL(hit, 0);
@@ -364,8 +352,7 @@
 BOOST_AUTO_TEST_CASE(Assign)
 {
   int hit1 = 0, hit2 = 0;
-  ScopedEventId se1(scheduler);
-  se1 = scheduler.scheduleEvent(10_ms, [&] { ++hit1; });
+  ScopedEventId se1 = scheduler.scheduleEvent(10_ms, [&] { ++hit1; });
   se1 = scheduler.scheduleEvent(10_ms, [&] { ++hit2; });
   this->advanceClocks(1_ms, 15);
   BOOST_CHECK_EQUAL(hit1, 0);
@@ -376,8 +363,7 @@
 {
   int hit = 0;
   {
-    ScopedEventId se(scheduler);
-    se = scheduler.scheduleEvent(10_ms, [&] { ++hit; });
+    ScopedEventId se = scheduler.scheduleEvent(10_ms, [&] { ++hit; });
     se.release();
   } // se goes out of scope
   this->advanceClocks(1_ms, 15);
@@ -389,17 +375,15 @@
   int hit = 0;
   unique_ptr<ScopedEventId> se2;
   {
-    ScopedEventId se(scheduler);
-    se = scheduler.scheduleEvent(10_ms, [&] { ++hit; });
+    ScopedEventId se = scheduler.scheduleEvent(10_ms, [&] { ++hit; });
     se2 = make_unique<ScopedEventId>(std::move(se)); // move constructor
   } // se goes out of scope
   this->advanceClocks(1_ms, 15);
   BOOST_CHECK_EQUAL(hit, 1);
 
-  ScopedEventId se3(scheduler);
+  ScopedEventId se3;
   {
-    ScopedEventId se(scheduler);
-    se = scheduler.scheduleEvent(10_ms, [&] { ++hit; });
+    ScopedEventId se = scheduler.scheduleEvent(10_ms, [&] { ++hit; });
     se3 = std::move(se); // move assignment
   } // se goes out of scope
   this->advanceClocks(1_ms, 15);