3
\$\begingroup\$

A bit of context: I've had a fair share of programming in Java but want to refine my C++ skills with a college task. I've wanted to focus on C++11 and C++14, as they are recent but not latest, and C++14 is the default revision of my compiler.

I want to share a header (hpp) file and a test implementation one (cpp), and would like a review of naming, aliasing (with using), use of ordering functions (operator<), encapsulation, defensive programming (invariants), proper use of data clases and algorithms and whatever you may need to point, like template use or abuse.

Here's the code, disim.hpp:

#ifndef __DISIM_HPP
#define __DISIM_HPP

// std::ostream
#include <iostream>

// std::map
#include <map>

// std::priority_queue
#include <queue>

// std::vector
#include <vector>

// std::function
#include <functional>

namespace disim {

/// Type of the Event name (const char*, std::string_view, std::string)
using EventName = const char*;

/// Collection of Event names (std::vector)
using EventNameCollection = std::vector<EventName>;

/// Type of the Event timestamp (unsigned, long)
using EventTime = unsigned;

// TODO: Proper use of a struct? Rather use a std::tuple?
struct Event {
    EventName name;
    EventTime time;

    friend auto operator<(Event const& l, Event const& r) 
    { 
        return l.time < r.time; 
    }

    friend auto operator>(Event const& l, Event const& r) { return !(l < r) && l.time != r.time; }
};

// TODO: Convenience method: Useless? -> Directly disim::Event = {...}. Encapsulation?
Event make_event(EventName const& name, EventTime const& timestamp) 
{
    return {name, timestamp};
}

/// Collection of Events (std::vector)
using EventCollection = std::vector<Event>;

/// Function that generates a new Event for a given Timestamp
using EventGenerator = std::function<Event(EventName, EventTime)>;

/**
 * TODO: How to properly document this function?
 * 
 * Function that handles an event occurrence.
 * 
 * @param 0 Current System state
 * @param 1 Current (on trigger) time
 * @param 2 Difference between Event triggers
 */
template<class System>
using EventTrigger = std::function<EventNameCollection(System&, EventTime const&, EventTime const&)>;

// TODO: Is Name Redundant? Or as a whole? Only relevant as a Big Holder of the Three
template<class System>
struct EventHandler
{
    EventName name; 
    EventGenerator generator;
    EventTrigger<System> trigger;
};

// TODO: Convenience method: Useless? -> Directly disim::EventHandler = {...}. Encapsulation?
template<class System>
EventHandler<System> make_handler(EventName const& name, EventGenerator generator, EventTrigger<System> trigger)
{
    return {name, generator, trigger};
}

template<class System>
class DiscreteSimulation 
{
    using ExitFunction = std::function<bool(System const&)>;
    using EventQueue = std::priority_queue<Event, EventCollection, std::greater<Event>>;

private:
    std::map<EventName, EventTrigger<System>> triggers;
    std::map<EventName, EventGenerator> generators;

public:
    DiscreteSimulation(std::initializer_list<EventHandler<System>> handlers) 
    {
        for(auto& h : handlers) {
            triggers.insert( {h.name, h.trigger} );
            generators.insert( {h.name, h.generator} );
        }   
    }

    System execute(System const& initial_state, 
                   EventCollection const& initial_events, 
                   ExitFunction exit_trigger) const 
    {
        if(initial_events.empty()) {
            return initial_state;
        }

        auto state = initial_state;
        auto queue = EventQueue(std::begin(initial_events), std::end(initial_events));

        EventTime clock = queue.top().time;
        EventTime time_diff = clock;
        while(!queue.empty() && !exit_trigger(state)) 
        {
            auto event = queue.top();
            queue.pop();

            auto trigger_it = triggers.find(event.name);
            if(trigger_it != triggers.end()) {
                EventTrigger<System> trigger = (*trigger_it).second;

                // Process event; get procced events
                auto generated_names = trigger(state, clock, time_diff);

                // Push every new event into the Queue
                for(auto const& name : generated_names) {
                    auto generator_it = generators.find(name);
                    if(generator_it != generators.end()) {
                        EventGenerator generator = (*generator_it).second;
                        Event generated_event = generator(name, clock);

                        queue.push(generated_event);
                    } else {
                        // Event not generated
                    }
                }
            } else {
                // Unhandled event
            }

            time_diff = queue.top().time - clock;
            clock = queue.top().time;
        }

        return state;
    }
};
} // end-namespace

#endif

If you need some context on what a Discrete Event simulation is, it's basically an Agenda of events that are ordered based on "sooner to occur" which act on a system (the domain-specific aggregate of data) State. Events may trigger the scheduling of other, different Events.

The test_sim.cpp file can help showing a simple test case: A Test Event increments the iterations of a given System, and generates two new Test Events, showing the correct ordering of the events.

#include <iostream>
#include <random>
#include "disim.hpp"

struct TestSystem {
    unsigned iterations = 0;
};

disim::EventGenerator test_gen = [](auto name, auto time) -> auto
{
    static std::random_device rd;
    static std::mt19937 gen(rd());
    static std::uniform_int_distribution<unsigned> dis(1, 100);

    auto new_time = time + dis(gen);

    std::printf("Test generated for time=%d\n", new_time);
    return disim::make_event(name, new_time);
};

disim::EventTrigger<TestSystem> test_trigger = [](auto& sys, auto time, auto diff) -> auto
{
    sys.iterations += 1;

    std::printf("Evaluating Test at time=%d\n", time);

    return std::vector<const char*> {"Test", "Test"};
};

std::function<bool(TestSystem const&)> exit_function = [](auto& sys)
{
    return !(sys.iterations < 10);
};

int main() {
    auto simulation = disim::DiscreteSimulation<TestSystem> {
        disim::make_handler("Test", test_gen, test_trigger)
    };

    auto initial_events = std::vector<disim::Event> {
        disim::make_event("Test", 0)
    };

    auto state_before = TestSystem {};
    auto state_after  = simulation.execute(state_before, initial_events, exit_function);

    std::printf("State before -> iterations=%d\n", state_before.iterations);
    std::printf("State after  -> iterations=%d\n", state_after.iterations);

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.