【问题标题】:Why does decltype(declval<T>().func()) work where decltype(&T::func) doesn't?为什么 decltype(declval<T>().func()) 在 decltype(&T::func) 不工作的地方工作?
【发布时间】:2017-12-31 19:10:33
【问题描述】:

我试图检测模板参数中是否存在成员函数baz()

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

template<typename T>
struct ImplementsBaz<T, decltype(&T::baz)> : public std::true_type { };

但它总是产生错误:

struct Foo {};
struct Bar { void baz() {} };

std::cout << ImplementsBaz<Foo>::value << std::endl;  // 0
std::cout << ImplementsBaz<Bar>::value << std::endl;  // also 0

虽然使用declval调用该方法确实有效:

template<typename T>
struct ImplementsBaz<T, decltype(std::declval<T>().baz())> : public std::true_type { };

当然,现在这只能检测到带有 0 个参数的 baz 函数。 为什么使用declval&lt;T&gt;().baz()时正确选择了专业化,而不是decltype(&amp;T::baz)

【问题讨论】:

  • 只是猜测:如果删除通用模板中的默认 = void 会发生什么?
  • 那么ImplementsBaz&lt;Foo&gt;::value是非法的:too few template arguments
  • 啊,是的,我确实忽略了这一点。

标签: c++ c++11 template-specialization sfinae member-functions


【解决方案1】:

这是因为decltype(&amp;T::baz) 是一个错误并且部分特化永远不会被实例化。在T(即Bar)中没有名为baz 的静态成员。

第二个做正确的事,即调用实例上的方法,然后使用它的返回类型。


如果您想检测方法的存在,而不管您传递给它的参数,如果只有一个重载。

template <typename Type, typename = std::enable_if_t<true>>
struct ImplementsBaz : public std::integral_constant<bool, true> {};
template <typename Type>
struct ImplementsBaz<Type, std::enable_if_t<
                         std::is_same<decltype(&T::baz), decltype(&T::baz)>
                             ::value>> 
    : public std::integral_constant<bool, false> {};

如果您想检测该方法是否存在(如果它包含重载),请查看member detection idiom。基本上,它假定存在具有该名称的方法,然后如果存在具有该名称的另一个方法,则特征类会出错并选择正确的 true_type 专业化或类似名称。看看吧!

【讨论】:

  • 我认为您的第一个答案实际上是正确的——看来返回类型确实很重要。 &amp;Bar::baz 应该是一个有效的表达式,那么有没有办法使用 decltype(&amp;T::baz) 来做到这一点?
  • 即使将 baz 设为静态,差异仍然存在。
  • @jwimberley 在这种情况下,表达式计算为一个地址,该地址仍然不是void
  • @jtbandes 不确定是否有其他方法,但使用declval 假装在实例上调用该方法很简单。这就是declval 的用途
  • @Curious 让它参数类型不可知怎么样?使用declval&lt;T&gt;().baz() 需要将参数显式传递给 baz,但我有兴趣检测 baz 方法的存在,而不管参数类型如何。
【解决方案2】:

试试

decltype(&T::baz, void())

decltype(std::declval&lt;T&gt;().baz()) 的示例和

struct Bar { void baz() {} };

之所以有效,是因为baz() 返回void,所以void 匹配非专用Implements_baz 结构中的默认typename = void

但是如果你定义Bar如下

struct Bar { int baz() { return 0; } };

您从Implement_baz 获得false,因为baz() 返回的intvoid 不匹配。

decltype(&amp;T::baz) 的同样问题:与 void 不匹配,因为返回方法的类型。

所以解决方案(嗯......一个可能的解决方案)是使用decltype(&amp;T::baz, void()),因为如果T::baz 存在则返回void(或者如果T::baz 不存在,则失败,并且不返回任何内容)。

【讨论】:

  • 这可能是最好的,除非用户只想匹配具有 void 返回类型的函数 T::baz。
  • @jwimberley - 将是一个不同的问题;在这种情况下,OP 解决方案是一个很好的解决方案,但只有在baz() 没有收到任何参数(或者如果开发人员知道参数的类型)时,这个问题仍然有效。
【解决方案3】:

如果您使用void_t“检测习语”,那么它确实按预期工作:

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

template <typename T>
struct ImplementsBaz<T, void_t<decltype(&T::baz)>> : std::true_type {};


struct Bar { void baz() {} };

static_assert(ImplementsBaz<Bar>::value); // passes

Godbolt link

至于为什么this question 详细解释了“void_t 技巧”的工作原理。引用已接受的答案:

就好像你写了has_member&lt;A, void&gt;::value。现在,将模板参数列表与模板has_member 的任何特化进行比较。只有当没有特化匹配时,主模板的定义才被用作后备。

在原始情况下,decltype(&amp;T::baz) 不是void,因此特化与原始模板不匹配,因此不予考虑。我们需要使用void_t(或其他一些机制,例如强制转换)将类型更改为void,以便使用特化。

【讨论】:

  • 我想我不明白为什么这会导致不使用部分专业化——你知道为什么/知道任何解释它的资源吗?
  • 谢谢,有道理:"就好像你写了has_member&lt;A, void&gt;::value。现在,模板参数列表与模板has_member的任何特化进行比较。只有当没有特化匹配时,主模板的定义用作后备。”
  • @jtbandes 这是一个很好的报价,我已将其纳入答案,希望你不介意:)
【解决方案4】:

另一种可能的解决方案是使用

template<typename T>
struct ImplementsBaz<T, typename std::result_of<decltype(&T::baz)(T)>::type > : public std::true_type { };

或者,如果您更喜欢可读性,

template<typename T>
using BazResult = typename std::result_of<decltype(&T::baz)(T)>::type;

template<typename T>
struct ImplementsBaz<T, BazResult<T> > : public std::true_type { };

这仅在您打算将函数 T::baz 与 void 返回类型匹配时才有效,尽管您的替代工作解决方案也是如此。它也存在只有在没有参数的情况下才能工作的缺陷,所以很遗憾,它与您的第二种解决方案在风格上有所不同。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多