Une meilleure macro LOG () utilisant la métaprogrammation du modèle

Une solution de journalisation typique basée sur une macro LOG () peut ressembler à ceci:

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

Cela permet aux programmeurs de créer des messages riches en données à l’aide d’opérateurs de diffusion pratiques et sécurisés:

 ssortingng file = "blah.txt"; int error = 123; ... LOG("Read failed: " << file << " (" << error << ")"); // Outputs: // test.cpp(5): Read failed: blah.txt (123) 

Le problème est que cela entraîne le compilateur à plusieurs appels en ligne ostream :: operator <<. Cela augmente le code généré et donc la taille de la fonction, ce qui, je suppose, pourrait nuire aux performances du cache d'instructions et entraver la capacité du compilateur à optimiser le code.

Voici une alternative “simple” qui remplace le code en ligne par un appel à une fonction de modèle variadique :

********* SOLUTION N ° 2: FONCTION VARIADIC TEMPLATE *********

 #define LOG(...) LogWrapper(__FILE__, __LINE__, __VA_ARGS__) // Log_Recursive wrapper that creates the ossortingngstream template void LogWrapper(const char* file, int line, const Args&... args) { std::ossortingngstream msg; Log_Recursive(file, line, msg, args...); } // "Recursive" variadic function template void Log_Recursive(const char* file, int line, std::ossortingngstream& 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; } 

Le compilateur génère automatiquement de nouvelles instanciations de la fonction de modèle selon les besoins, en fonction du nombre, du type et de l’ordre des arguments du message.

L’avantage est qu’il y a moins d’instructions sur chaque site d’appel. L’inconvénient est que l’utilisateur doit transmettre les parties du message en tant que parameters de fonction au lieu de les combiner à l’aide d’opérateurs de diffusion en continu:

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

********* SOLUTION N ° 3: MODÈLES D’EXPRESSION *********

À la suggestion de @ DyP, j’ai créé une solution alternative qui utilise des modèles d’expression :

 #define LOG(msg) Log(__FILE__, __LINE__, Part() << msg) template struct PartTrait { typedef T Type; }; // Workaround GCC 4.7.2 not recognizing noinline atsortingbute #ifndef NOINLINE_ATTRIBUTE #ifdef __ICC #define NOINLINE_ATTRIBUTE __atsortingbute__(( 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 void Log(const char* file, int line, const T& msg) NOINLINE_ATTRIBUTE { std::cout << file << ":" << line << ": " << msg << std::endl; } template struct Part : public PartTrait<Part> { Part() : value(nullptr), prev(nullptr) { } Part(const Part&) = default; Part operator=( const Part&) = 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 struct Part : public PartTrait<Part> { Part() : pfn(nullptr), prev(nullptr) { } Part(const Part& that) = default; Part operator=(const Part&) = 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 std::enable_if< std::is_base_of<PartTrait, TPreviousPart>::value, Part >::type operator<<(const TPreviousPart& prev, const T& value) { return Part(value, prev); } template typename std::enable_if< std::is_base_of<PartTrait, TPreviousPart>::value, Part >::type operator<<(const TPreviousPart& prev, PfnManipulator value) { return Part(value, prev); } template typename std::enable_if< std::is_base_of<PartTrait, TPart>::value, std::ostream&>::type operator<<(std::ostream& os, const TPart& part) { return part.output(os); } 

La solution de modèles d’expression permet au programmeur d’utiliser les opérateurs de streaming familiers, pratiques et sécurisés:

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

Cependant, lorsque la création de la Part est en ligne, aucun opérateur << n'est appelé, ce qui nous confère l'avantage des deux mondes: opérateurs de streaming pratiques et sécurisés avec moins d'instructions. ICC13 avec -O3 génère le code d'assemblage suivant:

 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_ 

Le total est de 19 instructions, dont un appel de fonction. Il semble que chaque argument supplémentaire diffusé ajoute 3 instructions. Le compilateur crée une instanciation de la fonction Log () différente en fonction du nombre, du type et de l’ordre des parties du message, ce qui explique le nom bizarre de la fonction.

********* SOLUTION N ° 4: GABARITS D’EXPRESSION DE CATO *********

Voici l’excellente solution de Cato avec un ajustement pour supporter les manipulateurs de stream (par exemple, endl):

 #define LOG(msg) (Log(__FILE__, __LINE__, LogData() << 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 void Log(const char* file, int line, LogData&& data) NOINLINE_ATTRIBUTE { std::cout << file << ":" << line << ": "; output(std::cout, std::move(data.list)); std::cout << std::endl; } struct None { }; template struct LogData { List list; }; template constexpr LogData<std::pair> operator<<(LogData&& begin, Value&& value) noexcept { return {{ std::forward(begin.list), std::forward(value) }}; } template constexpr LogData<std::pair> operator<<(LogData&& begin, const char (&value)[n]) noexcept { return {{ std::forward(begin.list), value }}; } typedef std::ostream& (*PfnManipulator)(std::ostream&); template constexpr LogData<std::pair> operator<<(LogData&& begin, PfnManipulator value) noexcept { return {{ std::forward(begin.list), value }}; } template  void output(std::ostream& os, std::pair&& data) { output(os, std::move(data.first)); os << data.second; } inline void output(std::ostream& os, None) { } 

Comme le souligne Cato, l’avantage par rapport à la dernière solution est qu’elle entraîne moins d’instanciations de fonctions puisque la spécialisation const char * gère tous les littéraux de chaîne. En outre, moins d’instructions sont générées sur le site de l’appel:

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

Faites-moi savoir si vous pouvez imaginer un moyen d’améliorer les performances ou la convivialité de cette solution.

Voici un autre modèle d’expression qui semble être encore plus efficace sur la base de certains tests que j’ai effectués. En particulier, cela évite de créer plusieurs fonctions pour des chaînes de longueurs différentes en spécialisant l’ operator<< pour utiliser un membre char * dans la structure résultante. Il devrait également être facile d’append d’autres spécialisations de ce type.

 struct None { }; template  struct Pair { First first; Second second; }; template  struct LogData { List list; }; template  LogData> operator<<(LogData begin,const Value &value) { return {{begin.list,value}}; } template  LogData> operator<<(LogData begin,const char (&value)[n]) { return {{begin.list,value}}; } inline void printList(std::ostream &os,None) { } template  void printList(std::ostream &os,const Pair &data) { printList(os,data.first); os << data.second; } template  void log(const char *file,int line,const LogData &data) { std::cout << file << " (" << line << "): "; printList(std::cout,data.list); std::cout << "\n"; } #define LOG(x) (log(__FILE__,__LINE__,LogData() << x)) 

Avec G ++ 4.7.2, avec optimisation -O2, cela crée une séquence d'instructions très compacte, équivalente au remplissage d'une structure avec les parameters à l'aide d'un caractère char * pour les littéraux de chaîne.

J’ai vécu exactement la même chose. Et je me suis retrouvé avec la même solution que celle que vous avez décrite, qui exige simplement que l’API cliente utilise une virgule au lieu de l’opérateur d’insertion. Cela simplifie les choses et fonctionne assez bien. Hautement recommandé.