3
\$\begingroup\$

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

\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

I could not find any issues with this code, except a slightly alternative way to implement it:

Avoiding arrays

I wanted to avoid recursing, mainly to simplify debugging (as our std::any can hold 20 different types).

Although recursion might actually result in the cleanest code here, I can see the point. However, creating arrays of function pointers is also not very nice in my opinion. The usual way you replace recursive templates since C++17 is by using fold expressions. We can do that here as well. Consider:

template <typename... Alternatives, typename Visitor, typename Value>
constexpr auto visit(Visitor &&visitor, Value &&value)
{
    using Return = std::common_type_t<std::invoke_result_t<Visitor, Alternatives>...>;
    Return (*retriever)(Visitor &&, Value &&) = {};

    ([&]{
        if (compatibility_layer<std::decay_t<Value>>::template holds_alternative<Alternatives>(value)) {
            retriever = &detail::retriever::get<Alternatives, Visitor, Value>;
        }
    }(), ...);

    if (retriever) {
        return retriever(std::forward<Visitor>(visitor), std::forward<Value>(value));
    } else {
        throw std::invalid_argument("Value does not hold any of the alternatives.");
    }
}

This could be improved by letting the lambda return a boolean, so you can short-circuit using ||.

It is very unfortunate that std::optional<void> is not valid, otherwise I think having a std::optional<Return> result variable would make this code even cleaner.

Visiting multiple variables at the same time

If you look at the documentation of std::visit(), you'll notice that it can take multiple variables as arguments. Consider that you might have a visitor that takes two or more arguments, like in:

std::variant<int, float> foo = 42;
std::any bar = 3.1415;
auto sum = visit<int, float, double>(
    [](auto a, auto b) -> double {return a + b;},
    foo, bar);

This is doable, but would of course complicate your code even more.

\$\endgroup\$
1
  • \$\begingroup\$ That is a great simplification! I have implemented the lambda expansion with short-circuiting and it does the job. Thanks for suggesting it! \$\endgroup\$ Oct 20, 2022 at 18:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.