Search code examples
c++templatesc++14abstraction

A C++ abstraction for sorted containers that differ only in sort function?


In C++, I have a class that contains two members which are sorted containers. The containers store the same object type, but one is sorted in ascending order and the other in descending order. Note: the object type in each container is the same, but the actual object instances stored in the two containers are different. In other words, the data is not duplicated.

I have several template methods which visit items in a container, with the template argument saying which container to visit (i.e. the one sorted in ascending order, or the other).

Regardless of which container I iterate on, I perform the exact same operations. But the order in which I visit the items is important.

So I'm looking for an abstraction or language construct that allows me to work with the two containers in a generic way.

Here is an example to help explain what I'm trying to do:

#include <set>
#include <iostream>

class MySets
{
    public:
        using fwd_set_t = std::set<int, std::less<int>>;
        using rev_set_t = std::set<int, std::greater<int>>;

        // BEGIN: added in edit
        using MyIterator = fwd_set_t::iterator;
        using MyConstIterator = fwd_set_t::const_iterator;

        // is this safe??? the compiler isn't complaining that I'm using
        // fwd_set_t::iterator and rev_set_t::iterator interchangeably
        template <bool IS_FWD> inline MyConstIterator begin() const
        {
            return (IS_FWD ? fwd_set_.begin() : rev_set_.begin());
        }

        template <bool IS_FWD> inline MyConstIterator end() const
        {
            return (IS_FWD ? fwd_set_.end() : rev_set_.end());
        }

        template <bool IS_FWD> inline void printSetAlternate() const
        {
            for (auto iter = begin<IS_FWD>(); iter != end<IS_FWD>(); ++iter)
            {
                std::cout << "  " << (*iter) << "\n";
            }
        }
        // END: added in edit


        MySets()
            : fwd_set_({10, 20, 30, 40, 50})
            , rev_set_({11, 21, 33, 44, 55})
            {
            }

        template <bool IS_FWD> inline void printSet() const
        {
            //auto const& s = (IS_FWD ? fwd_set_ : rev_set_); // ERROR - different types
            //for (auto const& n : s) { std::cout << "  " << n << "\n"; }

            if (IS_FWD) { for (auto const& n : fwd_set_) { std::cout << "  " << n << "\n"; } }
            else        { for (auto const& n : rev_set_) { std::cout << "  " << n << "\n"; } }
        }

    private:
        fwd_set_t fwd_set_;
        rev_set_t rev_set_;
};

int main(int argc, char* argv[])
{
    MySets ms;

    std::cout << "fwd:\n";
    ms.printSet<true>();

    std::cout << "rev:\n";
    ms.printSet<false>();

    return 0;
}

The key part of the example is the printSet() method. The commented-out code gives a compiler error ("operands to ‘?:’ have different types"), but shows the intent of what I'd like to do. Imagine this function wasn't trivial, but instead had a fair amount of logic to perform on each item in the set. I'd rather not copy and paste the code as I did in the example.

So I feel like there should be some kind of abstraction or language feature I can use to achieve the intended result, but I'm not sure what that is.

Note: I am forced to use C++14, so any solution that uses newer language features won't work for me.

Edit: I added some code to the example above. The compiler does not complain about using a fwd_set_t::iterator to also refer to a rev_set_t::iterator. And calling printSetAlternate() works as expected. My question is, is this safe to do? I would like to combine this with the suggestion from @NathanOliver below. The actual use-case involves passing iterators around to different functions.


Solution

  • You can write a couple private helper functions and tags like

    struct fwd_tag_t{};
    struct rev_tag_t{};
    auto& getSet(fwd_tag_t) const { return fwd_set_; }
    auto& getSet(rev_tag_t) const { return rev_set_; }
    

    and then use tag dispatch to call the one to get the correct set like

    template <bool IS_FWD> inline void printSet() const
    {
        auto const& s = getSet(std::conditional_t<IS_FWD, fwd_tag_t, rev_tag_t>{});
        for (auto const& n : s) { std::cout << n << " "; }
        std::cout << "\n";
    }
    

    If you can upgrade to C++17, you could removes the tags and the overloads and change getSet to just be

    template <bool IS_FWD> inline auto& getSet() const
    {
        if constexpr(IS_FWD) 
            return fwd_set;
        else 
            return rev_set;
    }
    

    and then printSet would become

    template <bool IS_FWD> inline void printSet() const
    {
        auto const& s = getSet();
        for (auto const& n : s) { std::cout << n << " "; }
        std::cout << "\n";
    }