2

The C++ standard library in many places offers a kind of "awaitable" API: e.g. std::future and std::condition_variable can instantly "try" to get their value, "wait" undefinitely for their value, "wait_for" a certain std::chrono::duration, or "wait_until" a certain std::chrono::time_point is reached. I am struggling to create an abstract base class that captures these same operations.

template <typename T>
class awaitable {
 public:
  virtual std::optional<T> try() = 0;

  virtual std::optional<T> wait() = 0;

  template <typename Rep, typename Period>
  virtual std::optional<T> wait_for(const std::chrono::duration<Rep, Period>&) = 0;

  template <typename Clock, typename Duration>
  virtual std::optional<T> wait_until(const std::chrono::time_point<Clock, Duration>&) = 0;
};

try and wait are of no issue. wait_for and wait_until require template parameters and can therefore not be virtual.

Is there a "clean" way to define an interface like this?`

Some options I have considered which (unless I am missing something) do not seem viable:

  • Using std::any or some other kind of type erasure. When internally passing the duration object to another function, I'd still need to know the exact type to properly cast it.
  • Using a visitor pattern. This would require me to specify a hardcoded list of all types that derive from "Awaitable" in one central location, introducting circular dependencies and limiting extensibility.
  • Using std::duration_cast and std::time_point_cast to cast any incoming type type to std::chrono::nanoseconds or std::chrono::time_point<std::chrono::high_resolution_clock>, so there'd be a non-virtual templated method and a virtual non-templated method. This seems like it would introduce unwanted overhead and potential misbehavior, as I am unsure whether every possible incoming type is guaranteed to be castable to those common types.

So far, the third variant seems like it would be my only option at all, and not a great one.

Andreas T
  • 714
  • 5
  • 21

1 Answers1

1

I think your third way might be the way to go. In doubt, you have to apply some effort to limit possible misusage.

You can also take a look at the acyclic visitor pattern:

https://en.wikipedia.org/wiki/Visitor_pattern

Acyclic Visitor C++

For some scenarios, the implementation way provided by Andrei Alexandrescu (can really recommend his according book) helped me a lot. It requires some efforts to fully understand, is a bit intrusive and as far as I know, it's not possible to stay 100% macro free here at least for C++ < 14, but it has the huge advantage of decentralization you might need here. Also its minor dynamic_cast-usage (not misused as a dynamic switch) is not really an issue for the majority of use-cases and modern architectures.

Secundi
  • 804
  • 1
  • 1
  • 7