이글의 한글판은 다음 링크를 따라 가시면 됩니다.

C++이야기 스물아홉번째: Portable C++ Timer Class #2


This time, let's dig into what should go into the Timer-related classes. I think we should start with TimerDriver because it is a class which users will play with in most cases.

As I mentioned before, the TimerDriver is a class which will expire registered timer instances at the designated time. Considering this, it is apparently necessary to define member functions for registering and unregistering timer instances as follows.

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);
};

After defining this interface, we quickly come to the idea that we need some storage for registered timer instances. Then which data structure should we use to store them? We need to think how for users to use the TimerDriver to pick up the most appropriate data structure.

The only member functions which users can use upto now are RegisterTimer() and UnregisterTimer(), which are similar to insertion and deletion. Users will drive the registered timer instances by traversing timer instances. And I'm quite sure that users do need neither sorted timer list, nor direct access to a timer instance, nor searching a timer instance very fast.

So, I think that the most appropriate data structure for the registered timer instances is std::list.

#include <list>

class Timer;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);

private:
    std::list<Timer> timer_list_;
};

Next, let's think about RegisterTimer() and UnregisterTimer()'s parameters and return values.

I introduced 'TimerId' type as a return value of RegisterTimer() and input parameter of UnregisterTimer(). The necessity of 'TimerId' type is very clear but what would the TimerId be like? Should the TimerDriver manage a identifier pool in it or could the TimerId be Timer*? If the TimerDriver manage a identifer pool, then where should each assigned identifer to a timer instance be stored? Is it OK that timer_list_ be just list<Timer> or should be something else, say, list<Timer*>? All these questions will hang around our mind until we come to sort of a conclusion. Totally, like a mess, huh? even for this simple class?

But still, as a software engineer, we need to handle this mess, finding rationales for our choices. Our life is full of problems and living itself is a journey to find a solution, right? So, don't be panic and calm down. Actually, all the questions are related to each other. And when we find a clue to one question, then Boom! we can find answers suddenly for all questions.

Let's solve problems one by one. Take a deeeeeeeeeeeeeeep breath.

TimerId! Should the TimerDriver manage identifer pool? Probably. The TimerDriver can assign a free id from identifier pool to newly registered timer instance whenever a user ask it to do by calling RegisterTimer(). And it should store the assigned identifier somewhere so that it can delete the timer when UnregisterTimer() is requested. But I really hesitate to do it now because that solution seems to be an overkill, considering current requirements. I DO prefer simpler design, which leads us to lesser codes and lesser bugs. So I exclude identifier pool solution from candidate designs. Then what are other solutions there? What do you think about the idea that we can just define the TimerId as Timer*

#include <list>

class Timer;
typedef Timer* TimerId;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);

private:
    std::list<Timer> timer_list_;
};

With this approach, we don't need to define some sort of identifier pool and RegisterTimer() implementation will be very simple because it can just return &t as a return value. right? Then we found nearly perfect solution. right? Uh! Uh! Uh! The life is not that easy. Let's think it more carefully.

To simply define the TimerId as Timer* is to open a gate for user to handle directly internal representation of timer instances, which breaks encapsulation rule. If we change the internal representation of timer instance inside TimerDriver, then that will break some user codes which are dependent on the fact that TimerId is Timer*. So I'd rather to define TimerId as void*. Then users will have no idea about what the internal representation of Timer inside TimerDriver.

#include <list>

class Timer;
typedef void* TimerId;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);

private:
    std::list<Timer> timer_list_;
};

Next, let's think about timer_list_ member variable. As I mentioned in the previous article, I'd like to define PeriodicTimer and OneShotTimer which are derived from Timer. We should define common interface in Timer and two timer classes will implement that interface so that they show polymorphic behaviors. Then, we need to define timer_list_ as std::list<Timer*> instead.

#include <list>

class Timer;
typedef void* TimerId;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);

private:
    std::list<Timer*> timer_list_;
};

I think there is one missing member function in current TimerDriver's interface. That is, sort of a driving function. Typically users first register timer instances and then run those instances. So I add Run() member function as follows

#include <list>

class Timer;
typedef void* TimerId;

class TimerDriver {
public:
    TimerId RegisterTimer(const Timer& t);
    void UnregisterTimer(TimerId tid);
    void Run();

private:
    std::list<Timer*> timer_list_;
};

Now, I feel that the interface of TimerDriver is nearly perfect. Let's think about the implementation of TimerDriver in the next article.