Home > Back-end >  how to push data into different queues based on the data type in cpp?
how to push data into different queues based on the data type in cpp?

Time:04-09

I have problems to push data into different queues based on the data type.

To be specific, I have multiple request classes. For example:

class RequestA {
};
class RequestB {
};
class RequestC {
};
...

These requests might be inherited by certain class (e.g., class Request).

Each object of these data types must be put into different queues. For example:

std::vector<RequestA> queueA;
std::vector<RequestB> queueB;
std::vector<RequestC> queueC;
...

The reason why I need different queues for each class is that each request at the top (front) of the queue requires different (post-)processing steps later.

For instance,

class RequestHandler {
public:
  void tick() {
     // Multiple requests might be accumulated in the queue before calling tick()
     if (!queueA.empty()) {
        processA(queueA.front())
        queueA.pop_front(); // assume std::vector has pop_front()
     }
     // queueB can be empty although other queues are full of elements
     if (!queueB.empty()) {
       ...
     }
     ...
     // Polymorphism might be used.
     // if RequestA, RequestB, ... are derived by Request:
     if (!poly_queue.empty()) {
        Request* req = poly_queue.top();
        req->process(/* status of handler */);
        poly_queue.pop_front(); // assume std::vector has pop_front() 
     // However, the result is not same as one using separated queue,
     // because the order of requests is not preserved.
     // For example, in the separated queue example, the processing order might be, 
     // RequestA -> Request C or only RequestA. 
     // However, if I use the single queue, we cannot achieve same result.
     
   }
private:
 std::vector<RequestA> queueA;
 std::vector<RequestB> queueB;
 std::vector<RequestC> queueC;
...
 std::vector<Request*> poly_queue;
};

However, the problem is that ,to push data into the queue, I might use followings:

if (typeid()) {
   queueA.push_back();
} else if (typeid()) {
   queueB.push_back();
} ...

or if RequestA, RequestB, ... are inherited by class Request:

std::map<std::type_index, std::vector<Request*>> queue;

queue[typeid()].push_back(...);

might be used.

However, it might be better to avoid using typeid or dynamic_cast.

How to efficiently handle this scenario?

CodePudding user response:

If the behavior for each type is truly so distinct as to impose stricter pre or post-conditions than the base class, using a public base class as the common behavior (rather than as an implementation detail for code re-use) sounds like a violation of the Liskov Substitution Principle.

If you can manage to encapsulate those stricter conditions within the derived classes so that they are no longer part of the public interface, or add to the base class public interface a common way to check and handle the different conditions, then Some programmer dude's answer sounds like a good option for you. If you have a larger number of types, or are likely to add many more types in the future, you want to spend some time trying to make that work. If not, you might want to reconsider whether you should be treating these objects through a common base.

If the number of types is and is likely to remain small, you might consider using a discriminated union like std::variant, perhaps as std::variant<RequestA *, RequestB *, RequestC *>, which would be stored in your queue instead of Request *. The variant keeps track of which type it contains, and you can use std::visit() and overloads for each type to use the appropriate function for the appropriate type. Be aware some implementations of std::visit std::variant used to have disappointing performance, so if that's a concern, check your compiler and standard library versions.

If you are absolutely committed to your existing hierarchy, but can't encapsulate the different requirements, then for this small number of subtypes, you could provide a virtual method alternative to dynamic_cast , similar to this answer to a related question:

class Request
{
   //whatever else your request class already has
   virtual RequestA * cast_to_RequestA() 
   {
       return nullptr;
   }
   virtual RequestB * cast_to_RequestB() 
   {
       return nullptr;
   }
   virtual RequestC * cast_to_RequestC() 
   {
       return nullptr;
   }
};

class RequestA
{
   // whatever else RequestA has
   virtual RequestA * cast_to_RequestA() override
   {
       return this;
   }
};
// similarly for the appropriate types for RequestB, RequestC

Depending on your compiler, a virtual call may be more performant/deterministic than dynamic_cast, but it doesn't address the underlying issue of "this looks like a violation of the Liskov substitution principle". This means more of your source code may need to be changed if you change anything about these classes in the future, compared to Some programmer dude's method or std::variant, and makes it easier to miss something somewhere when adding a new type. For very small numbers of types where you have a high degree of confidence that new types will never be added, that impact might be adequately limited, but I don't know that I would recommend this method generally.

CodePudding user response:

One possible way is to use templates and std::map. In particular, you can create a member function template named add to the RequestHandler class to handle different requests.

Method 1

Step 1

Create a std::map.

std::map<std::type_index, void*> myMap{{typeid(RequestA), &queueA}, 
                                       {typeid(RequestB), &queueB}, 
                                       {typeid(RequestC), &queueC}};

Step 2

Add declaration for member function template add<> inside class RequestHandler.

template<typename T> void add(T Arg);

Step 3

Implement add.

template<typename T> void RequestHandler::add(T Arg) // - STEP 3
{
    
   
    (*static_cast<std::vector<decltype(Arg)>*>(myMap.at(typeid(Arg)))).push_back(Arg);
    
}

Working example

#include <iostream>
#include <map>
#include <vector>
#include <typeindex>


struct RequestA{};
struct RequestB{};
struct RequestC{};


class RequestHandler {
    public:
        void tick() 
        {
            std::cout<<"tick called"<<std::endl;
        }
    private:
        std::vector<RequestA> queueA;
        std::vector<RequestB> queueB;
        std::vector<RequestC> queueC;
 
