Search code examples
c++variantstatic-polymorphism

Static polymorphism using std::variant


I'm trying to implement something like static polimorphism using std::variant. I want to declare methods using VARIANT_METHOD or VARIANT_METHOD_CONST, that should take return type, method name, arguments, and qualifiers.

#include <variant>

#define VARIANT_METHOD(retType, name, ...) \
    template <typename... Args> retType name (Args&&... args) __VA_ARGS__ {                   \
    return std::visit([...args = std::forward<Args>(args)] <typename T>(T& self) -> retType { \
        return self.name(args...);                                                                   \
    }, static_cast<variant&>(*this));                                                                \
}
#define VARIANT_METHOD_CONST(retType, name, ...) template <typename... Args> \
    retType name (Args&&... args) __VA_ARGS__ {                                                    \
    return std::visit([...args = std::forward<Args>(args)]<typename T>(const T& self) -> retType { \
        return self.name(args...);                                                                        \
    }, static_cast<const variant&>(*this));                                                               \
}
#define VARIANT_FIELD(name) \
    decltype(auto) name() noexcept {                                \
        return std::visit([](auto& self) -> decltype(auto) {        \
            return self.name;                                       \
        }, static_cast<variant&>(*this));                           \
    }                                                               \
    decltype(auto) name() const noexcept {                          \
        return std::visit([](const auto& self) -> decltype(auto) {  \
            return self.name;                                       \
        }, static_cast<const variant&>(*this));                     \
    }

struct A {
    int field;
    int field2;

    int func(int a) const noexcept {
        return a + field;
    }
    int func(int a, int b) const noexcept {
        return a * a;
    }
};

struct B {
    int field2;
    int func(int a) const noexcept {
        return a * a;
    }
    int func(int a, int b) const noexcept {
        return a * a;
    }
};

struct C : protected std::variant<A, B> {
    using variant::variant;

    VARIANT_FIELD(field2);
    VARIANT_METHOD_CONST(int, func, const noexcept); // (1)
};

int main() {
    std::vector<C> vec;
    vec.emplace_back(A{.field = 0, .field2 = 1});
    vec.emplace_back(B{.field2 = 3});

    for (auto& c : vec) {
        c.func(10);
    }
}

I can't declare two methods with same names, but with different arguments. I want to write something like this:

VARIANT_METHOD_CONST(int, func, (int a), const noexcept);
VARIANT_METHOD_CONST(int, func, (int a, int b), const noexcept);

Solution

  • The cleanest way I know of is to abuse operator->* and make polymorphic method pointers. You can get the same syntax (except ->* instead of .), no macros.

    template<class F>
    struct poly_member {
      F f;
      friend decltype(auto) operator->*( auto&& t, poly_member const& self) {
        return self.f(decltype(t)(t));
      }
      template<class...Ts>
      friend decltype(auto) operator->*( std::variant<Ts...>& var, poly_member const& self ) {
        return std::visit( self.f, var );
      }
      template<class...Ts>
      friend decltype(auto) operator->*( std::variant<Ts...>&& var, poly_member const& self ) {
        return std::visit( self.f, var );
      }
      template<class...Ts>
      friend decltype(auto) operator->*( std::variant<Ts...> const& var, poly_member const& self ) {
        return std::visit( self.f, var );
      }
      template<class...Ts>
      friend decltype(auto) operator->*( std::variant<Ts...> const&& var, poly_member const& self ) {
        return std::visit( self.f, var );
      }
    };
    template<class F>
    poly_member(F)->poly_member<F>;
    template<class F>
    struct poly_method {
      F f;
      auto operator()(auto&&...args)const {
        return poly_member{[&](auto&& t)->decltype(auto){
          return f( decltype(t)(t), decltype(args)(args)... );
        }};
      }
      friend auto operator->*( auto&& t, poly_method const& self) {
        return [&](auto&&...args)->decltype(auto){
          return t->*self.f(decltype(args)(args)...);
        };
      }
      friend auto operator->*( auto* t, poly_method const& self) {
        return (*t)->*self;
      }
    };
    template<class F>
    poly_method(F)->poly_method<F>;
    

    a bit of alphabet soup. Replace the auto args with template<class T> etc for a version.

    You end up making the poly members like this:

    constexpr poly_member field { [](auto&& t)->decltype(auto){ return t.field; } };
    constexpr poly_member field2 { [](auto&& t)->decltype(auto){ return t.field2; } };
    constexpr poly_method func { [](auto&& t, auto&&...args)->decltype(auto){ return t.func(decltype(args)(args)...); } };
    

    and your code looks like:

    for (auto& c : vec) {
        c->*func(10);
    }
    

    now if you don't like writing those constexpr lambdas, you can just make them be written by a macro.

    Those poly methods and members work on any instance of any class that supports the lambda stored in them. The difference between members and methods is that methods require a trailing () and pass in those arguments, and members do not.

    For example:

    A a;
    a->*func(3);
    

    acts on an a just like it does on a variant<A, other stuff>.

    Live example.

    If you are really, really tied to your syntax, I'd advise looking at how google mock does it.

    I sometimes extend this and write

    template<auto*...methods>
    struct poly_any:private std::any {
      // code that makes the above alphabet soup look cute
    };
    

    and dynamically build vtables for the std::any I store to access each method, giving you:

    using C = poly_any< &field, &field2, &func >;
    C c = A{};
    std::cout << c->*field;
    std::cout << c->*func(2,3);
    

    where we type erase anything that supports the methods.

    But that is my private insanity. (this extension requires you mark up the poly members/methods with partial signature information).