Lepsze makro LOG () przy użyciu metaprogramowania szablonu
Typowe rozwiązanie rejestrowania oparte na makrze LOG () może wyglądać mniej więcej tak:
#define LOG(msg) \
std::cout << __FILE__ << "(" << __LINE__ << "): " << msg << std::endl
Dzięki temu programiści mogą tworzyć bogate w dane wiadomości, korzystając z wygodnych i bezpiecznych dla operatora operatorów strumieniowania:
string file = "blah.txt";
int error = 123;
...
LOG("Read failed: " << file << " (" << error << ")");
// Outputs:
// test.cpp(5): Read failed: blah.txt (123)
Problem polega na tym, że powoduje to, że kompilator wbudowuje wiele wywołań ostream :: operator <<. Zwiększa to generowany kod, a zatem rozmiar funkcji, co, jak podejrzewam, może zaszkodzić wydajności pamięci podręcznej instrukcji i utrudniać kompilatorowi optymalizację kodu.
Oto „prosta” alternatywa, która zastępuje kod wstawiony wywołaniem afunkcja szablonu zmiennego:
*********
ROZWIĄZANIE # 2: FUNKCJA SZABLONU ZMIENNEGO *********
#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;
}
Kompilator automatycznie generuje nowe instancje funkcji szablonu w zależności od liczby, rodzaju i kolejności argumentów komunikatu.
Korzyść wynika z mniejszej liczby instrukcji na każdej stronie połączenia. Minusem jest to, że użytkownik musi przekazać części wiadomości jako parametry funkcji zamiast łączyć je za pomocą operatorów strumieniowania:
LOG("Read failed: ", file, " (", error, ")");
*********
ROZWIĄZANIE # 3: SZABLONY EKSPRESJI *********
Na sugestię @ DyP stworzyłem alternatywne rozwiązanie, które wykorzystujeszablony wyrażeń:
#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);
}
Rozwiązanie szablonów wyrażeń pozwala programiście korzystać ze znanych wygodnych i bezpiecznych dla operatora operatorów strumieniowania:
LOG("Read failed: " << file << " " << error);
Jednak kiedyPart<A, B>
tworzenie jest zaznaczone, nie są wywoływane żadne operator << wywołania, co daje nam korzyści z obu światów: wygodni i bezpieczni operatorzy strumieniowania + mniej instrukcji. ICC13 z -O3 tworzy następujący kod zespołu dla powyższego:
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_
Łącznie jest 19 instrukcji, w tym jedno wywołanie funkcji. Wygląda na to, że każdy dodatkowy argument strumieniowany dodaje 3 instrukcje. Kompilator tworzy inną instancję funkcji Log () w zależności od liczby, rodzaju i kolejności części komunikatów, co wyjaśnia dziwną nazwę funkcji.
*********
ROZWIĄZANIE # 4: SZABLONY EKSPRESJI CATO *********
Oto doskonałe rozwiązanie Cato z ulepszeniem do obsługi manipulatorów strumieniowych (np. 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)
{ }
Jak wskazuje Cato, korzyścią wynikającą z ostatniego rozwiązania jest to, że skutkuje mniejszą liczbą instancji funkcji, ponieważ specjalizacja const char * obsługuje wszystkie literały łańcuchowe. Powoduje to również generowanie mniejszej liczby instrukcji na stronie połączenia:
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&)
Daj mi znać, jeśli możesz pomyśleć o jakimkolwiek sposobie poprawy wydajności lub użyteczności tego rozwiązania.