【问题标题】:Simplest implementation of "lightweight type categorization idiom"?“轻量级类型分类习语”的最简单实现?
【发布时间】:2018-10-14 05:00:12
【问题描述】:

我的目标是实现一个谓词来检测嵌套的using 别名(或typedef)的存在,该别名充当轻量级标签以指示类具有某些属性(出于通用编程的目的)。例如,has_my_tag<T> 谓词的行为如下:

struct A {
  using my_tag = void;
};

struct B {};

int main()
{
    static_assert(has_my_tag<A>::value, "");  // evaluate to true if my_tag=void is present
    static_assert(!has_my_tag<B>::value, ""); // false otherwise
}

用户@JoelFalcou 将此称为“轻量级类型分类习语”,并在this answer 中提供了解决方案。我一直找不到该名称的成语的任何参考资料(你知道吗?)这是乔尔对has_my_tag&lt;&gt; 的实现:

template<class T, class R = void>  
struct enable_if_type { typedef R type; };

template<class T, class Enable = void>
struct has_my_tag : std::false_type {};

template<class T>
struct has_my_tag<T, typename enable_if_type<typename T::my_tag>::type> : 
std::true_type
{};

这是编译器资源管理器上的工作版本:https://godbolt.org/z/EEOBb-

我想出了以下简化版:

template<class T, class Enable = void>
struct has_my_tag : std::false_type {};

template<class T>
struct has_my_tag<T, typename T::my_tag> : std::true_type
{};

https://godbolt.org/z/yhkHp7

我的问题:简化版本是否是一种可接受的方式来实现这个成语?有没有会失败的情况?有在 C++11 中工作的更简单的版本吗?我应该更喜欢哪个版本?

据我了解,Joel 的版本将允许 my_tag 为任何类型起别名,而我的版本要求 my_tagvoid 起别名。但是考虑到为轻量级谓词测试标记类型的目标,我不清楚哪个版本是首选。

辅助问题:另外,这个成语还有其他名称吗?它是否用于我可以调查的任何库中?到目前为止,我还没有找到可以显示任何搜索结果的名称。

【问题讨论】:

  • “简化版本是否是一种可接受的方式来实现成语”显然完全取决于它是否正常工作。那是你自己做的决定。它是否正常工作,是否符合您的预期目的。这是您可以简单地为其编写测试用例的东西,如果最终结果是正确的,那么它是“实现这个习语的一种可接受的方式”。你不应该期望任何人告诉你“[你]更喜欢哪个版本”。你可以自己做出决定。最后,至于是否有“其他名称”:不幸的是,我不玩流行语宾果游戏。
  • @SamVarshavchik 使用习语的目的是保持一致并熟悉读者的期望。您的建议似乎表明,只要通过测试,晦涩、陌生和单一的代码是完全可以接受的。我不同意。
  • 为什么要使用最简单的相关性实现?将实现细节放在内部命名空间中,has_my_tag 的用户将永远不必关心它......
  • 您的版本需要更改名称,因为您还要求 my_tagvoid
  • 我称之为检测成语(即使有几种不同的实现(如std::experimental::is_detected))

标签: c++ c++11


【解决方案1】:

对于您的设置,原始版本与您的设置没有区别。两者都使用 SFINAE 来选择正确的 has_my_tag。但是,您的版本确实将您的 typedef/using 限制为 my_tag=void。如果 my_tag 与任何其他类型一样被 typedef,则您的专业化将不匹配,您最终将实例化主模板,如 here 所示。

原因是你在main中实例化模板的地方,static_assert(has_my_tag&lt;A&gt;::value, "");你没有指定第二个参数,所以使用默认(void),即has_my_tag&lt;A,void&gt;::value

您的专业必须与此匹配才能被考虑。

enable_if_type 的用法,(基本上是在 c++17 中完成 void_t 的工作)是在 T 的 ::type 成员上启用 SFINAE,但总是导致无效,这样你的专业化就会当::type 存在时匹配,不管my_tag typedef 的类型。

这让你只关心它是否存在,而不是它的类型;

我个人会使用不依赖于 my_type 被 typedef 为 void 的方法,无论是 enable_if_type 版本,还是类似的东西......

#include <iostream>
#include <type_traits>

struct A {
  using my_tag = void;
};

struct B {};

struct C {
  using my_tag = int; // with void_t also works with my_tag = int
};

struct D {
  struct my_tag{}; //or some struct as the tag
};

// same as your enable_if_type
template <typename...>
using void_t = void;


template<class T, class Enable = void>
struct has_my_tag : std::false_type {};

template<class T>
struct has_my_tag<T, void_t<typename T::my_tag>> : std::true_type
{};


