Intro
I want to visit the value held by a class, which could be of multiple types. This class is very similar to a std::any
, so I will be using this in this question.
I wanted to create a reusable solution that accesses the value efficiently (without recursion, or unnecessary calls), that worked similarly to how std::visit
works for std::variant
.
Function
This is what I have come up with:
namespace detail {
struct retriever {
template <typename Type, typename Return, typename Visitor, typename Value>
static Return get(Visitor&& visitor, Value&& value) {
decltype(auto) innerValue = compatibility_layer<std::decay_t<Value>>::template get<Type>(std::forward<Value>(value));
return std::invoke(std::forward<Visitor>(visitor), std::move(innerValue));
}
};
}
template <typename... Alternatives, typename Visitor, typename Value>
constexpr std::common_type_t<std::invoke_result_t<Visitor, Alternatives>...> visit(Visitor&& visitor, Value&& value) {
using Return = std::common_type_t<std::invoke_result_t<Visitor, Alternatives>...>;
constexpr std::array<bool(*)(const std::decay_t<Value>&), sizeof...(Alternatives)> compatibilityResult = {
&compatibility_layer<std::decay_t<Value>>::template holds_alternative<Alternatives>...
};
if (auto compatibleType = std::find_if(std::begin(compatibilityResult), std::end(compatibilityResult), [&] <typename F> (F&& f) { return std::invoke(std::forward<F>(f), value); });
compatibleType != std::end(compatibilityResult)) {
constexpr std::array<Return(*)(Visitor&&, Value&&), sizeof...(Alternatives)> retrievers = {
&detail::retriever::get<Alternatives, Return, Visitor, Value>...
};
std::size_t index = std::distance(std::begin(compatibilityResult), compatibleType);
return retrievers[index](std::forward<Visitor>(visitor), std::forward<Value>(value));
}
else {
throw std::invalid_argument("Value does not hold any of the alternatives.");
}
}
The inputs to the function are a visitor and the value to be visited. Also, as template arguments, you have to provide which types should be tested for compatibility (the types to visit).
The function uses a compatibility_layer
class (which needs to be specialized for the type you want to support visiting (e.g., std::any
), to:
- Determine whether the value holds a specific type
- Retrieve a certain type from the value
For std::any
, the implementation would be like this:
template <>
struct compatibility_layer<std::any> {
template <typename T>
constexpr static bool holds_alternative(const std::any& value) {
return value.type() == typeid(T);
}
template <typename T>
constexpr static decltype(auto) get(const std::any& value) {
return std::any_cast<T>(value);
}
};
We don't invoke holds_alternative
for all the types provided in the template argument Alternatives
, but only until we find a true
value. We do this by iterating over a std::array
(populated at compile-time) holding pointers to the respective static methods holds_alternative
.
Similarly, we call get
at most once, only for that type that is compatible. To map the compatible type to the get
method to call, we create another std::array
(also populated at compile-time) that holds pointers to the get
methods. Then, we access the correct get
method based on the index of the compatible type from the previous array.
Example
In this example, I visit a std::any
and a std::variant
(just as a test - I know std::visit
should be used for variants).
namespace {
template <typename T>
void doVisit(T&& value) {
auto visitor = [](auto &&v) { std::cout << "Made it! " << v << std::endl; };
vac::visit<bool, int, double, std::string>(std::move(visitor), std::forward<T>(value));
}
}
int main() {
std::cout << "I am running" << std::endl;
// Any example.
std::any anyValue = std::string{"Any!"};
::doVisit(std::move(anyValue));
// Variant example.
std::variant<bool, int, double, std::string> variantValue = true;
::doVisit(std::move(variantValue));
}
The compatibility_layer
for the variant is similar:
template <typename... Ts>
struct compatibility_layer<std::variant<Ts...>> {
template <typename T>
constexpr static bool holds_alternative(const std::variant<Ts...>& value) {
return std::holds_alternative<T>(value);
}
template <typename T>
constexpr static decltype(auto) get(const std::variant<Ts...>& value) {
return std::get<T>(value);
}
};
Feedback
The function does what I need, but I am not sure whether it's any better than a recursive function like:
template <typename FirstAlternative typename... OtherAlternatives, typename Visitor, typename Value>
constexpr std::invoke_result_t<Visitor, FirstAlternative> visit(Visitor&& visitor, Value&& value) {
if (compatibility_layer<std::decay_t<Value>>::template holds_alternative<FirstAlternative>(value)) {
return detail::retriever::get<Alternatives, Return, Visitor, Value>(std::forward<Visitor>(visitor), std::forward<Value>(value));
}
else {
if constexpr (sizeof...(OtherAlternatives) == 0) {
throw std::invalid_argument("Value does not hold any of the alternatives.");
}
else {
return visit<OtherAlternatives...>(std::forward<Visitor>(visitor), std::forward<Value>(value));
}
}
}
I wanted to avoid recursing, mainly to simplify debugging (as our std::any
can hold 20 different types).