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::anyor some other kind of type erasure. When internally passing thedurationobject 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_castandstd::time_point_castto cast any incoming type type tostd::chrono::nanosecondsorstd::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.