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:
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.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.
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
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.