当operator<<()失败时,回退到to_string()

时间:2016-01-22 06:56:52

标签: c++ c++11 overloading tostring template-meta-programming

我见过具有相应to_string()功能的类型,但没有重载operator<<()。因此,当插入流时,必须<< to_string(x)这是详细的。我想知道是否有可能编写一个用户operator<<()支持的通用函数,如果不支持则返回<< to_string()

4 个答案:

答案 0 :(得分:9)

SFINAE过度使用,使用ADL。

诀窍是确保一个 operator<<可用,不一定是类型定义提供的那个:

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;

此技巧通常用于通用代码,需要在std::swap<T>和专用Foo::swap(Foo::Bar&, Foo::Bar&)之间进行选择。

答案 1 :(得分:2)

尝试

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

答案 2 :(得分:1)

是的,这是可能的。

#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';
}

通用operator<<仅在表达式to_string(obj)的{​​{1}}和obj类型合适并且结果类型为const T&时才可用。正如您在评论中已经猜到的那样,这确实是SFINAE的工作。如果std::string表达式格式不正确,我们将获得替换失败,并且重载将消失。

但是,这可能会让您在过度模糊的情况下遇到麻烦。至少,将您的后备decltype放入其自己的operator<<中,并在需要时仅通过namespace声明将其拖入本地。我认为你最好写一个做同样事情的命名函数。

using

然后像这样使用它。

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;
}

函数int main() { std::ostringstream nl {"\n"}; // has `str` member out(std::cout, streamy {}, nl, stringy {}, '\n'); } 为您提供了决定如何输出给定类型的完全灵活性,即使有多个可用选项也是如此。它也很容易扩展。例如,某些类型具有decide_how成员函数,而不是ADL可查找str自由函数。 (实际上,我已经这样做了。)

函数to_string使用tag dispatching来选择合适的输出方法。

detail::out谓词是使用我发现非常优雅的void_t trick实现的。

可变can_HOW函数使用“for each argument” trick,我发现它更优雅。

请注意,代码是C ++ 14,需要最新的编译器。

答案 3 :(得分:1)

根据@MSalters的答案(信用证给他),这个解决了我的问题,应该做出完整的答案。

#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;
}