Home > Net >  Handling custom vector classes
Handling custom vector classes

Time:05-16

I have come across many occasions where I want to have an item which is selected inside a vector, for this I have written the template class:

// a vector wrapper which allows a specific item to be currently selected
template<typename T>
class  VectorSelectable
{
public:
    VectorSelectable() {};
    VectorSelectable(std::initializer_list<T> items) : m_Items(items) {};
    void Add(const T& v)                { m_Items.push_back(v); m_CurrentIndex = m_Items.size()-1; } // lvalue & refs
    void Add(T&& v)                     { m_Items.push_back(std::move(v)); m_CurrentIndex = m_Items.size()-1; } // rvalue
    void Remove(size_t index) { 
        assert(index < m_Items.size()); 
        m_Items.erase(m_Items.begin()   index);
        if(m_CurrentIndex != -1 && (int)index <= m_CurrentIndex) 
            m_CurrentIndex--; 
    }
    void RemoveCurrent()                { assert(m_CurrentIndex > -1 && m_CurrentIndex < (int)m_Items.size()); Remove(m_CurrentIndex); }
    T& CurrentItem()                    { assert(m_CurrentIndex > -1 && m_CurrentIndex < (int)m_Items.size()); return m_Items[m_CurrentIndex]; }
    T& operator [](size_t index)        { assert(index < Size()); return m_Items[index]; }
    // moves value of n_next onto n, and n_new onto n
    void ItemSwap(size_t n, size_t n_Next) {   
        assert(n < m_Items.size()); 
        assert(n_Next < m_Items.size()); 
        T itemBuf = std::move(m_Items[n]);                
        m_Items[n] = m_Items[n_Next];
        m_Items[n_Next] = std::move(itemBuf);
    }
    size_t Size()                       { return m_Items.size(); }
    const std::vector<T>& Data()        { return m_Items; }
    std::vector<T>* DataPtr()           { return &m_Items; }
    T* ItemPtr(size_t index)            { assert(index < m_Items.size()); return &m_Items[index]; }
    void SetCurrentIndex(int index)     { assert(index >= -1 && index < (int)m_Items.size()); m_CurrentIndex = index; }
    int& CurrentIndex()                 { return m_CurrentIndex; }
    bool HasItemSelected()              { return m_CurrentIndex != -1; }
private:
    std::vector<T> m_Items;
    int m_CurrentIndex = -1;
};

I am also coming across many scenarios where I want a vector of unique_ptrs (generally for polymorphic classes), this looks like this:

template<typename T>
class Vector_UniquePtrs
{
public:
    // Adds an Item (and returns a raw ptr to it)
    // usage: v.Add() ... (equivelent to v.Add<base_class>())
    template<typename... Args>
    T* Add(Args... args) {
        return Add<T>(args...);
    }
    // Adds a Polymorphic Item (and returns a raw ptr to it)
    // usage: v.Add<sub_class>()
    template<typename T2, typename... Args>
    T* Add(Args... args) {
        m_Items.push_back(std::unique_ptr<T>(new T2(args...)));
        return m_Items.back().get();
    }
    // Remove Item
    void Remove(size_t index) { 
        assert(index < m_Items.size()); 
        m_Items.erase(m_Items.begin()   index);
    }
    T* operator [](size_t index)            { assert(index < Size()); return m_Items[index].get(); }
    size_t Size()                       { return m_Items.size(); }
private:
    std::vector<std::unique_ptr<T>> m_Items;
};

My question is:

  • How can I handle a combination of these 2 class types (e.g. VectorSelectable<unique_ptr>) as one returns ptrs, the other returns references, is the only option to write an entirely new class?

CodePudding user response:

You mainly need to put the std::vector<std::unique_ptr<T>> in VectorSelectable and hide all the pointer stuff from the interface. With a few small changes to your class, it could look like this:

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <utility>
#include <vector>

template <typename T>
class VectorPtrSelectable {
public:
    VectorPtrSelectable() = default;

    VectorPtrSelectable(std::initializer_list<T> items) :
        m_CurrentIndex(items.size() - 1) 
    {
        m_Items.reserve(items.size());
        // fill `m_Items` from the initializer list ...
        std::transform(items.begin(), items.end(), std::back_inserter(m_Items),
            [](const T& item) {
                // ... by creating a unique_ptr from each element (transformation)
                return std::make_unique<T>(item);
            });
    };

    template <class U, class... Args>
    T& Add(Args&&... args) {
        // make `Add` forward to `make_unique`
        m_Items.emplace_back(std::make_unique<U>(std::forward<Args>(args)...));
        m_CurrentIndex = m_Items.size() - 1;
        // and return a reference instead
        return *m_Items.back();
    }

    template <class... Args>
    T& Add(Args&&... args) {
        // forward to Add<U>
        return Add<T>(std::forward<Args>(args)...);
    }

    void Remove(size_t index) {
        m_Items.erase(std::next(m_Items.begin(), index));
        if (m_CurrentIndex != static_cast<size_t>(-1) && index <= m_CurrentIndex)
            m_CurrentIndex--;
    }

    T& operator[](size_t index) { return *m_Items[index]; }
    const T& operator[](size_t index) const { return *m_Items[index]; }

    T& CurrentItem() { return *m_Items[m_CurrentIndex]; }
    const T& CurrentItem() const { return *m_Items[m_CurrentIndex]; }

    void SetCurrentIndex(size_t index) { m_CurrentIndex = index; }
    void RemoveCurrent() { Remove(m_CurrentIndex); }
    bool HasItemSelected() { return m_CurrentIndex != static_cast<size_t>(-1); }

    void ItemSwap(size_t n, size_t n_Next) {
        // simplified swapping:
        std::swap(m_Items[n], m_Items[n_Next]);
    }
    
    // make functions that does not change your instance const qualified:
    size_t CurrentIndex() const { return m_CurrentIndex; }
    size_t Size() const { return m_Items.size(); }

private:
    std::vector<std::unique_ptr<T>> m_Items;
    size_t m_CurrentIndex = static_cast<size_t>(-1);  // size_t for the index
};

Example usage:

#include <iostream>
#include <string>

int main() {
    VectorPtrSelectable<std::string> vs{"World", "Hello"};
    std::cout << vs.CurrentItem() << '\n';
    vs.ItemSwap(0, 1);
    std::cout << vs.CurrentItem() << '\n';
    vs.RemoveCurrent();
    std::cout << vs.CurrentItem() << '\n';
    std::cout << vs.Add("Add and get a reference") << '\n';
}

Output:

Hello
World
Hello
Add and get a reference
  • I made m_CurrentIndex a size_t because that's idiomatic but if you'd like to keep it as an int, that's fine too.
  • std::next(m_Items.begin(), index) will do the same as m_Items.begin() index, but in cases where the iterator returned by m_Items.begin() is a plain pointer, using std::next avoids potential warnings about using pointer arithmetic.
  • Returning a reference instead of a pointer to the added element makes no difference other than making the interface more idiomatic. It's simply what a user of the class is likely to expect. Returning a pointer also opens up questions like "can it return nullptr?" etc.
  • The added const qualified functions makes those functions usable in const contexts too.
    template<class T>
    void foo(const VectorPtrSelectable<T>& vps) { // note: const&
        if(vps.Size() > 0) {
            std::cout << "the first element is " << vps[0] << '\n';
            std::cout << "the current element is " << vps.CurrentItem() << '\n';
        }
    }
    
    None of the three member functions used above could be used without the const qualified overloads.
  • Related