当 operator<<() 失败时回退到 to_string()
Fallback to to_string() when operator<<() fails
我见过具有相应 to_string()
功能但未重载 operator<<()
的类型。因此,当插入到流中时,必须 << to_string(x)
这是冗长的。我想知道是否可以编写一个通用函数,如果支持则用户 operator<<()
,如果不支持则回退到 << to_string()
。
是的,有可能。
#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)
是 well-typed for obj
a const T&
且结果类型为 std::string
。正如您已经在评论中推测的那样,这确实是 SFINAE 在起作用。如果 decltype
表达式不是 well-formed,我们将得到一个替换失败并且重载将消失。
但是,这可能会让您陷入模棱两可的重载的麻烦。至少,将后备 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
让您可以完全灵活地决定如何输出给定类型,即使有多个选项可用。它也很容易扩展。例如,某些类型具有 str
成员函数而不是 ADL find-able to_string
自由函数。 (实际上,我已经这样做了。)
函数detail::out
使用tag dispatching到select适当的输出方法。
can_HOW
谓词是使用 void_t
trick 实现的,我觉得它非常优雅。
可变参数 out
函数使用 “for each argument” trick,我觉得它更优雅。
请注意,代码是 C++14,需要 up-to-date 编译器。
尝试
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();
}
SFINAE 有点矫枉过正,使用 ADL。
诀窍是确保 an 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&)
之间进行选择的通用代码。
根据@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;
}
我见过具有相应 to_string()
功能但未重载 operator<<()
的类型。因此,当插入到流中时,必须 << to_string(x)
这是冗长的。我想知道是否可以编写一个通用函数,如果支持则用户 operator<<()
,如果不支持则回退到 << to_string()
。
是的,有可能。
#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)
是 well-typed for obj
a const T&
且结果类型为 std::string
。正如您已经在评论中推测的那样,这确实是 SFINAE 在起作用。如果 decltype
表达式不是 well-formed,我们将得到一个替换失败并且重载将消失。
但是,这可能会让您陷入模棱两可的重载的麻烦。至少,将后备 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
让您可以完全灵活地决定如何输出给定类型,即使有多个选项可用。它也很容易扩展。例如,某些类型具有 str
成员函数而不是 ADL find-able to_string
自由函数。 (实际上,我已经这样做了。)
函数detail::out
使用tag dispatching到select适当的输出方法。
can_HOW
谓词是使用 void_t
trick 实现的,我觉得它非常优雅。
可变参数 out
函数使用 “for each argument” trick,我觉得它更优雅。
请注意,代码是 C++14,需要 up-to-date 编译器。
尝试
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();
}
SFINAE 有点矫枉过正,使用 ADL。
诀窍是确保 an 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&)
之间进行选择的通用代码。
根据@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;
}