Note that there are some explanatory texts on larger screens.

plurals
  1. POA better LOG() macro using template metaprogramming
    primarykey
    data
    text
    <p>A typical LOG() macro-based logging solution may look something like this:</p> <pre><code>#define LOG(msg) \ std::cout &lt;&lt; __FILE__ &lt;&lt; "(" &lt;&lt; __LINE__ &lt;&lt; "): " &lt;&lt; msg &lt;&lt; std::endl </code></pre> <p>This allows programmers to create data rich messages using convenient and type-safe streaming operators:</p> <pre><code>string file = "blah.txt"; int error = 123; ... LOG("Read failed: " &lt;&lt; file &lt;&lt; " (" &lt;&lt; error &lt;&lt; ")"); // Outputs: // test.cpp(5): Read failed: blah.txt (123) </code></pre> <p>The problem is that this causes the compiler to inline multiple ostream::operator&lt;&lt; calls. This increases the generated code and therefore function size, which I suspect may hurt instruction cache performance and hinder the compiler's ability to optimize the code.</p> <p>Here's a "simple" alternative that replaces the inlined code with a call to a <strong>variadic template function</strong>:</p> <p><code>*********</code> <strong>SOLUTION #2: VARIADIC TEMPLATE FUNCTION</strong> <code>*********</code></p> <pre><code>#define LOG(...) LogWrapper(__FILE__, __LINE__, __VA_ARGS__) // Log_Recursive wrapper that creates the ostringstream template&lt;typename... Args&gt; void LogWrapper(const char* file, int line, const Args&amp;... args) { std::ostringstream msg; Log_Recursive(file, line, msg, args...); } // "Recursive" variadic function template&lt;typename T, typename... Args&gt; void Log_Recursive(const char* file, int line, std::ostringstream&amp; msg, T value, const Args&amp;... args) { msg &lt;&lt; value; Log_Recursive(file, line, msg, args...); } // Terminator void Log_Recursive(const char* file, int line, std::ostringstream&amp; msg) { std::cout &lt;&lt; file &lt;&lt; "(" &lt;&lt; line &lt;&lt; "): " &lt;&lt; msg.str() &lt;&lt; std::endl; } </code></pre> <p>The compiler automatically generates new instantiations of the template function as needed depending on the number, kind, and order of message arguments.</p> <p>The benefit is there are fewer instructions at each call site. The downside is the user must pass the message parts as function parameters instead of combining them using streaming operators:</p> <pre><code>LOG("Read failed: ", file, " (", error, ")"); </code></pre> <p><code>*********</code> <strong>SOLUTION #3: EXPRESSION TEMPLATES</strong> <code>*********</code></p> <p>At @DyP's suggestion I created an alternative solution that uses <strong>expression templates</strong>:</p> <pre><code>#define LOG(msg) Log(__FILE__, __LINE__, Part&lt;bool, bool&gt;() &lt;&lt; msg) template&lt;typename T&gt; struct PartTrait { typedef T Type; }; // Workaround GCC 4.7.2 not recognizing noinline attribute #ifndef NOINLINE_ATTRIBUTE #ifdef __ICC #define NOINLINE_ATTRIBUTE __attribute__(( noinline )) #else #define NOINLINE_ATTRIBUTE #endif // __ICC #endif // NOINLINE_ATTRIBUTE // Mark as noinline since we want to minimize the log-related instructions // at the call sites template&lt;typename T&gt; void Log(const char* file, int line, const T&amp; msg) NOINLINE_ATTRIBUTE { std::cout &lt;&lt; file &lt;&lt; ":" &lt;&lt; line &lt;&lt; ": " &lt;&lt; msg &lt;&lt; std::endl; } template&lt;typename TValue, typename TPreviousPart&gt; struct Part : public PartTrait&lt;Part&lt;TValue, TPreviousPart&gt;&gt; { Part() : value(nullptr), prev(nullptr) { } Part(const Part&lt;TValue, TPreviousPart&gt;&amp;) = default; Part&lt;TValue, TPreviousPart&gt; operator=( const Part&lt;TValue, TPreviousPart&gt;&amp;) = delete; Part(const TValue&amp; v, const TPreviousPart&amp; p) : value(&amp;v), prev(&amp;p) { } std::ostream&amp; output(std::ostream&amp; os) const { if (prev) os &lt;&lt; *prev; if (value) os &lt;&lt; *value; return os; } const TValue* value; const TPreviousPart* prev; }; // Specialization for stream manipulators (eg endl) typedef std::ostream&amp; (*PfnManipulator)(std::ostream&amp;); template&lt;typename TPreviousPart&gt; struct Part&lt;PfnManipulator, TPreviousPart&gt; : public PartTrait&lt;Part&lt;PfnManipulator, TPreviousPart&gt;&gt; { Part() : pfn(nullptr), prev(nullptr) { } Part(const Part&lt;PfnManipulator, TPreviousPart&gt;&amp; that) = default; Part&lt;PfnManipulator, TPreviousPart&gt; operator=(const Part&lt;PfnManipulator, TPreviousPart&gt;&amp;) = delete; Part(PfnManipulator pfn_, const TPreviousPart&amp; p) : pfn(pfn_), prev(&amp;p) { } std::ostream&amp; output(std::ostream&amp; os) const { if (prev) os &lt;&lt; *prev; if (pfn) pfn(os); return os; } PfnManipulator pfn; const TPreviousPart* prev; }; template&lt;typename TPreviousPart, typename T&gt; typename std::enable_if&lt; std::is_base_of&lt;PartTrait&lt;TPreviousPart&gt;, TPreviousPart&gt;::value, Part&lt;T, TPreviousPart&gt; &gt;::type operator&lt;&lt;(const TPreviousPart&amp; prev, const T&amp; value) { return Part&lt;T, TPreviousPart&gt;(value, prev); } template&lt;typename TPreviousPart&gt; typename std::enable_if&lt; std::is_base_of&lt;PartTrait&lt;TPreviousPart&gt;, TPreviousPart&gt;::value, Part&lt;PfnManipulator, TPreviousPart&gt; &gt;::type operator&lt;&lt;(const TPreviousPart&amp; prev, PfnManipulator value) { return Part&lt;PfnManipulator, TPreviousPart&gt;(value, prev); } template&lt;typename TPart&gt; typename std::enable_if&lt; std::is_base_of&lt;PartTrait&lt;TPart&gt;, TPart&gt;::value, std::ostream&amp;&gt;::type operator&lt;&lt;(std::ostream&amp; os, const TPart&amp; part) { return part.output(os); } </code></pre> <p>The expression templates solution allows the programmer to use the familiar convenient and type-safe streaming operators:</p> <pre><code>LOG("Read failed: " &lt;&lt; file &lt;&lt; " " &lt;&lt; error); </code></pre> <p>However, when <code>Part&lt;A, B&gt;</code> creation is inlined no operator&lt;&lt; calls are made, giving us the benefit of both worlds: convenient and type-safe streaming operators + fewer instructions. ICC13 with -O3 produces the following assembly code for the above:</p> <pre><code>movl $.L_2__STRING.3, %edi movl $13, %esi xorl %eax, %eax lea 72(%rsp), %rdx lea 8(%rsp), %rcx movq %rax, 16(%rsp) lea 88(%rsp), %r8 movq $.L_2__STRING.4, 24(%rsp) lea 24(%rsp), %r9 movq %rcx, 32(%rsp) lea 40(%rsp), %r10 movq %r8, 40(%rsp) lea 56(%rsp), %r11 movq %r9, 48(%rsp) movq $.L_2__STRING.5, 56(%rsp) movq %r10, 64(%rsp) movq $nErrorCode.9291.0.16, 72(%rsp) movq %r11, 80(%rsp) call _Z3LogI4PartIiS0_IA2_cS0_ISsS0_IA14_cS0_IbbEEEEEENSt9enable_ifIXsr3std10is_base_ofI9PartTraitIT_ESA_EE5valueEvE4typeEPKciRKSA_ </code></pre> <p>The total is 19 instructions including one function call. It appears each additional argument streamed adds 3 instructions. The compiler creates a different Log() function instantiation depending on the number, kind, and order of message parts, which explains the bizarre function name.</p> <p><code>*********</code> <strong>SOLUTION #4: CATO'S EXPRESSION TEMPLATES</strong> <code>*********</code></p> <p>Here is Cato's excellent solution with a tweak to support stream manipulators (eg endl):</p> <pre><code>#define LOG(msg) (Log(__FILE__, __LINE__, LogData&lt;None&gt;() &lt;&lt; msg)) // Workaround GCC 4.7.2 not recognizing noinline attribute #ifndef NOINLINE_ATTRIBUTE #ifdef __ICC #define NOINLINE_ATTRIBUTE __attribute__(( noinline )) #else #define NOINLINE_ATTRIBUTE #endif // __ICC #endif // NOINLINE_ATTRIBUTE template&lt;typename List&gt; void Log(const char* file, int line, LogData&lt;List&gt;&amp;&amp; data) NOINLINE_ATTRIBUTE { std::cout &lt;&lt; file &lt;&lt; ":" &lt;&lt; line &lt;&lt; ": "; output(std::cout, std::move(data.list)); std::cout &lt;&lt; std::endl; } struct None { }; template&lt;typename List&gt; struct LogData { List list; }; template&lt;typename Begin, typename Value&gt; constexpr LogData&lt;std::pair&lt;Begin&amp;&amp;, Value&amp;&amp;&gt;&gt; operator&lt;&lt;(LogData&lt;Begin&gt;&amp;&amp; begin, Value&amp;&amp; value) noexcept { return {{ std::forward&lt;Begin&gt;(begin.list), std::forward&lt;Value&gt;(value) }}; } template&lt;typename Begin, size_t n&gt; constexpr LogData&lt;std::pair&lt;Begin&amp;&amp;, const char*&gt;&gt; operator&lt;&lt;(LogData&lt;Begin&gt;&amp;&amp; begin, const char (&amp;value)[n]) noexcept { return {{ std::forward&lt;Begin&gt;(begin.list), value }}; } typedef std::ostream&amp; (*PfnManipulator)(std::ostream&amp;); template&lt;typename Begin&gt; constexpr LogData&lt;std::pair&lt;Begin&amp;&amp;, PfnManipulator&gt;&gt; operator&lt;&lt;(LogData&lt;Begin&gt;&amp;&amp; begin, PfnManipulator value) noexcept { return {{ std::forward&lt;Begin&gt;(begin.list), value }}; } template &lt;typename Begin, typename Last&gt; void output(std::ostream&amp; os, std::pair&lt;Begin, Last&gt;&amp;&amp; data) { output(os, std::move(data.first)); os &lt;&lt; data.second; } inline void output(std::ostream&amp; os, None) { } </code></pre> <p>As Cato points out, the benefit over the last solution is that it results in fewer function instantiations since the const char* specialization handles all string literals. It also causes fewer instructions to be generated at the call site:</p> <pre><code>movb $0, (%rsp) movl $.L_2__STRING.4, %ecx movl $.L_2__STRING.3, %edi movl $20, %esi lea 212(%rsp), %r9 call void Log&lt;pair&lt;pair&lt;pair&lt;pair&lt;None, char const*&gt;, string const&amp;&gt;, char const*&gt;, int const&amp;&gt; &gt;(char const*, int, LogData&lt;pair&lt;pair&lt;pair&lt;pair&lt;None, char const*&gt;, string const&amp;&gt;, char const*&gt;, int const&amp;&gt; &gt; const&amp;) </code></pre> <p><strong>Please let me know if you can think of any way to improve the performance or usability of this solution.</strong></p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload