FinanceGuyThatCantCode
FinanceGuyThatCantCode

Reputation: 391

Variadic template where we want to automate the creation of operator@ where it exists for each template variable

Suppose that we have a template like below, but we would prefer that it be variadic and we can automate the creation of the various operators that should be implemented - How do we approach this?

template <typename T1, typename T2, typename T3, typename T4>
class Foo { 
private:
    boost::variant<T1, T2, T3, T4> var_;
public:
    Foo(const T1& t1) : var_(t1) { }
    Foo(const T2& t2) : var_(t2) { }
    Foo(const T3& t3) : var_(t3) { }
    Foo(const T4& t4) : var_(t4) { }

    Foo operator@(const T1& t1) const { return t1 @ boost::get<T1>(var_);}
    Foo operator@(const T2& t2) const { return t2 @ boost::get<T2>(var_);}
    Foo operator@(const T3& t3) const { return t3 @ boost::get<T3>(var_);}
    Foo operator@(const T4& t4) const { return t4 @ boost::get<T4>(var_);}
    Foo operator@(const Foo& foo) const
    {
        switch (foo.which())
        {
         case 0: return *this @ boost::get<T1>(foo.var_);
         case 1: return *this @ boost::get<T2>(foo.var_);
         case 2: return *this @ boost::get<T3>(foo.var_);
         case 3: return *this @ boost::get<T4>(foo.var_);
         }
     }
};

int main()
{
    T1 t11, t12;
    Foo<T1, T2, T3, T4> foo1(t11), foo2(t12);
    Foo<T1, T2, T3, T4> foo = foo1 @ foo2;
    Foo<T1, T2, T3, T4> foo11 = t11 @ foo2;
    Foo<T1, T2, T3, T4> foo12 = foo1 @ t12;
    // assuming we have implemented operator==, I would expect that
    // foo, f11, f12 were all equal here.

    return 0;
}

I'm leaving out some type checking and other things for brevity. Hopefully, the point is clear.

Using std::enable_if_t, I'm sure we can figure out how to know when operator@ should be implemented for each T_i. However, is there a clever way to use variadic templates to create such a class and have all the operators I want overloaded in the canonical way? I don't specifically need to do this really as there is only one version of such a variadic template that I would use 99.9% of the time, but I think it is an interesting question.

Upvotes: 1

Views: 108

Answers (3)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

I'll solve this for + and +=.

template<class D, std::size_t I, class T>
struct plus_helper {
  D& operator+=( T const& rhs ) & {
    using boost::get;
    get<I>(self()) += rhs;
    return self();
  }

  friend T operator+( plus_helper<D,I,T>&& self, T const& rhs ) {
    using boost::get;
    return get<I>(std::move(self.self())) + rhs;
  }

  friend T operator+( plus_helper<D,I,T>const& self, T const& rhs ) {
    using boost::get;
    return get<I>(self.self()) + rhs;
  }

private:
  D const& self() const { return *static_cast<D const*>(this); }
  D & self() { return *static_cast<D*>(this); }
};


template<class D, class Indexes, class...Ts>
struct plus_helpers;

template<class D, std::size_t...Is, class...Ts>
struct plus_helpers<D, std::index_sequence<Is...>, Ts...> : plus_helper<D, Is, Ts>...
{
  template<typename T>
  T& get() {return boost::get<T>(self().var_);}
  template<typename T>
  const T& get() const {return boost::get<T>(self().var_);}

  using own_type = plus_helpers<D, std::index_sequence<Is...>, Ts...>;
  //using plus_helper<D,Is,Ts>::operator+...;
  using plus_helper<D,Is,Ts>::operator+=...;

  D& operator+=( D const& rhs )& {
    using fptr = void(*)( D& lhs, D const& rhs );

    // dispatch table: (or use boost::visit)
    static constexpr fptr table[] = {
      (+[]( D& lhs, D const& rhs ) {
        using boost::get;
        lhs += get<Is>(rhs);
      })...
    };

    table[rhs.which()]( self(), rhs );

    return self();
  }

  friend D operator+(own_type&& lhs, D const& rhs ) {
    lhs += rhs;
    return std::move(lhs.self());
  }

