Calling parametrised method on list items with different template parameters

184 views Asked by At

I'm trying to store and manipulate a list of template class objects with different parameter types; the template class has two parametrised methods, one returning the parameter type and a void one accepting it as input.

More specifically, I have a template class defined as follows:

template<typename T>
class Test
{
public:
    virtual T a() = 0;
    virtual void b(T t) = 0;
};

And different specifications of it, such as:

class TestInt : public Test<int>
{
public:
    int a() {
        return 1;
    }
    void b(int t) {
        std::cout << t << std::endl;
    }
};

class TestString : public Test<std::string>
{
public:
    std::string a() {
        return "test";
    }
    void b(std::string t) {
        std::cout << t << std::endl;
    }
};

I'd like to be able to store in one single list different objects of both TestInt and TestString type and loop through it calling one method as input for the other, as in:

for (auto it = list.begin(); it != list.end(); ++it)
    (*it)->b((*it)->a());

I've looked into boost::any but I'm unable to cast the iterator to the specific class, because I don't know the specific parameter type of each stored object. Maybe this cannot be done in a statically typed language as C++, but I was wondering whether there could be a way around it.

Just for the sake of completeness, I'll add that my overall aim is to develop a "parametrised observer", namely being able to define an observer (as with the Observer Pattern) with different parameters: the Test class is the observer class, while the list of different types of observers that I'm trying to properly define is stored within the subject class, which notifies them all through the two methods a() and b().

2

There are 2 answers

2
sehe On BEST ANSWER

The virtuals have actually no meaning here, since for each T the signatures are distinct.

So it seems you have Yet Another version of the eternal "how can we emulate virtual functions templates" or "how to create an interface without virtual functions":

The first one basically contains an idea that you could employ here.

Here's an idea of what I'd do:

Live On Coliru

#include <algorithm>
#include <iostream>

namespace mytypes {
    template <typename T>
    struct Test {
        T a() const;
        void b(T t) { std::cout << t << std::endl; } 
    };

    template <> int         Test<int>::a()         const { return 1;      }
    template <> std::string Test<std::string>::a() const { return "test"; }

    using TestInt    = Test<int>;
    using TestString = Test<std::string>;
}

#include <boost/variant.hpp>

namespace mytypes { 
    using Value = boost::variant<int, std::string>;

    namespace detail {
        struct a_f : boost::static_visitor<Value> {
            template <typename T>
            Value operator()(Test<T> const& o) const { return o.a(); }
        };
        struct b_f : boost::static_visitor<> {
            template <typename T>
            void operator()(Test<T>& o, T const& v) const { o.b(v); }

            template <typename T, typename V>
            void operator()(Test<T>&, V const&) const {
                throw std::runtime_error(std::string("type mismatch: ") + __PRETTY_FUNCTION__);
            }
        };
    }

    template <typename O>
    Value a(O const& obj) {
        return boost::apply_visitor(detail::a_f{}, obj);
    }

    template <typename O, typename V>
    void b(O& obj, V const& v) {
        boost::apply_visitor(detail::b_f{}, obj, v);
    }
}

#include <vector>

int main()
{
    using namespace mytypes;
    using AnyTest = boost::variant<TestInt, TestString>;

    std::vector<AnyTest> list{TestInt(), TestString(), TestInt(), TestString()};

    for (auto it = list.begin(); it != list.end(); ++it)
        b(*it, a(*it));
}

This prints

1
test
1
test

Bonus Points

If you insist, you can wrap the AnyTest variant into a proper class and have a() and b(...) member functions on that:

Live On Coliru

int main()
{
    using namespace mytypes;
    std::vector<AnyTest> list{AnyTest(TestInt()), AnyTest(TestString()), AnyTest(TestInt()), AnyTest(TestString())};

    for (auto it = list.begin(); it != list.end(); ++it)
        it->b(it->a());
}
0
Daniel Jour On

Expanding on my comment above, the simplest what I can currently think of to achieve what you are trying to do - at least as I understood it from your example code - is the following:

/* Interface for your container, better not forget the destructor! */
struct Test {
  virtual void operate(void) = 0;
  virtual ~Test() {}
};
/* Implementation hiding actual type */
template<typename T>
struct TestImpl : public T, public Test {
  void operate(void) {
    T::b(T::a());
  }
};

/* Actual code as template policies */
struct IntTest {
  int a(void) {
    return 42;
  }
  void b(int value) {
    std::cout << value << std::endl;
  }
};
struct StringTest {
  std::string a(void) {
    return "Life? Don't talk to me about life.";
  }
  void b(std::string value) {
    std::cout << value << std::endl;
  }
};

You would then need to create a container for objects of class Test and fill it with objects of the respective TestImpl<IntTest>, TestImpl<StringTest>, and so on. To avoid object slicing you need reference or pointer semantics, that is std::vector<std::unique_ptr<Test> > for example.

for (auto it = list.begin(); it != list.end(); ++it) {
  (*it)->operate();
}