Fallback to to_string() when operator<<() fails
Asked Answered
S

4

6

I've seen types that have corresponding to_string() function, but haven't overloaded operator<<(). So, when inserting to stream, one has to << to_string(x) which is verbose. I'm wondering whether it's possible to write a generic function that users operator<<() if supported and falls back to << to_string() if not.

Sinatra answered 22/1, 2016 at 6:56 Comment(0)
M
9

SFINAE is overkill, use ADL.

The trick is to make sure that an operator<< is available, not necessarily the one supplied by the type definition:

namespace helper {
   template<typename T> std::ostream& operator<<(std::ostream& os, T const& t)
   {
      return os << to_string(t);
   }
}
using helper::operator<<;
std::cout << myFoo;

This trick is commonly used in generic code which needs to choose between std::swap<T> and a specialized Foo::swap(Foo::Bar&, Foo::Bar&).

Misvalue answered 22/1, 2016 at 7:53 Comment(10)
I agree this is simpler for the case where you only want operator<< for type T defined in its namespace or else to_string(T) and nothing more, which, admittedly, is what the OP has asked for, so +1. If you need to dispatch further, this won't work. Also, the error messages generated by this solution might not be as helpful as they could be.Humanist
This is nice. But then I have to overload operator<<() for each type that only has overloaded to_string(). I want to avoid such tedious work.Sinatra
@ling what? Why do you think you have to do that?Mitchmitchael
What happens with struct bob{ template<class T>friend ostream& operator<<(ostream&, T&& t ); };? Is there not a risk of ambiguity?Mitchmitchael
@Lingxi: No, you don't have to do that for every type. That's why I made the operator<< a template. It will be instantiated for every type which you print.Misvalue
@Yakk: Sounds like a fair concern, but I'm not sure what exactly you mean form just that snippet. Considering we're talking ADL, the exact namespaces matter. Also, are you trying to print bob objects or others?Misvalue
There is an ambiguity issue. For example, the overload for std::basic_string have std::basic_ostream for the first operand. As a result, it won't be more specialized than the custom version.Sinatra
Here is a live example of what I mean.Sinatra
@Lingxi: Good point. To be precise, that's because there's no operator<< for just std::string. The ambiguity is between two templates, and the idea of my solution is that a template candidate ranks worse than a non-template candidate. There's probably a hack around that, worsening overload candidates is a known technique.Misvalue
I made a revision to your solution (see here). I guess it solves my problem. Thanks for the inspiration.Sinatra
B
2

Try

template <typename T>
void print_out(T t) {
    print_out_impl(std::cout, t, 0);
}

template <typename OS, typename T>
void print_out_impl(OS& o, T t, 
                    typename std::decay<decltype(
                      std::declval<OS&>() << std::declval<T>()
                    )>::type*) {
    o << t;
}

template <typename OS, typename T>
void print_out_impl(OS& o, T t, ...) {
    o << t.to_string();
}

LIVE

Berti answered 22/1, 2016 at 7:40 Comment(0)
H
1

Yes, it is possible.

#include <iostream>
#include <sstream>
#include <string>
#include <type_traits>

struct streamy
{
};

std::ostream&
operator<<(std::ostream& os, const streamy& obj)
{
  return os << "streamy [" << static_cast<const void *>(&obj) << "]";
}

struct stringy
{
};

std::string
to_string(const stringy& obj)
{
  auto oss = std::ostringstream {};
  oss << "stringy [" << static_cast<const void *>(&obj) << "]";
  return oss.str();
}

template <typename T>
std::enable_if_t
<
  std::is_same
  <
    std::string,
    decltype(to_string(std::declval<const T&>()))
  >::value,
  std::ostream
>&
operator<<(std::ostream& os, const T& obj)
{
  return os << to_string(obj);
}

int
main()
{
  std::cout << streamy {} << '\n';
  std::cout << stringy {} << '\n';
}

The generic operator<< will only be available if the expression to_string(obj) is well-typed for obj a const T& and has a result of type std::string. As you have already conjectured in your comment, this is indeed SFINAE at work. If the decltype expression is not well-formed, we will get a substitution failure and the overload will disappear.

However, this will likely get you into troubles with ambiguous overloads. At the very least, put your fallback operator<< into its own namespace and only drag it in locally via a using declaration when needed. I think you will be better off writing a named function that does the same thing.

