markf78
markf78

Reputation: 627

avoid writing the same repetitive type-checking code with std::any

I want to use std::any in my program but I find myself writing a lot of conditional statements like this:

   if (anything.type() == typeid(short)) {
      auto s = std::any_cast<short>(anything);
   } else if (anything.type() == typeid(int)) {
      auto i = std::any_cast<int>(anything);
   } else if (anything.type() == typeid(long)) {
      auto l = std::any_cast<long>(anything);
   } else if (anything.type() == typeid(double)) {
      auto d = std::any_cast<double>(anything);
   } else if (anything.type() == typeid(bool)) {
      auto b = std::any_cast<bool>(anything);
   } 

Note that I omitted much of the else if conditions for brevity.

My program can use any of the defined types that can be stored in std::any so these if-then statements are quite long. Is there a way to refactor the code so that I can write it once?

My original inclination was to use templates like so:

template<typename T>
T AnyCastFunction(std::any) {

   T type;
   if (anything.type() == typeid(short)) {
      type = std::any_cast<short>(anything);
   } else if (anything.type() == typeid(int)) {
      type = std::any_cast<int>(anything);
   } else if (anything.type() == typeid(long)) {
      type = std::any_cast<long>(anything);
   } else if (anything.type() == typeid(double)) {
      type = std::any_cast<double>(anything);
   } else if (anything.type() == typeid(bool)) {
      type = std::any_cast<bool>(anything);
   } 

   return type;
}

However, this leads to "couldn't deduce template parameter T" errors. How can I refactor this to avoid writing the large if/else blocks many times throughout the program?

Upvotes: 0

Views: 830

Answers (4)

Dietmar K&#252;hl
Dietmar K&#252;hl

Reputation: 154005

The basic idea is to create an std::any visitor and do the necessary processing in a function called from the visitor. That basic principle is straight forward. Let's start with supporting just one type:

#include <any>
#include <iostream>
#include <type_traits>

template <typename T, typename Any, typename Visitor>
auto any_visit1(Any&& any, Visitor visit)
    -> std::enable_if_t<std::is_same_v<std::any, std::decay_t<Any>>>
{
    if (any.type() == typeid(T)) {
        visit(std::any_cast<T>(std::forward<Any>(any)));
    }
}

int main() {
    std::any a0(17);

    any_visit1<int>(a0, [](auto value){ std::cout << "value=" << value << "\n"; });
}

The next step is to remove the one type restriction. As the explicit template parameters come first and are an open-ended list and the function object should be a deduced template parameter, you can't quite use a function template. However, a variable template (with an inline constexpr, of course, hence variable...) does the trick:

#include <any>
#include <iostream>
#include <type_traits>

template <typename... T>
inline constexpr auto any_visit =
    [](auto&& any, auto visit) -> std::enable_if_t<std::is_same_v<std::any, std::decay_t<decltype(any)>>> {
    (
    (any.type() == typeid(T) && (visit(std::any_cast<T>(std::forward<decltype(any)>(any))), true))
    || ...)
    // Uncomment the line below to have visit(any) called for unhandled types
    // || (visit(std::forward<decltype(any)>(any)), true)
    ;
};

void test(std::any any)
{
    any_visit<int, double, char const*>(any, [](auto value){ std::cout << "value=" << value << "\n"; });
}

int main() {
    test(17);
    test(3.14);
    test(+"foo");
}

If you need multiple std::any objects decoded you'd just pass suitable [lambda?] functions into it which refer to the other objects and keep building up the object until you got all the ones you need.

Upvotes: 1

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275820

I find this type of code fun to write.

any_visitor<types...> is a function object that visits a set of types.

You invoke it with an any followed by a function object. It then invokes the function object with whichever of the types... is in the any.

So you do any_vistor<int, double>{}( something, [](auto&& x) { /* some code */ } ).

If none of the types... are in the any, it invokes the function object with a std::any for you to deal with the extra case.

We can also write a variant that instead of passing the std::any to the functor, throws or returns false or something.