  friend D operator+(own_type const& lhs, D const& rhs ) {
    auto tmp = lhs.self();
    return std::move(tmp)+rhs;
  }

private:
  D& self() { return *static_cast<D*>(this); }
  D const& self() const { return *static_cast<D const*>(this); }
};

this uses one bit of -- the using /*[...]*/::operator+...;. To do this in you need to build a tree (possibly a linear tree) of plus_helper and binary using operator+ at each bud.

You are likely to have similar code for each operator; but not identical. + and == are not the same because you want to support += but not ===. ;)

You'll probably want a few macros that spew out most of the above boilerplate.

In the final class we do:

template<class...Ts>
class Foo:
 public plus_helpers<Foo<Ts...>, std::index_sequence_for<Ts...>, Ts...>
{
  boost::variant<Ts...> var_;
public:
  template<std::size_t I>
  friend decltype(auto) get( Foo<Ts...> const& foo ) {
    using boost::get;
    return get<std::tuple_element_t<I, std::tuple<Ts...>>>(foo.var_);
  }
  template<std::size_t I>
  friend decltype(auto) get( Foo<Ts...> & foo ) {
    using boost::get;
    return get<std::tuple_element_t<I, std::tuple<Ts...>>>(foo.var_);
  }
};

You can add type-based get:

template<class T>
friend decltype(auto) get( Foo<Ts...> & foo ) {
    using boost::get;
    return get<T>(foo.var_);
}
template<class T>
friend decltype(auto) get( Foo<Ts...>const & foo ) {
    using boost::get;
    return get<T>(foo.var_);
}

as well.

Live example.


A naive inheritance/using replacement:

template<class...Bases>
struct inherit_plus_operations {}; // empty
template<class Lhs, class...Rhs>
struct inherit_plus_operations<Lhs, Rhs...>:
  Lhs,
  inherit_plus_operations<Rhs...>
{
  //using Lhs::operator+;
  //using inherit_plus_operations<Rhs...>::operator+;
  using Lhs::operator+=;
  using inherit_plus_operations<Rhs...>::operator+=;
};
template<class Lhs> // one
struct inherit_plus_operations<Lhs>:
  Lhs
{
  //using Lhs::operator+;
  using Lhs::operator+=;
};

template<class D, std::size_t...Is>, class...Ts>
struct plus_helpers<D, std::index_sequence<Is...>, Ts...>:
  inherit_plus_operations<plus_helper<D, Is, Ts>...>

then get rid of using /* ... */::operator+/*...*/...; from body.

Upvotes: 2

Jarod42
Jarod42

Reputation: 217225

If you can get rid of "optimized" version with direct T1, T4, you might simply use boost::apply_visitor(/std::visit in C++17 with std::variant) :

template <typename... Ts>
class Foo { 
private:
    boost::variant<Ts...> var_;
public:
    // ...

    friend decltype(auto) operator@(const Foo& lhs_foo, const Foo& rhs_foo)
    {
        return boost::apply_visitor([](auto&& lhs, auto&& rhs) { return lhs @ rhs; },
                                    lhs_foo.var_,
                                    rhs_foo.var_);
    }
};

Upvotes: 3

lubgr
lubgr

Reputation: 38267

An approach to keep the amount of boilerplate as small as possible is to allow the implicit construction of Foo instances. This way, you don't even have to write any operator overload for a type other than Foo. As an example, consider the following template.

template <typename ...Args> class Foo { 
    public:
        // Evil shoot-yourself-in-the-foot-catch-all ctor:
        template <class T> Foo(T&& t) : var(std::forward<T>(t)) {}

        friend bool operator == (const Foo<Args...>& lhs, const Foo<Args...>& rhs)
        {
            return lhs.var == rhs.var;
        }

    private:
        std::variant<Args...> var;
};

It allows any argument that can be used to construct the data member var to implicitly instantiate a Foo instance suitable for the operator ==.

Foo<int, double, std::string> f1(42);
Foo<int, double, std::string> f2(43);

assert(f1 == f1);
assert(!(f1 == f2));

assert(42 == f1);
assert(!(f1 == 1.2345));
assert(!(f1 == "hello"));

Upvotes: 1

Related Questions