Using C++ 17. I have the following:

template <typename T>
using ptr_t = std::shared_ptr<const T>;

class some_type;

class A { some_type foo() const; }
class B { some_type foo() const; }
class C { some_type foo(int) const; }

std::variant<ptr_t<A>, ptr_t<B>, ptr_t<C>>;

A variant holds shared_ptr(s) to different types. All expected to have function foo() that may be void or take a parameter. I will then have a visitor that would correctly dispatch foo, something like this (conceptually):

struct visitor
{
  template <typename T>
  ptr_t<some_type> operator()(const T& config) const
  {
    if constexpr (// determine if foo() of the underlying type of a shared_ptr can be called with int param)
        return config->foo(15);
    else
        return config->foo();
  }

is there a way to say this? I tried various ways but can't come with something that compiles. Template parameter, T, is ptr_t<A|B|C>.

1

There are 1 best solutions below

2
On
  1. std::is_invocable_v<Callable, Args...> is the way to go. Unfortunatelly, it will not compile just like that with if constexpr. It will either fail because "there is no operator()() overload", or there is no overload for operator taking Args....

  2. I suggest you add a wrapper class for a callable and use it with a specialized alias template of std::variant instead of writing your own visitor. It will allow you to use std::visit seamlessly.


#include <type_traits>
#include <variant>

template <typename Callable>
class wrapped_callable
{
    Callable c;
public:
    wrapped_callable(Callable c)
        : c(c)
    {}

    template <typename ... Args>
    constexpr decltype(auto) operator()(Args &&... args) const
    {
        return _invoke(std::is_invocable<Callable, Args...>{}, c, std::forward<Args>(args)...);
    }

private:
    using _invocable = std::true_type;
    using _non_invocable = std::false_type;

    template <typename T, typename ... Args>
    constexpr static decltype(auto) _invoke(_invocable, const T& t, Args &&... args)
    {
        return t(std::forward<Args>(args)...);
    }

    template <typename T, typename ... Args>
    constexpr static decltype(auto) _invoke(_non_invocable, const T& t, Args ... args)
    {
        return t();
    }
};

template <typename ... T>
using variant_callable = std::variant<wrapped_callable<T>...>;

struct int_callable
{
    int operator()(int i) const
    {
        return i;
    }
};

struct non_callable
{
    int operator()() const
    {
        return 42;
    }
};

#include <iostream>

int main()
{
    using variant_t = variant_callable<int_callable, non_callable>;

    // 23 is ignored, 42 is printed
    std::visit([](const auto &callable){
        std::cout << callable(23) << '\n';
    }, variant_t{non_callable()});

    // 23 is passed along and printed
    std::visit([](const auto &callable){
        std::cout << callable(23) << '\n';
    }, variant_t{int_callable()});
}
Program returned: 0
42
23

https://godbolt.org/z/e6GzvW6n6

But The idea is not to have any specialization for all types in a variant as it will then require changing the visitor code every time a new type is added. That is what template alias of std::variant<wrapped_callable<T>...> for. You just add append a new type to the list, that's it.

Take notice, that it does not depend on if constexpr. So if you manage to provide your own variant and is_invocable_v, it will work for C++14. For C++11 possibly, but some modifications regarding constexpr functions might be needed.


Of course you can implement your visitor in the same manner if you want to use std::shared_ptr istead of a callable. But I don't see any reason to use:

  • visitor + smart pointer. Just use a smart pointer - it will give you runtime polymorphism in a "classic" way (via virtual inheritence)
  • why std::shared_ptr? Do you really need to share the ownership? Just stick with std::unique_ptr