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 theduration
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
andstd::time_point_cast
to cast any incoming type type tostd::chrono::nanoseconds
orstd::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.