int main() {
    std::cout << has_my_tag<A>::value << std::endl;
    std::cout << has_my_tag<B>::value << std::endl;
    std::cout << has_my_tag<C>::value << std::endl;
    std::cout << has_my_tag<D>::value << std::endl;
    return 0;
}

Demo

【讨论】:

  • 我不得不说,专业化在这里(以及在 OP 中)交互的方式可能不直观。如果我理解正确,模板的实例化将通过false_type 版本的模板签名推断has_my_tag&lt;T, void&gt;,然后将T 替换为两个版本,最后匹配true_type 版本,因为它更专业。我说对了吗?
  • 是的,听起来你做对了。很简单,当实例化 has_my_tag 时,它使用默认值,yielding has_my_tag。默认值仅在实例化时相关。当考虑模板和特化时,可以忽略默认值,剩下的是 (主模板)和特化 显然更专门用于这些类型,因此选择了 - 就像这个 ideone.com/LA6Plr - 很遗憾,实际上如此简单的东西在 c++ 中看起来如此不直观和复杂。
  • 好的,很好!我同意主要的混淆源于false_type 模板签名被“触摸”,即使您最终得到true_type 一个。当然,每个“基本案例 + 专业化”模板都会发生这种情况,但您通常不必考虑了解选择哪个版本。
【解决方案2】:

首先,你的工作,但确实取决于它是void。如果该轻量级标签在某些情况下带有非 void 类型,则它可能会很有用,如果它是非 void 类型,您会默默地检测失败,这似乎很糟糕。

其次,您的类型标记要求您修改类型,这意味着您无法获得内置或您不拥有的类型(如std 中的类型)。我们可以解决这个问题。

namespace type_tag {

  namespace adl {
    template<template<class...>class tag>
    struct tag_token_t {};

    template<class T, template<class...> class tag>
    constexpr
    decltype( (void)(std::declval<tag<T>>()), std::true_type{} )
    tag_test( T*, tag_token_t<tag> ) { return {}; }

    template<class T, template<class...> class tag, class...LowPriority>
    constexpr
    std::enable_if_t<!std::is_same<T,int>{}, std::false_type> tag_test(T*, tag_token_t<tag>, LowPriority&&...) {
      return {};
    }
  }
  template<template<class...>class Z>using tag_token_t = adl::tag_token_t<Z>;

  template<template<class...>class tag>
  constexpr tag_token_t<tag> tag_token{};

  namespace details {
    template<class T, template<class...>class tag>
    constexpr auto test_impl( T*, tag_token_t<tag> ) {
      return tag_test( (T*)nullptr, tag_token<tag> );
    }
  }
  template<class T, template<class>class tag>
  constexpr auto tag_test() {
    return details::test_impl((T*)nullptr, tag_token<tag>);
  }
}

所以现在的标签是这样的:

template<class T>
using my_tag = typename T::my_tag;

我们可以如下测试:

constexpr auto double_has_tag = type_tag::tag_test< double, my_tag >();

如果 double 有标签,则返回编译时 true 或 false。

我们可以通过以下方式确定int 有标签:

namespace type_tag::adl {
  constexpr std::true_type tag_test( int*, type_tag::tag_token_t<my_tag> ) {
    return {};
  }
}

或者,对于我们控制的类型:

struct my_tagged_type {
  using my_tag = void;
};

对于我们可以将名称注入其命名空间的类型(即,不是std 或内置类型),我们可以这样做:

namespace not_my_ns {
  constexpr std::true_type tag_test( not_my_type*, ::tag_test::tag_token_t<::my_tag> ) {
    return {};
  }
}

突然type_tag::tag_test&lt;not_my_ns::not_my_type, ::my_tag&gt;() 是真的。

一旦我们有了tag_test&lt; type, tag_name &gt;(),我们就可以使用通常的std::enable_if,而不是一些自定义系统。

本系统的优点包括:

  1. 它可以在不改变任何你要标记的类型的情况下进行扩展。

  2. 可以使用您的系统使用的using tag=void;using tag=int; 对其进行扩展。

  3. 在 SFINAE 使用点,它只是另一个编译时 bool。所以你现有的 SFINAE 模式可以使用它。

  4. 如果您在结构中为其他人出于不相关原因使用的标签类型选择了一个糟糕的名称,tag_test 可以根据每个类型覆盖它。

缺点是需要一点魔法才能做到这一点。但在常见的用例中,最终用户所需的工作与轻量级系统所需的工作相同。在更复杂的用例中,这可以让您完成轻量级无法做到的事情。

Live example.

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-07
    相关资源
    最近更新 更多