Part 1 Part 2 Part 3 Part 4 Part 5 Postscript
Callable things. There are several types:
- functions
- member functions
std::function
s
- bind expressions
- lambdas
- objects that support
operator()
(function objects)
So, going back to my tag code, so far (with everything I’ve added) and including callable things, it will look like:
template <typename T>
using stringifier_tag = std::conditional_t<
is_outputtable<T>::value,
is_outputtable_tag,
std::conditional_t<
is_callable<T>::value,
is_callable_tag,
std::conditional_t<
is_iterable<T>::value,
is_iterable_tag,
std::conditional_t<
is_pair<T>::value,
is_pair_tag,
std::conditional_t<
is_tuple<T>::value,
is_tuple_tag,
void>>>>>; |
template <typename T>
using stringifier_tag = std::conditional_t<
is_outputtable<T>::value,
is_outputtable_tag,
std::conditional_t<
is_callable<T>::value,
is_callable_tag,
std::conditional_t<
is_iterable<T>::value,
is_iterable_tag,
std::conditional_t<
is_pair<T>::value,
is_pair_tag,
std::conditional_t<
is_tuple<T>::value,
is_tuple_tag,
void>>>>>;
This is getting to be a large nested “if-statement”, but it’s easy to follow, and the compiler doesn’t mind, so I don’t. Basically, this is the preferential order I want to use for outputting things. But then I discovered something puzzling, that took me a while to figure out. (In my defence, I discovered it late at night when I was probably not too sharp!)
// this outputs "1"!
cout << [](){} << endl; |
// this outputs "1"!
cout << [](){} << endl;
Lambdas (and in fact, functions) can be passed to operator<<
– which means they’ll get is_outputtable_tag
and produce 1 when printed. Not good. I want to print “<callable (function)>” or “<callable (function object)>”. (I’m OK with lambdas and function objects coinciding here.) So why does printing a plain lambda work at all? Well, the answer (which took me too long to see, and doubtless you, learned reader, have already seen) is that a non-capturing lambda has an implicit conversion to a function pointer. And that, like all pointers, has an implicit conversion to bool
. Anything non-zero (like a perfectly good pointer) is true
, and when you print true
(without using boolalpha
), you get 1.
Hm. Think think think.
So, I think there is going to be a compromise here. And that compromise is going to be triggered if someone deliberately writes a function object with a conversion to bool
and supporting operator<<
. Because lambdas have operator()
and a conversion to bool
, and I don’t want to use operator<<
on them. So, it’s void_t
to the rescue again, and by now I have macroed the detection code.
#define SFINAE_DETECT(name, expr) \
template <typename T> \
using name##_t = decltype(expr); \
template <typename T, typename = void> \
struct has_##name : public std::false_type {}; \
template <typename T> \
struct has_##name<T, void_t<name##_t<T>>> \
: public std::true_type {}; |
#define SFINAE_DETECT(name, expr) \
template <typename T> \
using name##_t = decltype(expr); \
template <typename T, typename = void> \
struct has_##name : public std::false_type {}; \
template <typename T> \
struct has_##name<T, void_t<name##_t<T>>> \
: public std::true_type {};
I want to know if something’s implicitly convertible to bool
:
void bool_conversion_test(bool);
SFINAE_DETECT(bool_conversion,
bool_conversion_test(std::declval<T>())) |
void bool_conversion_test(bool);
SFINAE_DETECT(bool_conversion,
bool_conversion_test(std::declval<T>()))
I don’t need to implement anything for bool_conversion_test
because it’s never called, just used to check well-formedness of conversion. I also want to know if something has a function call operator:
SFINAE_DETECT(call_operator, &T::operator()) |
SFINAE_DETECT(call_operator, &T::operator())
And now I can massage is_outputtable
appropriately: something that has operator<<
but is not a function and is not a function object convertible to bool
:
template<typename T>
using is_outputtable = typename std::conditional<
has_operator_output<T>::value &&
!std::is_function<T>::value &&
!(has_call_operator<T>::value && has_bool_conversion<T>::value),
std::true_type, std::false_type>::type; |
template<typename T>
using is_outputtable = typename std::conditional<
has_operator_output<T>::value &&
!std::is_function<T>::value &&
!(has_call_operator<T>::value && has_bool_conversion<T>::value),
std::true_type, std::false_type>::type;
Now that I’ve eliminated the callable types from the outputtable types, the detection of a callable type is fairly straightforward, between what the STL gives me and what is easy to make myself:
template <typename T>
struct is_std_function : public std::false_type {};
template <typename T>
struct is_std_function<std::function<T>> : public std::true_type {};
template<typename T>
using is_callable = typename std::conditional<
has_call_operator<T>::value
|| is_std_function<T>::value
|| std::is_function<T>::value
|| std::is_bind_expression<T>::value,
std::true_type, std::false_type>::type; |
template <typename T>
struct is_std_function : public std::false_type {};
template <typename T>
struct is_std_function<std::function<T>> : public std::true_type {};
template<typename T>
using is_callable = typename std::conditional<
has_call_operator<T>::value
|| is_std_function<T>::value
|| std::is_function<T>::value
|| std::is_bind_expression<T>::value,
std::true_type, std::false_type>::type;
And now all that’s left is to distinguish between the callable types themselves for the purposes of printing. This can be done with judicious use of std::enable_if
.
template <typename T>
constexpr static std::enable_if_t<is_std_function<T>::value,
const char*>
callable_type() { return "(std::function)"; }
template <typename T>
constexpr static std::enable_if_t<std::is_function<T>::value,
const char*>
callable_type() { return "(function)"; }
template <typename T>
constexpr static
std::enable_if_t<std::is_bind_expression<T>::value,
const char*>
callable_type() { return "(bind expression)"; }
template <typename T>
constexpr static
std::enable_if_t<!is_std_function<T>::value &&
has_call_operator<T>::value,
const char*>
callable_type() { return "(function object)"; } |
template <typename T>
constexpr static std::enable_if_t<is_std_function<T>::value,
const char*>
callable_type() { return "(std::function)"; }
template <typename T>
constexpr static std::enable_if_t<std::is_function<T>::value,
const char*>
callable_type() { return "(function)"; }
template <typename T>
constexpr static
std::enable_if_t<std::is_bind_expression<T>::value,
const char*>
callable_type() { return "(bind expression)"; }
template <typename T>
constexpr static
std::enable_if_t<!is_std_function<T>::value &&
has_call_operator<T>::value,
const char*>
callable_type() { return "(function object)"; }
Finally, I am ready to write the actual printing code for callable types.
template <typename T>
struct stringifier_select<T, is_callable_tag>
{
explicit stringifier_select(T) {}
std::ostream& output(std::ostream& s) const
{
return s << "<callable "
<< callable_type<T>()
<< '>';
}
}; |
template <typename T>
struct stringifier_select<T, is_callable_tag>
{
explicit stringifier_select(T) {}
std::ostream& output(std::ostream& s) const
{
return s << "<callable "
<< callable_type<T>()
<< '>';
}
};
Phew!
Recap: so far, we can print:
- things that have
operator<<
- containers
pair
and tuple
- callable things
- and by default, things that just say “unknown”
Next, what to do about strings and arrays?