template<class...Ts>
struct any_visitor;

template<>
struct any_visitor<> {
    template<class F>
    decltype(auto) operator()( std::any& a, F&& f ) const {
        return std::forward<F>(f)(a);
    }
};

template<class...Ts>
struct any_visitor {
private:
    struct accum {
        std::size_t x = 0;
        friend accum operator+( accum lhs, accum rhs ) {
            if (lhs.x || rhs.x) return {lhs.x+1};
            else return {};
        }
    };
public:
    template<class Any, class F>
    void operator()(Any&& any, F&& f) const {
        // sizeof...(Ts) none in the list
        // otherwise, index of which any is in the list
        std::size_t which = sizeof...(Ts) - (accum{} + ... + accum{ any.type() == typeid(Ts) }).x;

        using table_entry = void(*)(Any&&, F&&);

        static const table_entry table[] = {
            +[](Any&& any, F&& f) {
                std::forward<F>(f)( std::any_cast<Ts>( std::forward<Any>(any) ) );
            }...,
            +[](Any&& any, F&& f) {
                std::forward<F>(f)( std::forward<Any>(any) );
            }
        };

        table[which]( std::forward<Any>(any), std::forward<F>(f) );
    }
};

template<class...Fs>
struct overloaded:Fs... {
    using Fs::operator()...;
};
template<class...Fs>
overloaded(Fs&&...)->overloaded<std::decay_t<Fs>...>;

I also included overloaded which makes it easier to dispatch. If you want to handle all types uniformly, except handle an error case, you can do:

overloaded{
  [](auto const& x){ std::cout << x << "\n"; },
  [](std::any const&){ std::cout << "Unknown type\n"; }
}

and pass that as the function object to any_visitor.

Here is some test code:

std::any foo=7;
std::any bar=3.14;

auto visitor = overloaded{
    [](int x){std::cout << x << "\n";},
    [](auto&&){std::cout << "Unknown\n";}
};
any_visitor<int>{}( foo, visitor );
any_visitor<int>{}( bar, visitor );

which outputs:

7
Unknown

Live example.

Implementation wise, this code uses a dispatch table (sort of like a vtable) to map the index of the type stored in the any to which overload of the function object we invoke.


Yet another approach would be to write:

template<class...Ts>
std::optional<std::variant<Ts...>> to_variant( std::any );

which converts a std::any to a variant if its types match. Then use the usual visiting machinery on std::variant instead of rolling your own.

Upvotes: 1

Barry
Barry

Reputation: 303537

If you have a known, fixed list of possible types, don't use std::any. Use std::variant<Ts...>. That makes Dietmar's answer look like this:

#include <variant>

void test(std::variant<int, double, char const*> v)
{
    std::visit([](auto value){ std::cout << "value=" << value << "\n"; }, v);
}

which is the same thing, except (a) you don't have to implement visit yourself (b) this is massively more efficient at runtime and (c) this is type safe - you can't forget to check a particular type! Really even if you don't care about (a) or (b), (c) is a huge win.

And if you don't have a known, fixed list of possible types - which is the typical use-case for wanting std::any - then anything you're doing with std::any doesn't make sense anyway. You can't enumerate all possible copyable types (there are an infinite amount of them), so you can't necessarily retrieve the contents. So I really think variant is what you want.

Upvotes: 3

bipll
bipll

Reputation: 11940

Well, if you're sure you need such a broad range stored in any...

template<typename T> void visit(T &&t) { std::cout << "Hi " << t << "!\n"; }

void try_visit(std::any &&) { std::cout << "Unknown type\n"; }

template<typename T, typename... Ts> void try_visit(std::any thing) {
     if(thing.type() == typeid(T)) {
         visit(std::any_cast<T>(thing));
         return;
     }
     if constexpr(sizeof...(Ts) > 0) try_visit<Ts...>(std::move(thing));
     else try_visit(std::move(thing));
}

int main() {
    try_visit<short, int, double, bool, long>(std::any{42});
}

%-}

Upvotes: 1

Related Questions