2

I am trying to implement a templated request-response journal class.

This is a usage example:

struct RequestA {
    std::string data;
};

struct ResponseA {
    int code;
};

struct RequestB {
    int data;
};

struct ResponseB {
    double value;
};

int main(int argc, char* argv[])
{
    constexpr std::size_t maxEntries = 5;
    RequestJournal<maxEntries, std::pair<RequestA, ResponseA>, std::pair<RequestB, ResponseB>> requestJournal{ 1000 };

    auto requestA = std::make_shared<RequestA>(RequestA{ "RequestA data"});
    requestJournal.addRequest(0, requestA);

    auto requestB = std::make_shared<RequestB>(RequestB{ 10 });
    requestJournal.addRequest(1, requestB);
}

The idea is to have an infrastructure class which is not aware about request/response types, to register all possible request-response pairs upon creation and to be able to define specific slot for a request.

The following is a prototype (based on this approach link) :


inline constexpr std::size_t npos = -1;

template <typename T, typename... Ts>
struct index : std::integral_constant<std::size_t, npos> {};

template <typename T, typename... Ts>
inline constexpr std::size_t index_v = index<T, Ts...>::value;

template <typename T, typename... Ts>
struct index<T, T, Ts...> : std::integral_constant<std::size_t, 0> {};

template <typename T, typename Head, typename... Tail>
class index<T, Head, Tail...>
{
    static constexpr std::size_t tmp = index_v<T, Tail...>;

public:
    static constexpr std::size_t value = tmp == npos ? tmp : tmp + 1;
};

// Helper function that gets the variant type information for a specific type
template <typename T, typename... Ts>
std::pair<const std::type_info*, std::any> GetVariantTypeInfo(std::variant<Ts...>& variant)
{
    // Get the index of the specified type in the variant
    constexpr static auto indexOfType = index_v<T, Ts...>;

    // Check if the specified type is currently stored in the variant
    if (indexOfType == variant.index())
    {
        auto obj = std::get<indexOfType>(variant);

        // Get the type information and object for the specified type
        return std::make_pair(&typeid(obj), std::any(obj));
    }

    // Return a null pointer to indicate that the type information was not found
    return std::make_pair(nullptr, std::any());
}

// Helper function that calls GetVariantTypeInfo for each type in a parameter pack
template <typename... Ts>
const std::type_info* GetVariantTypeInfo(std::variant<Ts...>& variant)
{
    // Call GetVariantTypeInfo for each type in the parameter pack using fold expression
    const std::initializer_list<std::pair<const std::type_info*, std::any>> typeInfos = { GetVariantTypeInfo<Ts, Ts...>(variant)... };
    for (const auto& typeInfo : typeInfos)
    {
        if (typeInfo.first != nullptr)
        {
            const auto& typeIdx = *typeInfo.first;
            return typeInfo.first;
        }
    }
    return nullptr;
}

template <std::size_t maxEntries, typename... Pairs>
class RequestJournal {
public:
    using EntryIndex = std::size_t;

    using RequestTypesVariant = std::variant<std::shared_ptr<typename Pairs::first_type> ...>;
    using ResponseTypesVariant = std::variant<std::shared_ptr<typename Pairs::second_type> ...>;

    template <typename T>
    static constexpr bool request_is_in_pack = (std::is_same_v<T, typename Pairs::first_type> || ...);

    template <typename T>
    static constexpr bool response_is_in_pack = (std::is_same_v<T, typename Pairs::second_type> || ...);

    RequestJournal(int latencyMsec) {
        m_latency = std::chrono::milliseconds(latencyMsec);
    }

    template <typename T>
    std::enable_if_t<response_is_in_pack<T>, void> setResponse(EntryIndex index, std::shared_ptr<T> response) {

        const auto requestTypeInfo =
            GetVariantTypeInfo<std::shared_ptr<typename Pairs::first_type>...>
            (m_journal[index].requestContainer);
     
        // other code ...
    }

private:
    using Timestamp = std::chrono::time_point<std::chrono::steady_clock>;

    struct RequestEntry {
        RequestTypesVariant requestContainer;
        ResponseTypesVariant responseContainer;
    };

    RequestJournal() {}

