Mike Lischke
Mike Lischke

Reputation: 53367

How to store different specialized template classes in the same member variable?

Assume I have a class A that I want to store in an unordered_set with custom hasher and comparer. I also have a container class B that stores this set:

class B {
private:
  std::unordered_set<A, Hasher, Comparer> set;
};

To make this compile I would have to make B a template class, which I want to avoid, as this would lead to some major refactoring and actually moves this problem just a layer up where I would then have to handle template parameters for B.

Next I tried to make class that specialize the set:

class MySet1 : public std::unordered_set<A, MyHasher1, MyComparer1> {
};
class MySet2 : public std::unordered_set<A, MyHasher2, MyComparer2> {
};

Obviously that doesn't help as I still have no common base class for my set var in class B.

To solve this I moved down the unordered set one level:

class MySet {
public:
  // Some abstract functions...
};

class MySet1 : public MySet {
public:
  // Implementation of the abstract functions.
private:
  std::unordered_set<A, MyHasher1, MyComparer1> set;
};

class MySet2 : public MySet {
public:
  // Implementation of the abstract functions.
private:
  std::unordered_set<A, MyHasher2, MyComparer2> set;
};

Now I have a common base class (MySet) for class B. But the obvious disadvantages are: code duplication for each set specialization and I would have to imlement custom iterators to make the sets work with the STL. Here's were I stopped and asked myself if there's a better way to accomplish what I actually want: store different unordered_set classes in the same member var without the need to make the owner of that var templated as well.

Upvotes: 1

Views: 131

Answers (1)

gluk47
gluk47

Reputation: 1820

Main idea

You can happily employ multiple inheritance here.

The main idea is: create a base class tagging your sets and make it a base class for all your sets. Then explicitly instantiate the set class for each template arguments you need, creating an empty class inherited publicly from both the set container and your tagging interface. Then you'll have nothing to add, no code duplication seems to be needed.

Anyway, you'll need to create some (maybe virtual) functions that will work for all template specializations. We'll need to be able to use a single variable in the same context regardless of what it holds. But you can try reduce some code with more using declarations due to inheritance and use implicit type conversion (e.g. if your sets contain numbers only).

#include <set>

class setInterface {
    /* Code common for all set template specializations
       (you have to have some common interface anyway) */
};

template <typename T> class TSet: public setInterface, std::set<T> {
    using std::set<T>::set;
    /* more using-declarations or templated versions of some functions
       You can use SFINAE here to achieve more magical results,
       or use template specializations for specific types. */
};

using intSet = TSet<int>;
using doubleSet = TSet<double>;

class B {
public:
  setInterface values;
};

int main () {
    B b;
    b.values = intSet {1, 2, 3} ;
    b.values = doubleSet {1., 2., 3.};
}

PS: thanks go to @Jarod42 for the template using syntax.

A working implementation

The following assumptions have been made:

  • We will use only the sets with items convertible to long long. We can use void* in general case and add some additional methods for convenience/safety.
  • We are sane and will never compare iterators of differently typed sets. The results will be unpredictable.
  • We don't need to check pointers for nullptrs (well, it will bring no more value in my code sample, sure in real world you always need).

The solution is able to iterate over the map using non-const begin/ends and using the new shiny range-based for. See the main; compile and run it (-std=c++14) to see the result.

#include <set>
#include <memory>
#include <iostream>

using common_denominator_type = long long;

class setInterface {
protected:
    class iterator_impl;
public:
    class iterator {
    public:
        iterator (iterator_impl* impl) : impl (impl) {}

        iterator& operator++ () { ++*impl; return *this; };
        bool operator != (const iterator& rhs) const { return *impl != *rhs.impl; };
        common_denominator_type operator* () const { return **impl; };
    private:
        std::shared_ptr <iterator_impl> impl;
    };
    virtual iterator begin() = 0;
    virtual iterator end() = 0;
    virtual size_t size() const = 0;
protected:
    class iterator_impl {
    public:
        virtual iterator_impl& operator++ () = 0;
        virtual bool operator != (const iterator_impl&) const = 0;
        virtual common_denominator_type operator* () const = 0;
        virtual void* as_std_set_iterator () = 0;
        virtual const void* as_std_set_iterator () const = 0;
    };
};

template <typename T> class TSet: public setInterface, std::set<T> {
public:
    using std::set<T>::set;
    size_t size () const override { return std::set<T>::size(); }
    iterator begin () override { return iterator (new TSet<T>::iterator_impl (std::set<T>::begin())); }
    iterator end   () override { return iterator (new TSet<T>::iterator_impl (std::set<T>::end  ())); }
protected:
    class iterator_impl: public setInterface::iterator_impl {
    public:
        using std_it = typename std::set<T>::iterator;
        iterator_impl (std_it&& _) : m_real_iterator(std::move (_)) {}
        iterator_impl& operator++ () override { ++m_real_iterator; return *this; }
        bool operator != (const setInterface::iterator_impl& rhs) const override {
            return *reinterpret_cast <const std_it*>(as_std_set_iterator())
                    !=
                   *reinterpret_cast <const std_it*>(rhs.as_std_set_iterator());
        }
        common_denominator_type operator* () const override { return *m_real_iterator; }
        void* as_std_set_iterator () override { return &m_real_iterator; }
        const void* as_std_set_iterator () const override { return &m_real_iterator; }
    private:
        std_it m_real_iterator;
    };
};

using intSet = TSet<int>;
using longSet = TSet<long>;

class B {
public:
    std::shared_ptr <setInterface> values;
};

std::ostream& operator<< (std::ostream& str, B& b) {
    str << "[" << b.values->size() << "] [";
    for (auto i = b.values->begin(); i != b.values->end(); ++i)
        str << *i << " ";
    str << "][";
    for (auto i : *b.values)
        str << i << " ";
    return str << "]";
}

int main () {
    B b;
    b.values.reset (new intSet {1, 2, 3});
    std::cout << b << std::endl;
    b.values.reset (new longSet {10l, 20l, 30l});
    std::cout << b << std::endl;
}

Upvotes: 2

Related Questions