【问题标题】:SFINAE : Know if a function already exist or noSFINAE : 知道一个函数是否已经存在
【发布时间】:2017-07-13 16:30:46
【问题描述】:

基本上,我想写这样的代码:

std::vector<float> a = { 54, 25, 32.5 };
std::vector<int> b = { 55, 65, 6 };

std::cout << a << b << std::string("lol");

这是不可能的,因为operator&lt;&lt;(ostream&amp;, vector)没有过载

所以,我写了一个函数来完成这项工作:

template<template<typename...> typename T, typename ...Args>
std::enable_if_t<is_iterable_v<T<Args...>>>, std::ostream> &operator<<(std::ostream &out, T<Args...> const &t) {
    for (auto const &e : t)
        out << e << " ";
    out << std::endl;
    return out;
}

这很好用,但我的字符串有问题。因为字符串是可迭代的,而且字符串有operator&lt;&lt;函数。

所以我用!is_streamable_out &amp;&amp; _is_iterable 之类的另一个特征进行了测试:std::declval&lt;std::ostream&amp;&gt;() &lt;&lt; std::declval&lt;T&gt;() 以及它是否具有开始/结束功能。它在 MSVC 上运行良好,但在 Clang 上运行良好(我认为这是因为编译器也使用了我刚刚创建的函数,所以它找到了一个可用于所有方法的重载)。

所以,我目前正在使用!is_same_v&lt;string, T&gt;,但恕我直言,它并不完美。

有没有办法在不重新声明函数的情况下知道函数是否存在?

基本上,我想做这样的事情

if function foo does not exist for this type.
then function foo for this type is ...

这与Is it possible to write a template to check for a function's existence? 的问题不同,因为在另一个线程中,函数并不完全相同(toString 与 toOptionalString)。就我而言,功能是相同的。

这是我的完整代码:

template <class...>
struct make_void { using type = void; };

template <typename... T>
using void_t = typename make_void<T...>::type; // force SFINAE

namespace detail {
    template<typename AlwaysVoid, template<typename...> typename Operator, typename ...Args>
    struct _is_valid : std::false_type {};


    template<template<typename...> typename Operator, typename ...Args>
    struct _is_valid<void_t<Operator<Args...>>, Operator, Args...> : std::true_type { using type = Operator<Args...>; };
}

template<template<typename ...> typename Operator, typename ...Args>
using is_valid = detail::_is_valid<void, Operator, Args...>;

#define HAS_MEMBER(name, ...)\
template<typename T>\
using _has_##name = decltype(std::declval<T>().name(__VA_ARGS__));\
\
template<typename T>\
using has_##name = is_valid<_has_push_back, T>;\
\
template<typename T>\
constexpr bool has_##name##_v = has_##name<T>::value

HAS_MEMBER(push_back, std::declval<typename T::value_type>());
HAS_MEMBER(begin);
HAS_MEMBER(end);

template<typename T>
using is_iterable = std::conditional_t<has_begin_v<T> && has_end_v<T>, std::true_type, std::false_type>;

template<typename T>
constexpr bool is_iterable_v = is_iterable<T>::value;



template<class T, class Stream, class = void>
struct can_print : std::false_type {};

template<class T, class Stream>
struct can_print<T, Stream, void_t<decltype(std::declval<Stream&>() << std::declval<const T&>())>> : std::true_type {};

template<class T, class Stream = std::ostream>
constexpr bool can_print_v = can_print<T, Stream>::value;

template<typename T>
std::enable_if_t<is_iterable_v<T> && !can_print_v<T>, std::ostream> &operator<<(std::ostream &out, T const &t) {
    for (auto const &e : t)
        out << e << " ";
    out << std::endl;
    return out;
}

template<typename A, typename B>
std::ostream &operator<<(std::ostream &out, std::pair<A, B> const &p) {
    out << p.first << " " << p.second << std::endl;
    return out;
}

template<typename T>
std::enable_if_t<has_push_back_v<T>, T> &operator<<(T &c, typename T::value_type const &e) {
    c.push_back(e);
    return c;
}

还有主要的:

#include <iostream>
#include <vector>
#include "Tools/stream.h"
#include <string>
#include <map>

int main() {
    std::vector<float> a = { 54, 25, 32.5 };
    std::vector<int> b = { 55, 65, 6 };

    std::cout << a;

    std::cout << has_push_back<float>::value  << " " << has_push_back<std::vector<float>>::value << std::endl;
    std::cout << is_iterable<std::vector<float>>::value << " " << is_iterable<float>::value << std::endl;

    getchar();
    return 0;
}

【问题讨论】:

  • std::enable_if_t 直到 C++14 才存在,那么你的代码真的是 C++11 吗?
  • 可以用C++11来实现,所以也可以用C++11来回答这个问题。但是,是的,代码是 C++14(甚至是带有 Clang 的 C++1z)
  • 与您的直接问题无关,但您为什么要打扰...Args?你的函数可以作为一个简单的template&lt;typename T&gt;
  • @Antoine Morrier 但template&lt;typename T&gt; 匹配 any 类型,无论是否模板化:pastebin.com/jqYBsvRK

标签: c++ c++11 sfinae


【解决方案1】:

How to avoid this sentence is false in a template SFINAE? 提供了解决您问题的答案——重载&lt;&lt;(ostream&amp;, Ts...),它的优先级低于任何其他&lt;&lt; 重载。

同时我想说你的计划很糟糕。重载 std 类型的运算符是一个糟糕的计划,原因有两个。

