Doing Something Once
Mar. 5th, 2022 10:41 am There's a not uncommon use case in programming where you want to do something once in a specific context: the typical example being where you want to log an event the first time, and only the first time, it happens (and it's not part of the program's startup, and typically may never happen at all, but if it happens at all may occur many, many times, which is why you want to limit the logging).
ETA: you can make this more generic and more encapsulated by making the smart pointer a custom class which hides the delegation entirely and implements the logging interface (and, naturally, holds a smart pointer as a delegate).
There's a common idiom for this:
static bool logged = false;
if (!logged)
{
DoLogging(message);
logged = true;
}
We will assume for the sake of discussion that this is not a routine which can be called by multiple threads at once, which would complicate the issue somewhat.
There are two small problems with this. The first is that it's an ugly little block, especially if it's used multiple times. (You can't extract it into a function called in multiple contexts because that static variable has to be unique for each context.) The second is that it's inefficient: we have to check the flag every time through. That means branching (always inefficient) and, worse, because it's a static variable it will almost certainly not be in the memory cache, making for a slow load.
So what can we do? We need a mechanism which chooses one path once, another path after that, and which neither branches nor uses static data on subsequent runs.
If we think of it in terms of typical one-time activities, we might think of a constructor. We can do the following:
class OneTimeLogger
{
public:
OneTimeLogger(const std::string& inMessage)
{
DoLogging (inMessage);
}
};
In context:
//...do stuff
static OneTimeLogger logger(message);
//...do other stuff
This looks attractive, but actually it solves nothing. First, because it's not a standard idiom it's going to confuse the hell out of a maintenance programmer. Any line which requires a detailed comment saying "do not delete this apparent no-op" is a bad thing. Secondly, it actually hides an expensive if/else switch. The compiler has to emit code checking for a first time on initializing a static object, and, worse, at least post-C++11 it has to make that check, and the initialization, thread-safe. (If this *is* a multi-threaded situation with potential contention, this might mean that the maintenance cost is worth it; it's the simplest thread-safe way of doing this I know. In that case, you might want to add a no-op log() function to the class and call it in every pass, so that it looks normal to a maintainer, although then you have to explain the no-op function where it's defined. The next alternative involves replacing the unique_ptr in the solution below with a shared_ptr or putting a mutex around the point where the smart pointer is updated.)
The expensive part of the test is one-time, but the test is still there, and it's going to be on static data. All that we've done is hidden the if/else test.
The other way out is polymorphism. Assuming that the calling context is in a class and not a freestanding function, we can do the following:
class MyClass
{
public:
class IThisEventLogger
{
virtual ~IThisEventLogger() { }
virtual void log() = 0;
};
class OneTimeEventLogger : public IThisEventLogger
{
OneTimeEventLogger(std::unique_ptr<IThisEventLogger>& inParent, const std::string& inMessage):
m_parent(inParent), m_message(inMessage)
{ }
class NullThisEventLogger: public IThisEventLogger {
virtual void log() { }
};
virtual void log()
{
DoLogging (m_message);
m_parent.reset( new NullThisEventLogger());
}
private:
std::unique_ptr<IThisEventLogger>& m_parent;
std::string m_message;
};
MyClass(): m_logger(new OneTimeEventLogger(m_logger, "Message to be logged"))
{ }
void doSomething()
{
//...stuff
m_logger->log();
//... more stuff
}
private:
std::unique_ptr<IThisEventLogger> m_logger;
};
The trick is that the reset call in the logger is a fancy way of doing "delete this" (i.e. committing suicide), which is entirely legal in C++. (Also, passing in the address of the parent while constructing the parent is fine, because nothing happens with it until the object is fully constructed.) We choose to pass the message in the constructor because it reduces the cost of the no-op call to a bare minimum.
The first time log() is called, the message gets printed, and then the execution path for all future calls is changed to a no-op function. We still have a virtual call, but that should be on an object in the cache, and virtual calls are typically cheaper than branching. The call site is simplified; the only time complexity appears is when looking into the very detailed implementation, well away from the business logic in doSomething();
If the logging logic is sufficiently generic, the machinery for managing this can be extracted and reused so that it doesn't clog up the interface of the calling class. If the logic is complicated internally then the logger and the interface will have to be created locally. (If we have to log four variables, two of them integers and one floating-point as well as a string, we want to put the expense of generating the message to log into the one-time call as well, so a generic "pass a string to log as a parameter" may not be a good match, and pre-generating the message in the constructor, as above, may be impossible -- though usually something logged once and for all will have a very generic message).
The downside is that you need a separate logger for every message you want to print once - a classic trade of space for time. Of course, if your parent class is well-designed, it will have limited responsibilities, and the number of instances it will need will be correspondingly small. And the logger itself is cheap to construct - not much larger or costlier than the text of the message it logs; if it builds a custom message its content model is even simpler and its associated costs are even smaller.
ETA: you can make this more generic and more encapsulated by making the smart pointer a custom class which hides the delegation entirely and implements the logging interface (and, naturally, holds a smart pointer as a delegate).