Functions
-
int64_t operator<< (std::ostream & , short value)MakeCheckOpString is being specialized for every T and U pair that is being
passed to the CHECK_op macros. However, there is a lot of redundancy in these
specializations that creates unnecessary library and binary bloat.
The number of instantiations tends to be O(n^2) because we have two
independent inputs. This technique works by reducing `n`.
Most user-defined types being passed to CHECK_op end up being printed as a
builtin type. For example, enums tend to be implicitly converted to its
underlying type when calling operator
<
<
, and pointers are printed with the
`const void*` overload.
To reduce the number of instantiations we coerce these values before calling
MakeCheckOpString instead of inside it.
To detect if this coercion is needed, we duplicate all the relevant
operator
<
<
overloads as specified in the standard, just in a different
namespace. If the call to `stream
<
<
value` becomes ambiguous, it means that
one of these overloads is the one selected by overload resolution. We then
do overload resolution again just with our overload set to see which one gets
selected. That tells us which type to coerce to.
If the augmented call was not ambiguous, it means that none of these were
selected and we can't coerce the input.
As a secondary step to reduce code duplication, we promote integral types to
their 64-bit variant. This does not change the printed value, but reduces the
number of instantiations even further. Promoting an integer is very cheap at
the call site.
-
int64_t operator<< (std::ostream & , unsigned short value) -
int64_t operator<< (std::ostream & , int value) -
int64_t operator<< (std::ostream & , unsigned int value) -
int64_t operator<< (std::ostream & , long value) -
uint64_t operator<< (std::ostream & , unsigned long value) -
int64_t operator<< (std::ostream & , long long value) -
uint64_t operator<< (std::ostream & , unsigned long long value) -
float operator<< (std::ostream & , float value) -
double operator<< (std::ostream & , double value) -
long double operator<< (std::ostream & , long double value) -
bool operator<< (std::ostream & , bool value) -
const void * operator<< (std::ostream & , const void * value) -
const void * operator<< (std::ostream & , std::nullptr_t ) -
template <typename Traits>char operator<< (std::basic_ostream<char, Traits> & , char )These `char` overloads are specified like this in the standard, so we have to
write them exactly the same to ensure the call is ambiguous.
If we wrote it in a different way (eg taking std::ostream instead of the
template) then one call might have a higher rank than the other and it would
not be ambiguous.
-
template <typename Traits>signed char operator<< (std::basic_ostream<char, Traits> & , signed char ) -
template <typename Traits>unsigned char operator<< (std::basic_ostream<char, Traits> & , unsigned char ) -
template <typename Traits>const char * operator<< (std::basic_ostream<char, Traits> & , const char * ) -
template <typename Traits>const signed char * operator<< (std::basic_ostream<char, Traits> & , const signed char * ) -
template <typename Traits>const unsigned char * operator<< (std::basic_ostream<char, Traits> & , const unsigned char * ) -
template <typename T, typename = decltype(std::declval<std::ostream&>() << std::declval<const T&>())>const T & Detect (int )This overload triggers when the call is not ambiguous.
It means that T is being printed with some overload not on this list.
We keep the value as `const T
&
`.
-
template <typename T>decltype(detect_specialization::operator<<(std::declval<std::ostream &>(), std::declval<const T &>())) Detect (char )This overload triggers when the call is ambiguous.
It means that T is either one from this list or printed as one from this
list. Eg an enum that decays to `int` for printing.
We ask the overload set to give us the type we want to convert it to.