namespace detail
{

  enum class out_methods { directly, to_string, member_str, not_at_all };

  template <out_methods> struct tag {};

  template <typename T>
  void
  out(std::ostream& os, const T& arg, const tag<out_methods::directly>)
  {
    os << arg;
  }

  template <typename T>
  void
  out(std::ostream& os, const T& arg, const tag<out_methods::to_string>)
  {
    os << to_string(arg);
  }

  template <typename T>
  void
  out(std::ostream& os, const T& arg, const tag<out_methods::member_str>)
  {
    os << arg.str();
  }

  template <typename T>
  void
  out(std::ostream&, const T&, const tag<out_methods::not_at_all>)
  {
    // This function will never be called but we provide it anyway such that
    // we get better error messages.
    throw std::logic_error {};
  }

  template <typename T, typename = void>
  struct can_directly : std::false_type {};

  template <typename T>
  struct can_directly
  <
    T,
    decltype((void) (std::declval<std::ostream&>() << std::declval<const T&>()))
  > : std::true_type {};

  template <typename T, typename = void>
  struct can_to_string : std::false_type {};

  template <typename T>
  struct can_to_string
  <
    T,
    decltype((void) (std::declval<std::ostream&>() << to_string(std::declval<const T&>())))
  > : std::true_type {};

  template <typename T, typename = void>
  struct can_member_str : std::false_type {};

  template <typename T>
  struct can_member_str
  <
    T,
    decltype((void) (std::declval<std::ostream&>() << std::declval<const T&>().str()))
  > : std::true_type {};

  template <typename T>
  constexpr out_methods
  decide_how() noexcept
  {
    if (can_directly<T>::value)
      return out_methods::directly;
    else if (can_to_string<T>::value)
      return out_methods::to_string;
    else if (can_member_str<T>::value)
      return out_methods::member_str;
    else
      return out_methods::not_at_all;
  }

  template <typename T>
  void
  out(std::ostream& os, const T& arg)
  {
    constexpr auto how = decide_how<T>();
    static_assert(how != out_methods::not_at_all, "cannot format type");
    out(os, arg, tag<how> {});
  }

}

template <typename... Ts>
void
out(std::ostream& os, const Ts&... args)
{
  const int dummy[] = {0, ((void) detail::out(os, args), 0)...};
  (void) dummy;
}

Then use it like so.

int
main()
{
  std::ostringstream nl {"\n"};  // has `str` member
  out(std::cout, streamy {}, nl, stringy {}, '\n');
}

The function decide_how gives you full flexibility in deciding how to output a given type, even if there are multiple options available. It is also easy to extend. For example, some types have a str member function instead of an ADL find-able to_string free function. (Actually, I already did that.)

The function detail::out uses tag dispatching to select the appropriate output method.

The can_HOW predicates are implemented using the void_t trick which I find very elegant.

The variadic out function uses the “for each argument” trick, which I find even more elegant.

Note that the code is C++14 and will require an up-to-date compiler.

Humanist answered 22/1, 2016 at 7:13 Comment(4)
Is this SFINAE? So when to_string(x) compiles, the return os << to_string(obj); overload exists and not otherwise? Could I use std::enable_if instead of std::conditional?Sinatra
I think having the << to_string(x) overload if << x does not compile would be nice, if it's possible at all.Sinatra
Yes, this is SFINAE. Please see the updated answer (especially in reply to your second comment). I thought about using std::enable_if but I couldn't find a straight-forward solution so I went with the admittedly somewhat confusing std::conditional.Humanist
Changed code to use std::enable_if_t and require the result is of type std::string.Humanist
S
1

Based on the answer of @MSalters (credits goes to him), this one solves my problem and should make a complete answer.

#include <iostream>
#include <string>
#include <type_traits>

struct foo_t {};

std::string to_string(foo_t) {
  return "foo_t";
}

template <class CharT, class Traits, class T>
typename std::enable_if<std::is_same<CharT, char>::value,
                        std::basic_ostream<CharT, Traits>&>::type 
operator<<(std::basic_ostream<CharT, Traits>& os, const T& x) {
  return os << to_string(x);
}

int main() {
  std::cout << std::string{"123"} << std::endl;
  std::cout << foo_t{} << std::endl;
}
Sinatra answered 22/1, 2016 at 12:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.