使用模板元编程的更好的 LOG() 宏

Posted

技术标签:

【中文标题】使用模板元编程的更好的 LOG() 宏【英文标题】:A better LOG() macro using template metaprogramming 【发布时间】:2013-10-25 07:13:37 【问题描述】:

典型的基于 LOG() 宏的日志记录解决方案可能如下所示:

#define LOG(msg) \
    std::cout << __FILE__ << "(" << __LINE__ << "): " << msg << std::endl 

这允许程序员使用方便且类型安全的流式操作符创建数据丰富的消息:

string file = "blah.txt";
int error = 123;
...
LOG("Read failed: " << file << " (" << error << ")");

// Outputs:
// test.cpp(5): Read failed: blah.txt (123)

问题在于这会导致编译器内联多个 ostream::operator

这是一个“简单”的替代方法,它用调用可变参数模板函数替换内联代码:

********* 解决方案#2:可变模板函数 *********

#define LOG(...) LogWrapper(__FILE__, __LINE__, __VA_ARGS__)

// Log_Recursive wrapper that creates the ostringstream
template<typename... Args>
void LogWrapper(const char* file, int line, const Args&... args)

    std::ostringstream msg;
    Log_Recursive(file, line, msg, args...);


// "Recursive" variadic function
template<typename T, typename... Args>
void Log_Recursive(const char* file, int line, std::ostringstream& msg, 
                   T value, const Args&... args)

    msg << value;
    Log_Recursive(file, line, msg, args...);


// Terminator
void Log_Recursive(const char* file, int line, std::ostringstream& msg)

    std::cout << file << "(" << line << "): " << msg.str() << std::endl;

编译器会根据需要根据消息参数的数量、种类和顺序自动生成模板函数的新实例。

好处是每个呼叫站点的指令更少。缺点是用户必须将消息部分作为函数参数传递,而不是使用流操作符组合它们:

LOG("Read failed: ", file, " (", error, ")");

********* 解决方案#3:表达式模板 *********

根据@DyP 的建议,我创建了一个使用表达式模板的替代解决方案:

#define LOG(msg) Log(__FILE__, __LINE__, Part<bool, bool>() << msg)

template<typename T> 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<typename T>
void Log(const char* file, int line, const T& msg) NOINLINE_ATTRIBUTE

    std::cout << file << ":" << line << ": " << msg << std::endl;


template<typename TValue, typename TPreviousPart>
struct Part : public PartTrait<Part<TValue, TPreviousPart>>

    Part()
        : value(nullptr), prev(nullptr)
     

    Part(const Part<TValue, TPreviousPart>&) = default;
    Part<TValue, TPreviousPart> operator=(
                           const Part<TValue, TPreviousPart>&) = delete;

    Part(const TValue& v, const TPreviousPart& p)
        : value(&v), prev(&p)
     

    std::ostream& output(std::ostream& os) const
    
        if (prev)
            os << *prev;
        if (value)
            os << *value;
        return os;
    

    const TValue* value;
    const TPreviousPart* prev;
;

// Specialization for stream manipulators (eg endl)

typedef std::ostream& (*PfnManipulator)(std::ostream&);

template<typename TPreviousPart>
struct Part<PfnManipulator, TPreviousPart>
    : public PartTrait<Part<PfnManipulator, TPreviousPart>>

    Part()
        : pfn(nullptr), prev(nullptr)
     

    Part(const Part<PfnManipulator, TPreviousPart>& that) = default;
    Part<PfnManipulator, TPreviousPart> operator=(const Part<PfnManipulator,
                                                  TPreviousPart>&) = delete;

    Part(PfnManipulator pfn_, const TPreviousPart& p)
    : pfn(pfn_), prev(&p)
     

    std::ostream& output(std::ostream& os) const
    
        if (prev)
            os << *prev;
        if (pfn)
            pfn(os);
        return os;
    

    PfnManipulator pfn;
    const TPreviousPart* prev;
;

template<typename TPreviousPart, typename T>
typename std::enable_if<
    std::is_base_of<PartTrait<TPreviousPart>, TPreviousPart>::value, 
    Part<T, TPreviousPart> >::type
operator<<(const TPreviousPart& prev, const T& value)

    return Part<T, TPreviousPart>(value, prev);


template<typename TPreviousPart>
typename std::enable_if<
    std::is_base_of<PartTrait<TPreviousPart>, TPreviousPart>::value,
    Part<PfnManipulator, TPreviousPart> >::type
operator<<(const TPreviousPart& prev, PfnManipulator value)

    return Part<PfnManipulator, TPreviousPart>(value, prev);


template<typename TPart>
typename std::enable_if<
    std::is_base_of<PartTrait<TPart>, TPart>::value,
    std::ostream&>::type
operator<<(std::ostream& os, const TPart& part)

    return part.output(os);

表达式模板解决方案允许程序员使用熟悉的方便且类型安全的流式操作符:

LOG("Read failed: " << file << " " << error);

但是,当 Part&lt;A, B&gt; 内联创建时,不会进行 operator

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_

总共有 19 条指令,包括一个函数调用。流式传输的每个附加参数似乎都增加了 3 条指令。编译器根据消息部分的数量、种类和顺序创建不同的 Log() 函数实例化,这解释了奇怪的函数名称。

********* 解决方案 #4:CATO 的表达式模板 *********

这是 Cato 的出色解决方案,其中包含支持流操纵器(例如 endl)的调整:

