Let me describe a scenario, first we have a function which returns us some sort of data which we can't be sure about the validity of, i.e.
auto random_predicate() -> bool
{
int v = uniform_dist(e1); // uniform distribution 1 to 100
return (v % 5);
}
where uniform_dist()
is an appropriately seeded uniform distribution, and we have an enum class
which we shall use for error handling, i.e.
enum class Error
{
ValueError
};
Then we perform some sort of views-based processing which uses random_predicate()
from within an operation, as follows:
std::vector<int> vs{1,2,3,4,5};
auto result = vs
| views::filter([](int i){ return i % 2; })
| views::transform([](int i) -> std::expected<int, Error> {
auto v = random_predicate();
if (v) return std::unexpected<Error>(Error::ValueError);
else return i * i;
});
So, by the end of this operation, we can assert
static_assert(
std::is_same_v<
std::decay_t<std::ranges::range_value_t<result>>,
std::expected<int, Error>
>
)
will in fact be true.
The question is, then what? We know have a view of std::expected
values which we need to resolve to either: an error type which propagates up the call stack, or a view of the success type (i.e. a view of int
in the above example (with all elements not a multiple of 5!))
My solution is to simply check if each element is in error and then if there are no errors transform the result to the desired view, so something like
template<typename T>
static auto has_error(const std::expected<T, Error>& e){ return !e.has_value(); };
auto f(const std::vector<int>& vs)
{
auto c = vs
| views::filter([](int i){ return i % 2; })
| views::transform([](int i) -> std::expected<int, Error> {
auto v = random_predicate();
if (v) return std::unexpected<Error>(Error::ValueError);
else return i * i;
});
if (auto v = ranges::find_if(c, has_error<int>); v != c.end())
{
return (*v).error();
}
else
{
return c | views::transform([](auto&& e){ return e.value(); });
}
}
But then we run into the problem that the function cannot deduce the return type to be std::expected<T, Error>
where T
is the type of a container with elements of type (in the case of the above example) int
. And well, I dont even know what to write for T
here, so my question is how should this be implemented?
godbolt: https://godbolt.org/z/Wfjr8o3qM
Alternatively, I'm interested in hearing how others may approach this problem in a better way all together?
Thanks
Edit: I suppose, you dont really want to return a view of some elements as it may lead to a dangling view? In that case, is it best to just use ranges::to<T>()
when returning from a function?
Another option to still be able to return the range:
auto f(const std::vector<int> &vs) {
auto c = vs
| views::filter([](int i) { return i % 2; })
| views::transform([](int i) -> std::expected<int, Error> {
auto v = random_predicate();
if (v) return std::unexpected<Error>(Error::ValueError);
else return i * i;
});
auto values = c | views::transform([](auto &&e) { return e.value(); });
using success_t = decltype(values);
using ret_t = std::expected<success_t, Error>;
if (auto v = ranges::find_if(c, has_error<int>); v != c.end()) {
return ret_t(std::unexpected<Error>((*v).error()));
} else {
return ret_t(values);
}
}
It uses the fact that values as it is a view is computed lazily. So if it is not returned, values will never be computed, and we can use it to determine the return type. Next step is to ensure all returns are wrapped into the ret_t
type so that auto can guess correctly.
Note: both in this answer and your original question, this only works in the original range can be iterated on multiple time (can't remember the name of this concept sorry)