Call function on one of many vectors without enum and switch

Viewed 196

I have a class that contains several vectors of unrelated classes.

class Class0 {};
class Class1 {};
class Class2 {};

enum class RemoveFromVector : uint8_t { CLASS0 = 0, CLASS1 = 1, CLASS2 = 2 };

class C
{
public:
   std::vector<Class0> c0s;
   std::vector<Class1> c1s;
   std::vector<Class2> c2s;

   void RemoveFromVector(const RemoveFromVector RFV, const int Index)
   {
      // I'd like to replace this switch with something that's compile-time
      switch ((uint8_t)RFV)
      {
      case 0: c0s.erase(c0s.begin() + Index); break;
      case 1: c1s.erase(c1s.begin() + Index); break;
      case 2: c2s.erase(c2s.begin() + Index); break;
      default: break;
      }
   }
};

int main()
{
   C c;
   c.c0s.push_back(Class0());
   c.c1s.push_back(Class1());
   c.c1s.push_back(Class1());
   c.c1s.push_back(Class1());
   c.c2s.push_back(Class2());

   // this should remove the last element from C.c1s
   c.RemoveFromVector(RemoveFromVector::CLASS1, 2);
}

How would I write a function that removes an element from one of the vectors based on an enum (or int) at runtime without having to write a switch that's got a case for every single vector?

In other words, I'm looking for a way to deduce which vector to call erase() on statically at compile-time, which I then call at runtime. The correct term might be "static dispatch" though I'm not entirely certain.

3 Answers

You should keep the code as much as simple as possible. As per the currently shown code, it is simple and readable for every developer who works later on the codebase.

Secondly, the internal storage via std::vectors will make this task anyways at run-time. Because, most of the operations happen with the std::vector is run time overhead as they allocate the memory and manage it at run time. Therefore you can not do any compile-time work for std::vector::erase and whatsoever.


That being said, if you insist to avoid the switch statement, and bring the template complication, one is below, which still would have kind of template type mapping using if constexpr, and the vector erase happens at run-time.

#include <type_traits> // std::is_same_v

class C
{
   template<typename ClassType>
   auto& getVectorOf() /* noexcept */
   {
      if constexpr (std::is_same_v<ClassType, Class0>) return c0s;
      else if constexpr (std::is_same_v<ClassType, Class1>) return c1s;
      else if constexpr (std::is_same_v<ClassType, Class2>) return c2s;
   }
public:
   std::vector<Class0> c0s; // recommended to be private!
   std::vector<Class1> c1s;
   std::vector<Class2> c2s;

   template<typename ClassType>
   void RemoveFromVector(const std::size_t index) /* noexcept */
   {
      // some index check!
      auto& vec = getVectorOf<ClassType>();
      vec.erase(vec.begin() + index);
   }
};

Call the function like

C c;
// fill the vectors
c.RemoveFromVector<Class0>(0);
c.RemoveFromVector<Class1>(2);
c.RemoveFromVector<Class2>(0);

(See a Demo Online)

Here's a slightly more generic solution:

    template <typename... Types>
    class MultiStack
    {        
    public:

        template <typename T>
        /*const*/ T& Get() /*const*/
        {
            return GetStack<T>().back();
        }        

        template <typename T>
        void Push( const T& t )
        {
            GetStack<T>().push_back( t );
        }

        template <typename T>
        void Pop()
        {
            GetStack<T>().pop_back();
        }
        
        template <size_t... Sizes>
        void Reserve()
        {
            auto reserve = [&]( auto&... stacks ) { ( stacks.reserve( Sizes ), ... ); };
            std::apply( reserve, Stacks );
        }

    private:

        template <typename T>
        std::vector<T>& GetStack()
        { 
            return std::get<std::vector<T>>( Stacks ); 
        }

        std::tuple<std::vector<Types>...> Stacks;

    };

Usage looks nice and simple:

using MyStack = MultiStack<Class0,Class1,Class2>;
MyStack stack;

stack.Push( ClassX() );  // automatically pushes any compatible object on the appropriate stack

stack.Pop<Class1>();     // pops/"erases" last object of the Class1 stack (vector)

You could extend this if you need to Pop more than one object at a time, or 'call other functions' on the vector. You could/should also make Get return const T& depending on your needs. (Get() const will need GetStack() const)

I have left in the fancy Reserve() just to show off. ;-) You will probably want to set certain but different initial sizes for your stacks.

I think std::variant and std::visit are the way you can go. You also can use a new helper function make_variant_array to make the code even shorter:


#include <cstdint>
#include <vector>
#include <variant>
#include <array>
#include <functional>

class Class0 {};
class Class1 {};
class Class2 {};

enum class RemoveFromVector
    : uint8_t
{
    Class0 = 0, Class1 = 1, Class2 = 2
};

template <class... Args>
auto make_variant_array(Args&... args)
{
    using var_t = std::variant<std::reference_wrapper<Args>...>;
    return std::array<var_t, sizeof...(Args)>{std::ref(args)...};
}

class C
{
public:
    std::vector<Class0> c0s;
    std::vector<Class1> c1s;
    std::vector<Class2> c2s;

    void RemoveFromVector(const RemoveFromVector RFV, const int Index)
    {
        static auto lookup = make_variant_array(c0s, c1s, c2s);
        std::visit([Index](auto& vec) { vec.get().erase(vec.get().begin() + Index); }, lookup[(uint8_t)RFV]);
    }
};

int main()
{
    C c;
    c.c0s.push_back(Class0());
    c.c1s.push_back(Class1());
    c.c1s.push_back(Class1());
    c.c1s.push_back(Class1());
    c.c2s.push_back(Class2());

    // this should remove the last element from C.c1s
    c.RemoveFromVector(RemoveFromVector::Class1, 2);
}
Related