Overload on ostream in a variadic template function
-
13-11-2019 - |
Question
I have a variadic function that I want to overload on the first parameter type.
void write( void ) { }
void write( std::ostream& ) { }
template< typename Head, typename... Rest >
void write( std::ostream& out, Head&& head, Rest&&... rest )
{
out << head;
write( out, std::forward<Rest>(rest)... );
}
template< typename... Args >
void write( Args&&... args )
{
write( std::cout, std::forward<Args>(args)... );
}
But these functions don't behave as expected.
write( "## to cout ##" ); // printed to stdout as expected
write( std::cerr, "## to cerr ##" ); // printed to stderr as expected
std::ostringstream oss;
write( oss, "## to string ##" ); // error here
// '0x7fff9db8## to string ##' is printed to stdout!
What's going on here?
Why isn't overload resolution picking the function I want?
Is there a way to do this without lots of metaprogramming? (I was able to work around it with std::is_convertible
but the solution was much larger than the simple code I've shown above).
Solution
That's because ostringstream
needs a base conversion to ostream
when you pass it to the other template, while it doesn't need any conversion when you pass it to the template that forwards to write(std::cout, ...)
. So if you pass ostringstream
, it selects the more generic template which forwards the ostringstream as an argument to output to the more specific template. Outputting the ostringstream
converts it to a void*
, which is then printed.
You can solve this with is_base_of
(just feels better to me than using is_convertible
).
template<typename Arg, typename... Args, typename =
typename std::enable_if<
!std::is_base_of<
std::ostream,
typename std::remove_reference<Arg>::type,
>::value>::type
>
void write(Arg&& arg, Args&&... args )
{
write( std::cout, std::forward<Arg>(arg), std::forward<Args>(args)... );
}
I personally dislike using too much SFINAE in my code, because I can't cope with a certain level of angle brackets. So I like to use overloading
template< typename Arg, typename... Args >
void write_dispatch( std::true_type, Arg&& arg, Args&&... args )
{
std::ostream& os = arg;
write( os, std::forward<Args>(args)... );
}
template< typename Arg, typename... Args >
void write_dispatch( std::false_type, Arg&& arg, Args&&... args )
{
write( std::cout, std::forward<Arg>(arg), std::forward<Args>(args)... );
}
template< typename Arg, typename... Args >
void write( Arg&& arg, Args&&... args )
{
typedef typename std::remove_reference<Arg>::type nonref_type;
write_dispatch( std::is_base_of<std::ostream, nonref_type>(),
std::forward<Arg>(arg), std::forward<Args>(args)... );
}
This way, if you call it with something other than lvalue of ostream
as first argument, it will call write_dispatch
, which will transform the call into such an lvalue of ostream
, so that your other write
template can continue.
On a final note, you should say out << std::forward<Head>(head)
, otherwise all your work to use std::forward
in previous recursion steps would be for naught, because in the end you would output everything as lvalues anyway.