3

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.

JeJo
  • 30,635
  • 6
  • 49
  • 88
GlassBeaver
  • 196
  • 4
  • 15
  • Simply passing the vector itself is no option? – JulianW Sep 14 '20 at 07:35
  • Any reason the Enum value ain't a template argument, that way the optimizer will remove the switch for you – JVApen Sep 14 '20 at 07:36
  • Afraid, I do not understand. The enum value is provided at runtime? Yet the dispatch should be resolved at compile-time? – Quimby Sep 14 '20 at 07:36
  • Passing the vector is not an option, the enum value is not a template because it's not known at compile-time which vector I'll be removing from. It's entirely possible that I'm wanting something that's simply impossible to do. – GlassBeaver Sep 14 '20 at 07:39
  • @GlassBeaver if `RemoveFromVector` isn't known at compile time, how you want determine it at compile time? – JulianW Sep 14 '20 at 07:41
  • My idea was that it might be possible to get a compile-time list of all the vectors at play by templating Class0..Class2 perhaps. Based on that compile-time list, you could maybe make one templated RemoveFromVector function and figure out which one to call at runtime based on some int parameter - I lack the template skills to do that though. – GlassBeaver Sep 14 '20 at 07:44
  • I still don't get your point. If you do not know the `RFV` at compile time, there always will be a `switch` (at best, you could hide the `switch` behinde some complex TMP). So why you want throw TMP on your code? You always should avoid TMP if possible. – JulianW Sep 14 '20 at 07:52
  • if you look at https://stackoverflow.com/questions/7089284/dynamic-dispatching-of-template-functions they talk about bridging compile-time and runtime, albeit w/ a different usecase but not that entirely different - then there's also https://stackoverflow.com/questions/52286202/dynamic-dispatch-to-template-function-c where they're supplying the bools out at runtime - again, slightly different usecase – GlassBeaver Sep 14 '20 at 07:58
  • sorry to say, but your class is flawed. If external code can access directly to add, it should be able to access directly to remove. Rather than adding complexity to the class, you should first address this issue. – UmNyobe Sep 14 '20 at 08:30
  • https://stackoverflow.com/questions/7089284/dynamic-dispatching-of-template-functions does not fit what you want do... I never said what you want do is not possible (as far as I understand your desire), I just said, I do not get the point, why you want add this kind of complexity to your code. And what you can see here https://stackoverflow.com/questions/52286202/dynamic-dispatch-to-template-function-c imo. does not fit your problem either. What they did there is usefull if you have multiple template parameters, and if you want to have the set of possible combinations of them. – JulianW Sep 14 '20 at 08:47
  • in this case, adding complexity is totally fine - I'm exploring something in an AI system the details of which don't really matter wrt the question. I'm open to any and every (over)complicated template-magic way of achieving this – GlassBeaver Sep 14 '20 at 09:08
  • 1
    Well, if adding complexity is fine, you can also consider the case where you want to udate any possible combination of the vectors https://godbolt.org/z/E3adKK Not an answer to your question, though. – Bob__ Sep 14 '20 at 09:42
  • Looks like you need https://stackoverflow.com/questions/61299941/templated-multistack-implementation-how and then `using C = MultiStack` This will allow you to just `push/pop` like a simple stack. Classes have to be distinct, so you might need wrappers if e.g. some of them are ints. Or you use an index (which tuple can handle) – non-user38741 Sep 14 '20 at 15:33

3 Answers3

4

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)

JeJo
  • 30,635
  • 6
  • 49
  • 88
1

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.

non-user38741
  • 671
  • 5
  • 19
  • is there any way of doing `stack.Pop()` by not having to supply a compile-time template parameter but a runtime one instead? e.g. `stack.Pop(1)` <--- this would Pop() from the vector of Class1s – GlassBeaver Sep 14 '20 at 20:28
  • 1
    Oh, then I slightly misunderstood. So you have *runtime* polymorphism!? I don't think you can then profit in any way from compile-time polymorphism. You could find out the runtime type of your object, cast it to its real type, use `Pop()`. But this seems like a misuse. In any case it seems like you need some "trait" or "typeinfo" for your classes. That would get rid of the enum and switch, because a class had an internal "flag" to which bucket it belonged. – non-user38741 Sep 14 '20 at 20:59
  • Yeah it's essentially this runtime-compile time "gap" that I'm trying to bridge, which is why JulianH's answer comes close. I understand that my question is not phrased in the best possible way it could be but I'm really struggling to find the right terminology. Your answer would be perfect if I could do something like `stack.Pop(1)`. You mention it seems like a misuse - how would that look like though, care to elaborate? Might be what I need (the `Pop()`). – GlassBeaver Sep 14 '20 at 22:20
  • 1
    You can do `std::get(Stacks)` to get the N-th stack, but again you'd have to translate a "runtime" number to "compile-time". Can't help you any further, as that combination is problematic. I called it misuse, because you don't gain anything by using it that way. Just try to come up with a better runtime-only logic, maybe just array of vectors and each class knows its index... Could also ask the question again, I guess, without leading people to templates. – non-user38741 Sep 14 '20 at 23:57
0

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);
}
JulianW
  • 897
  • 9
  • 23
  • It also still has the `enum` too. – JeJo Sep 14 '20 at 09:16
  • How would you write an erase function based on an `enum` without `enum`? – JulianW Sep 14 '20 at 09:18
  • See the other answer.. ;) – JeJo Sep 14 '20 at 09:19
  • Your answer isn't based on an `enum`, so it's not an answer to the question. – JulianW Sep 14 '20 at 09:19
  • Didn't he ask to eliminate the `switch`? if there is no `switch` statement why do we need the `enum` too(if that is the only function where the mapping needed)? – JeJo Sep 14 '20 at 09:21
  • Because you are dispatching nothing at runtime, your code fully do it's work at compile time. – JulianW Sep 14 '20 at 09:22
  • Wrong, the index parameter is at run-time, also std::vector::erase too... The function only instantiates the member function at compile time and works on runtime. – JeJo Sep 14 '20 at 09:25
  • The statement "your code fully do it's work at compile time" was related to the dispatching part. – JulianW Sep 14 '20 at 09:28
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/221443/discussion-between-julianh-and-jejo). – JulianW Sep 14 '20 at 09:28
  • Thought I'd jump in: this solution seems like a better fit so far because I can specify which vector to erase() from at runtime thanks to the `enum RemoveFromVector`. Is there any way to skip having to list the vectors in `std::variant` by hand? Could that be automated? – GlassBeaver Sep 14 '20 at 09:45
  • 1
    @GlassBeaver you can do that by adding a helper function to the example of mine. I made an edit. – JulianW Sep 14 '20 at 10:05