    std::size_t i = 0;
    std::unordered_map<std::size_t, std::pair<std::type_index, std::type_index>> m_pairsMap{
        { i++, std::make_pair(std::type_index(typeid(typename Pairs::first_type)), std::type_index(typeid(typename Pairs::second_type)))}...
    };

    std::chrono::milliseconds m_latency;
    std::array<RequestEntry, maxEntries> m_journal;
};

The question is how (and if it is possible at all) can I propagate the object within a variant itself (auto obj = std::get<indexOfType>(variant);) to the setResponse function.

Brosbiln
  • 23
  • 4
  • 5
    Tried `std::visit`? – ALX23z Mar 08 '23 at 16:03
  • I mimimized a bit the question. I don't see how std::visit can solve my case. Please advice. – Brosbiln Mar 09 '23 at 08:28
  • 1
    Your code is a very convoluted way of getting the type of a an object the variant currently contains. You are reinventing the bicycle. `std::visit` does all that for you. Your job is to provide a behavior for each request type via a dispatcher (i.e. a template lambda) that has overloads of `operator()` for **each** of the type that your variant might have. I suggest you to understand what you want and to write the logic in simple steps, using plain english or pseudo code. There's no benefit in writing more code than needed – Sergey Kolesnik Mar 09 '23 at 13:30

2 Answers2

4

You need to simplify your code and use std::visit. Here's a simple version for dispatching responses, using std::variant: https://godbolt.org/z/jTYj8o13e


#include <variant>
#include <string>
#include <vector>
#include <iostream>

struct request_a {};
struct request_b{};

struct response_a{
    std::string value;
};
struct response_b{
    int value;
};

response_a respond(request_a) {
    std::cout << "responding to request_a\n";

    return {"oceanic"};
} 

response_b respond(request_b) {
    std::cout << "responding to request_b\n";

    return {815};
} 

int main() {

    std::vector<std::variant<request_a, request_b>> requests{request_a(), request_b()};

    for (const auto &req : requests)
    {
        std::visit([](const auto &req){
            std::cout << respond(req).value << '\n';
        }, req);
    }

    return 0;
}

Output:

responding to request_a
oceanic
responding to request_b
815
Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
  • Unfortunately, I cannot see how I can use it within a template class without having function for each possible request/response – Brosbiln Mar 09 '23 at 08:26
  • 3
    @Brosbiln If you don't plan to process each request in a separate piece of code, why do you have many types of request/response in the first place? – n. m. could be an AI Mar 09 '23 at 08:46
  • @Brosbiln cause you need function for each response type, unless you can categorize the respones in some way and write some templates that handle whole such categories. – ALX23z Mar 09 '23 at 08:46
  • @ALX23z yeah, categorization would involve some sophisticated pattern matching. The whole reason for such code is to have the ability to extend the behavior (support new requests) without modifying the core logic. – Sergey Kolesnik Mar 09 '23 at 13:39
0

Ok, I didn't find the way to access the object itself (std::get<>(variant)) inside getResponse or other functions, but realized, that actually I don't need to access it.

I got it working by propogating the object as std::any further (updating the GetVariantTypeInfo), there I can cast it to a template return type and that's what I actually need when returning the response.

// Helper function that calls GetVariantTypeInfoPointer for each type in a parameter pack
template <typename... Ts>
std::pair<const std::type_info*, std::any> GetVariantTypeInfo(std::variant<Ts...>& variant)
{
    // Call GetVariantTypeInfo for each type in the parameter pack using fold expression
    const std::initializer_list<std::pair<const std::type_info*, std::any>> typeInfos = { GetVariantTypeInfo<Ts, Ts...>(variant)... };
    for (const auto& typeInfo : typeInfos)
    {
        if (typeInfo.first != nullptr)
        {
            return typeInfo;
        }
    }

    return std::make_pair(nullptr, std::any());
}
    template <typename T>
    std::enable_if_t<response_is_in_pack<T>, std::optional<std::shared_ptr<T>>>
        getResponse(ChannelIndex channel, const std::type_info& requestType) {

        //some code ...

        const auto responseTypeInfo =
            GetVariantTypeInfo<std::shared_ptr<typename Pairs::second_type>...>
            (m_journal[channel].responseContainer);

        return std::any_cast<std::shared_ptr<T>>(responseTypeInfo.second);
    }

I am rather curious how it can be implemented with std::visit, so will be glad to see the example. Thanks everybody for help.

