The output system is more or less re-implemented with what I believe to be the same amount of features, but with a different architecture. For example, LogWriter (a class inheriting from OutputListener which writes output to a log file) is now implemented as a singleton, instead of accessing it indirectly through OutputHandler.
OutputManager, OutputStream, and orxout()
OutputHandler is removed and its functionality split into OutputManager, which distributes output to the registered OutputListeners, and OutputStream, which is a derivative of std::ostream and is used similar to std::cout.
In particular, you can write this code: OutputStream() << "Some text" << endl; This will send "Some text" to the OutputManager. Since creating an instance of OutputStream for every line of output is slow, orxout() returns a static instance of this class. Note that it may return a different static instance in every library of orxonox, but that doesn't matter. This design allows us to use different instances of OutputStream in each thread, even though thread safety is not yet achieved (OutputManager is not thread safe). But future changes are possible.
It's important to know that OutputStream sends a "message" to OutputManager whenever you use "endl". An example:
Code: Select all
OutputStream() << "Some text"; // does NOT send anything to OutputManager
OutputStream() << "Some text" << endl; // sends "Some text" to OutputManager
OutputStream() << "Some text" << '\n'; // does NOT send anything to OutputManager
OutputStream() << "Some text" << '\n' << "More text" << endl; // sends "Some text\nMore text" to OutputManager
Note that this example constructs a temporary instance of OutputStream for every line, hence output which is not sent to OutputManager is lost after the ";". Using orxout() this behaves differently:
Code: Select all
// this sends "SomeText" to OutputManager:
orxout() << "Some";
orxout() << "Text";
orxout() << endl;
Summarized, you should always use "endl" at the end of a message. Use \n only to split a single message into multiple lines. (Note that using endl is superior to \n anyway, since endl flushes the output buffer, but \n doesn't. Hence one should always use endl, also with std::cout)
Output levels
Concerning the output levels, there is also a point worth mentioning: They are not defined as numbers from 0 to x anymore, instead they are defined as binary masks from 0x0001 to 0x8000. It doesn't make sense to combine these masks for one line of output (e.g. the level of some output should always have only one bit set to 1), but it makes sense for OutputListeners to define the output they want as a mask (e.g. user_error | user_warning defines a mask that accepts only errors and warnings at the user level). However this is usually hidden from the developers, so the only common way to use output levels is in orxout(level).
Output contexts
With output masks we have the same, they are also defined as binary masks but this time with 64 bits, allowing a maximum of 64 different contexts. Note that "no specific context" is also a context, hence we remain with 63 effectively usable bits. As with output levels, an OutputListener can also define a mask of allowed contexts. The default listeners (LogWriter, ConsoleWriter) accept output of all contexts (their mask equals 0xFFFFFFFFFFFFFFFF).
However defining a context is a bit more difficult, because I want to assign a string to each context, e.g. "Network" to the network context. This allows us to print the output of a message to the log, and it also allows us to define the desired output contexts in the config file as strings instead of very complicated integer numbers.
To do so, one could use a pattern like this:
Code: Select all
OutputContext network = registerContext(0x0000000000000001, "Network");
But since we have to call registerContext() anyway, this function can generate the context itself, which leaves us with this code:
Code: Select all
OutputContext network = registerContext("Network");
In particular, OutputManager maintains a map with all contexts and their strings. If a string doesn't exist in the map, a new context is generated, otherwise the existing context is returned.
Unfortunately there is a problem because the contexts are defined in a common header file, these statements are included in virtually every source file. This means that registerContext() is called once for every context TIMES the number of source files. If in future we have 50 contexts and 2000 source files (we already have more than 350), we end up with 100'000 calls to registerContext(), which means 100'000 lookups in a map for a string. I have no clue how long this takes, but let's say 1 second. This is not much, but 1 second more between you start orxonox and before it actually starts loading might just be 1 second too much for the user.
So I decided to use functions instead of global variables, which allows a lazy initialization (and registration) of the contexts:
Code: Select all
OutputContext network() { return registerContext("Network"); }
Now this calls registerContext() every time you use the context, which can be improved by calling it only once and storing the result:
Code: Select all
OutputContext network() { static OutputContext context = registerContext("Network"); return context; }
Now this would mean that you have to use contexts this way (Note that all contexts are defined in the "context" namespace):
Code: Select all
orxout(level, context::network()) << "Some text" << endl;
But I'm a friendly person and allow you to pass contexts also directly as a function:
Code: Select all
orxout(level, context::network) << "Some text" << endl;
orxout then calls the context-function internally.
So far that's it, lots of internal details, but I thought some of you (reto) might be interested. Now I'm about to replace calls to the old implementation with their new counterparts. This may take some time because, for example, I also have to adapt the bindings of lua and tcl to orxout (formerly COUT) in a way which allows using the new levels and possibly also contexts.