首先,除非有充分的理由,否则您不应该为不属于您的类型重载运算符。

其次,如果你这样做,你应该在类型的命名空间中进行,并且你不能将你的&lt;&lt;注入namespace std而不会使你的程序格式错误。

在与相关类型不同的命名空间中重载的运算符只能在您进行重载的命名空间中找到。在与参数相关的命名空间中重载的运算符可以在任何地方找到。

这导致脆弱的&lt;&lt; 只能在一个命名空间中工作。


所以,改为这样做:

struct print_nothing_t {};
inline std::ostream& operator<<(std::ostream& os, print_nothing_t) {
  return os;
}
template<class C, class Sep=print_nothing_t>
struct stream_range_t {
  C&& c;
  Sep s;
  template<class T=print_nothing_t>
  stream_range_t( C&& cin, T&& sin = {} ):
    c(std::forward<C>(cin)),
    s(std::forward<T>(sin))
  {}
  friend std::ostream& operator<<(std::ostream& os, stream_range_t&& self) {
    bool first = true;
    for (auto&& x:self.c) {
      if (!first) os << self.s;
      os << decltype(x)(x);
      first = false;
    }
    return os;
  }
};
template<class C, class Sep = print_nothing_t>
stream_range_t<C, Sep> stream_range( C&& c, Sep&& s={} ) {
  return {std::forward<C>(c), std::forward<Sep>(s)};
}

现在,如果您希望嵌套向量起作用,您必须实现一个流范围,将适配器应用于其内容。

Live example 使用此测试代码:

std::vector<int> v{1,2,3,4};
std::cout << stream_range(v, ',') << "\n";

输出是1,2,3,4


如何使这个递归:

我们可以编写一个adapt_for_streaming 函数,将其分派到identity(用于已经可流式传输的事物)和stream_range 用于可迭代但尚未可流式传输的事物。

然后用户可以为其他类型添加新的adapt_for_streaming 重载。

stream_range_t 然后在其内容上使用adapt_for_streaming 进行流式传输。

接下来,我们可以将元组/对/数组重载添加到adapt_for_streaming,然后突然std::vector&lt; std::vector&lt; std::tuple&lt;std::string, int&gt; &gt; &gt; 可以流式传输。

最终用户可以直接调用stream_rangeadapt_for_streaming 来对可迭代容器进行字符串处理。您甚至可以直接在std::string 上调用stream_range,将其视为char 的可流式集合而不是字符串。

【讨论】:

  • 我越想我的答案,“这句话是假的”就让我大吃一惊。我认为 OP 想要漂亮的 stream &lt;&lt; T 语法,但我们无法通过 SFINAE 获得它,而不会在这种真/假情况下结束。
【解决方案2】:

您可以编写一个小的检测习语来测试表达式 stream &lt;&lt; value 是否格式正确*。

这里使用std::ostream:

template<class...>
using void_t = void;

template<class T, class Stream, class=void>
struct can_print : std::false_type{};

template<class T, class Stream>
struct can_print<T, Stream, void_t<decltype(std::declval<Stream&>() << std::declval<const T&>())>> : std::true_type{};

template<class T, class Stream=std::ostream>
constexpr bool can_print_v = can_print<T, Stream>::value;

现在您可以像这样为operator&lt;&lt; 编写重载代码:

template<template<class...> class C, class...T>
std::enable_if_t<!can_print_v<C<T...>>, std::ostream>& operator<<(std::ostream &out, C<T...> const &t) {
    for (auto const &e : t)
        out << e << " ";
    out << std::endl;
    return out;
}

还有一个测试

std::vector<float> a = { 54, 25, 32.5 };
std::vector<int> b = { 55, 65, 6 };
std::cout << a;
std::cout << b;
std::cout << std::string("lol") << std::endl;

Demo


Yakk 在their answer 中指出了一件有趣的事情。 This sentence is false

基本上,通过使用!can_print_v 启用operator&lt;&lt; 的重载,can_print_v 随后应该是true,但它是false,因为模板的第一次实例化导致结构派生自@987654335 @。因此,can_print_v 的后续测试无效。

我将这个答案作为一个警示故事。特征本身是可以的,但是将其用于 SFINAE 会使该特征无效的东西是不行的。

*看来 OP 已将此代码复制到他们自己的代码库中,然后修改了问题以包含它,以防您想知道为什么它看起来相反

【讨论】:

  • 这就是我在问题中提到的“is_streamable out”所尝试的,它在 Clang 上不起作用。仅在 MSVC 上。
  • 问题是当can_print_v()为false时,使用can_print_v()来启用operator&lt;&lt;()的定义;使用类似的解决方案,我的编译器会进入循环。
  • @max66:更新了更完整的示例。
  • @AntoineMorrier:你能详细说明一下吗?我在演示中使用了 clang。
  • 我正在使用与您相同或几乎相同的解决方案:std::enable_if_t&lt;is_iterable_v&lt;T&gt; &amp;&amp; !can_print_v&lt;T&gt;, std::ostream&gt; &amp;operator&lt;&lt;(std::ostream &amp;out, T const &amp;t)。当我尝试读取 std::string 时,现在没有问题了。但是当我使用 std::vector 时,有一个。因为 clang 似乎找到了即使它还没有构建的功能。
猜你喜欢
  • 2016-09-23
  • 1970-01-01
  • 1970-01-01
  • 2021-12-02
  • 2023-03-30
  • 2013-08-24
  • 1970-01-01
  • 2016-07-29
  • 1970-01-01
相关资源
最近更新 更多