I am building a system where a top layer communicates with a driver layer, who in turn communicate with a I2C layer. I have put my I2C driver behind a message queue, in order to make it thread safe and serialize access to the I2C bus. In order to return the reply to the driver, the I2C layer returns a std::future with a byte buffer inside that is filled out when the I2C bus read actually happens.
All this works and I like it.
My problem is that I also want the driver to return a future to the top layer, however this future will then depend on the previous future (when the I2C driver future-returns a byte buffer, the driver will have to interpret and condition those bytes to get the higher-level answer), and I am having problems making this dependency "nice".
For example, I have a driver for a PCT2075 temperature sensor chip, and I would like to have a:
future<double> getTemperature()
method in that driver, but so far I can't think of a better way than to make an intermediate "future-holder" class and then return that:
class PCT2075
{
public:
class TemperatureFuture
{
private:
std::future<std::pair<std::vector<uint8_t>, bool>> temperatureData;
public:
TemperatureFuture(std::future<std::pair<std::vector<uint8_t>, bool>> f);
template< class Clock, class Duration >
std::future_status wait_until(const std::chrono::time_point<Clock, Duration>& timeout_time) const;
void wait() const; // wait and wait_until just waits on the internal future
double get();
};
TemperatureFuture getTemperature();
};
This structure works and I can go forward with it, but for some reason I am not super happy with it (though I can't quite explain why... :/ ).
So my questions are:
- Is there some pattern that can make this better?
- Would it make sense to let
TemperatureFuture
inherit directly fromstd::future
(I have heard that "do not inherit from std classes" is a good rule)? - Or is this just how you do it, and I should stop worrying about nothing?
Ps. I also have another method whose answer relies on two I2C reads, and thus two different futures. It is possible to rework this to only have a one-on-one dependency, but the current way can handle the one-on-multiple variant so it would be nice if a potential new proposal also could.
CodePudding user response:
A future that just reinterprets the results of a previous future or gathers multiple futures is a good job for std::async(std::launch::deferred, ...)
. This doesn't launch any thread, it executes on request.
std::future<int> f1 = std::async([]() -> int { return 1; });
std::future<float> f2 = std::async(
std::launch::deferred,
[](std::future<int>&& f) -> float { return f.get(); },
std::move(f1));
std::printf("%f\n", f2.get());
The downside is that certain features will not work, e.g. wait_until
.
If, instead, you need to launch a new asynchronous action once the first future is ready (e.g. send another I2C message, or compute the higher-level result in a thread pool), C does not offer any better solution than making this part of your original task. For example your I2C driver could accept a list of std::functions as callbacks.
CodePudding user response:
You are looking for an operation called then
, which as commenters note is sadly missing even in C 20.
However, it's not hard to write a then
yourself.
template<typename Fun, typename... Ins>
std::invoke_result_t<Fun, Ins...> invoke_future(Fun fun, std::future<Ins>... futs) {
return fun(futs.get()...);
}
template<typename Fun, typename... Ins>
std::future<std::invoke_result_t<Fun, Ins...>> then(Fun&& fun, std::future<Ins>... futs) {
return std::async(std::launch::deferred, invoke_future<Fun, Ins...>, std::forward<Fun>(fun), std::move(futs)...);
}
I expect something like this wasn't standardised because it makes loads of assumptions about how the function should be run once the result is ready.