#define LOG(msg) (Log(__FILE__, __LINE__, LogData<None>() << 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<typename List>
void Log(const char* file, int line, 
         LogData<List>&& data) NOINLINE_ATTRIBUTE

    std::cout << file << ":" << line << ": ";
    output(std::cout, std::move(data.list));
    std::cout << std::endl;


struct None  ;

template<typename List>
struct LogData 
    List list;
;

template<typename Begin, typename Value>
constexpr LogData<std::pair<Begin&&, Value&&>> operator<<(LogData<Begin>&& begin, 
                                                          Value&& value) noexcept

    return  std::forward<Begin>(begin.list), std::forward<Value>(value) ;


template<typename Begin, size_t n>
constexpr LogData<std::pair<Begin&&, const char*>> operator<<(LogData<Begin>&& begin, 
                                                              const char (&value)[n]) noexcept

    return  std::forward<Begin>(begin.list), value ;


typedef std::ostream& (*PfnManipulator)(std::ostream&);

template<typename Begin>
constexpr LogData<std::pair<Begin&&, PfnManipulator>> operator<<(LogData<Begin>&& begin, 
                                                                 PfnManipulator value) noexcept

    return  std::forward<Begin>(begin.list), value ;


template <typename Begin, typename Last>
void output(std::ostream& os, std::pair<Begin, Last>&& data)

    output(os, std::move(data.first));
    os << data.second;


inline void output(std::ostream& os, None)
 

正如 Cato 所指出的,与最后一个解决方案相比,它的好处在于它减少了函数实例化,因为 const char* 专门化处理了所有字符串文字。它还导致在调用站点生成的指令更少:

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<pair<pair<pair<pair<None, char const*>, string const&>, char const*>, int const&> >(char const*, int, LogData<pair<pair<pair<pair<None, char const*>, string const&>, char const*>, int const&> > const&)

如果您能想到任何方法来提高此解决方案的性能或可用性,请告诉我。

【问题讨论】:

我不太明白你为什么使用临时字符串流。为什么#define LOG(msg) std::cout &lt;&lt; __FILE__ &lt;&lt; '(' &lt;&lt; __LINE__ &lt;&lt; '(' &lt;&lt; msg &lt;&lt; std::endl 不起作用? flush 的原因是什么(不需要转换为左值)?为什么是static_cast 而不是std::move 如果您想保留&lt;&lt; 运算符但由于某种原因调用函数而不是直接输出它们,您可以使用表达式模板来累积参数:#define LOG(msg) log_impl( my_expression_templ_start() &lt;&lt; msg )(其中my_expression_templ_start 是具有重载的结构 operator&lt;&lt;)。 由于ostringstreamostream,我看不出将&lt;&lt; 递归应用到ostringstream 会如何降低代码复杂度。 我不认为你会从Log_Recursive 得到一个非常有用的__LINE__。见this answer for a hint on how to mix variadic macros with variadic templates;你需要一个宏来在正确的时间扩展__LINE__ 我同意 printf 通常更易于阅读,但与流式传输不同,printf 不是类型安全的,需要将用户定义的类型转换为中间字符串。 【参考方案1】:

这是另一个表达式模板,根据我运行的一些测试,它似乎更加高效。特别是,它通过专门化operator&lt;&lt; 在结果结构中使用char * 成员来避免为不同长度的字符串创建多个函数。添加这种形式的其他特化也应该很容易。

struct None  ;

template <typename First,typename Second>
struct Pair 
  First first;
  Second second;
;

template <typename List>
struct LogData 
  List list;
;

template <typename Begin,typename Value>
LogData<Pair<Begin,const Value &>>
  operator<<(LogData<Begin> begin,const Value &value)

  return begin.list,value;


template <typename Begin,size_t n>
LogData<Pair<Begin,const char *>>
  operator<<(LogData<Begin> begin,const char (&value)[n])

  return begin.list,value;


inline void printList(std::ostream &os,None)




template <typename Begin,typename Last>
void printList(std::ostream &os,const Pair<Begin,Last> &data)

  printList(os,data.first);
  os << data.second;


template <typename List>
void log(const char *file,int line,const LogData<List> &data)

  std::cout << file << " (" << line << "): ";
  printList(std::cout,data.list);
  std::cout << "\n";


#define LOG(x) (log(__FILE__,__LINE__,LogData<None>() << x))

使用 G++ 4.7.2 和 -O2 优化,这将创建一个非常紧凑的指令序列,相当于使用 char * 为字符串文字填充参数的结构。

【讨论】:

这比我原来的解决方案简单得多,导致函数实例化更少,并导致在调用站点生成的指令更少。我将其添加到原始问题的末尾并对其进行了修改以支持流操纵器。【参考方案2】:

我也经历过完全相同的事情。我最终得到了您概述的相同解决方案,它只需要客户端 API 使用逗号而不是插入运算符。它使事情变得相当简单,并且运行良好。强烈推荐。

【讨论】:

我添加了一个替代表达式模板解决方案,允许您以 0 成本使用流式运算符。快来试试吧!

以上是关于使用模板元编程的更好的 LOG() 宏的主要内容,如果未能解决你的问题,请参考以下文章

Item 48:了解模板元编程

C++模板元编程深度解析:探索编译时计算的神奇之旅

模板元编程 - 使用 Enum Hack 和 Static Const 的区别

C++ 模板元编程的最佳介绍? [关闭]

模板元编程如何专注于集合

Item 48:了解模板元编程