Simplified example of usage:

struct RequestA {
    std::string data;
};

struct ResponseA {
    int code;
};

struct RequestB {
    int data;
};

struct ResponseB {
    double value;
};

int main(int argc, char* argv[])
{
    constexpr std::size_t channelsNum = 6;
    RequestJournal<channelsNum, std::pair<RequestA, ResponseA>, std::pair<RequestB, ResponseB>> requestJournal{ 1000000 };

    // Start the first thread which generates requests
    std::thread requestThread([&requestJournal, channelsNum]() {
        for (int i = 0; i < channelsNum; i++) {
            if (i % 2 == 0) {
                auto request = std::make_shared<RequestA>(RequestA{ "RequestA data #" + std::to_string(i) });
                requestJournal.addRequest(i, request);
            }
            else {
                auto request = std::make_shared<RequestB>(RequestB{ i });
                requestJournal.addRequest(i, request);
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(10)); // sleep for 10 milliseconds
        }

        while (requestJournal.getPendingRequestsNum(typeid(RequestA)) != 0) {
            // Check the responses
            for (int i = 0; i < channelsNum; i++) {

                auto responseA = requestJournal.getResponse<ResponseA>(i, typeid(RequestA));
                if (responseA.has_value() == true) {
                    std::cout << "ResponseA received: " << responseA->get()->code << std::endl;
                }
            }
        }

        while (requestJournal.getPendingRequestsNum(typeid(RequestB)) != 0) {
            // Check the responses
            for (int i = 0; i < channelsNum; i++) {

                auto responseB = requestJournal.getResponse<ResponseB>(i, typeid(RequestB));
                if (responseB.has_value()) {
                    std::cout << "ResponseB received: " << responseB->get()->value << std::endl;
                }
            }
        }
        });

    // Start the second thread which sets responses
    std::thread responseThread([&requestJournal, channelsNum]() {
        for (int i = 0; i < channelsNum; i++) {
            std::this_thread::sleep_for(std::chrono::milliseconds(20)); // sleep for 20 milliseconds
            if (i % 2 == 0) {
                auto response = std::make_shared<ResponseA>(ResponseA{ 200 });
                requestJournal.setResponse(i, response);
            }
            else {
                auto response = std::make_shared<ResponseB>(ResponseB{ 3.14 });
                requestJournal.setResponse(i, response);
            }
        }
        });

    // Wait for the threads to finish
    requestThread.join();
    responseThread.join();
}
Brosbiln
  • 23
  • 4
  • why would you ever need to use `std::any`? Basically you know all the request/response types at compile time. The only place you actually need to identify a type is upon receiving a new message (since you receive just bytes). All you have to do in your case is to have a set of overloads for `getResponse(ChannelIndex, RequestType)` to support dispatching with `std::visit`. Overall it is very unclear what exactly you want to implement. You need to state your problem, not the intent for your solution. You might as well realize that you actually don't know what you want. Take a step back – Sergey Kolesnik Mar 09 '23 at 15:21
  • Thank you for your involvement. Please look I've added some simplified usage example (I got things working, so just may be interested in a better implementation approach). In different applications, there are differen request/response pairs. – Brosbiln Mar 09 '23 at 15:42
  • your handwritten `getPendingRequestsNum` make absolutelly no sense. Basically you could have a `switch` table doing exactly that - enumerating requests by id and dispatching them. And each time you add a new request type, you have to modify the code of `requestThread`. Why `std::variant` then? You get no benefit from it at all. You see, using `std::variant` with `std::visit` is basically an extendable `switch` at compile time, so that you can add neq request types without modifying your `requestThread` code. – Sergey Kolesnik Mar 09 '23 at 15:49
  • btw, you should be aware that you can't `// sleep for 10 milliseconds` - you need a real-time OS for that. On Windows you may sleep 20-30 milliseconds minimum – Sergey Kolesnik Mar 09 '23 at 15:54
  • I’m glad to learn from you, but it is kinda confusing. What can be improved? This code is working , the handwritten function returns the number of pending requests for specific request type. The problem is using variant without visit (I know how it works) or using the variant in overall? I didn’t try to acknowledge all design constraints, but specific c++ related issue. Can you please provide some example, where I can see what do you mean? – Brosbiln Mar 09 '23 at 18:36