        //create std::map -                             STEP 1
        std::map<std::type_index, void*> myMap{{typeid(RequestA), &queueA}, 
                                               {typeid(RequestB), &queueB}, 
                                               {typeid(RequestC), &queueC}};
    public:
        //create member function template -             STEP 2
        template<typename T> void add(T Arg);

};
template<typename T> void RequestHandler::add(T Arg) // - STEP 3
{
    std::cout  << "add called on " <<typeid(Arg).name() << std::endl;//just for debugging purposes
    std::cout << "size before push_back "<< (*static_cast<std::vector<decltype(Arg)>*>(myMap.at(typeid(Arg)))).size()<<std::endl;//just for debugging
    (*static_cast<std::vector<decltype(Arg)>*>(myMap.at(typeid(Arg)))).push_back(Arg);
    std::cout << "size after push_back "<< (*static_cast<std::vector<decltype(Arg)>*>(myMap.at(typeid(Arg)))).size()<<std::endl;

    std::cout<<"--------------------------------------"<<std::endl;
}


int main()
{
    
    RequestA A;
    RequestB B;
    RequestC C;
    
    RequestHandler rq;
    
    //call RequestHandler's add method simulating the requests
    rq.add(A);
    
    rq.add(B);
    rq.add(B);
    
    rq.add(C);
    
}

Working demo


Method 2

With C 17 we can use std::any instead of void*. The basic steps remains the same as in the previous method.

#include <iostream>
#include <map>
#include <vector>
#include <typeindex>
#include <any>
#include <functional>
struct RequestA{};
struct RequestB{};
struct RequestC{};


class RequestHandler {
    public:
        void tick() 
        {
            std::cout<<"tick called"<<std::endl;
        }
    private:
        std::vector<RequestA> queueA;
        std::vector<RequestB> queueB;
        std::vector<RequestC> queueC;
 
        //create std::map -                             STEP 1
        std::map<std::type_index, std::any> myMap{{typeid(RequestA), std::ref(queueA)}, 
                                                  {typeid(RequestB), std::ref(queueB)}, 
                                                  {typeid(RequestC), std::ref(queueC)}};
    public:
        //create member function template -             STEP 2
        template<typename T> void add(T Arg);

};
template<typename T> void RequestHandler::add(T Arg) // - STEP 3
{
    std::cout  << "add called on " <<typeid(Arg).name() << std::endl;//just for debugging purposes
    std::cout << "size before push_back "<< std::any_cast<std::reference_wrapper<std::vector<T>>>(myMap.at(typeid(T))).get().size()<<std::endl;//just for debugging
   
    std::any_cast<std::reference_wrapper<std::vector<T>>>(myMap.at(typeid(T))).get().push_back(Arg);
    std::cout << "size after push_back "<< std::any_cast<std::reference_wrapper<std::vector<T>>>(myMap.at(typeid(T))).get().size()<<std::endl;

    std::cout<<"--------------------------------------"<<std::endl;
}


int main()
{
    
    RequestA A;
    RequestB B;
    RequestC C;
    
    RequestHandler rq;
    
    //call RequestHandler's add method simulating the requests
    rq.add(A);
    
    rq.add(B);
    rq.add(B);
    
    rq.add(C);
    
}

Working demo

The above is just a demonstration according to my current understanding of your requirements. It took me a while(around 40 minutes) to understand the requirements and writing the code accordingly. I may still be wrong in understanding your requirements. So let me know if this is what you wanted. You can also modify the code according to your needs.

  • Related