可靠的方式来为类型特征订购多个std::void_t部分特化

huangapple go评论64阅读模式
英文:

Reliable way of ordering mulitple std::void_t partial specializations for type traits

问题

I want to classify the "level of feature support" for a type.
The type can contain aliases red and blue. It can contain just one, both, or neither, and I want to classify each of these cases with an enum class:

enum class holder_type {
    none,
    red,
    blue,
    both
};

This is a use-case for the "member detector idiom", and I've tried to implement it using std::void_t:

// primary template
template <class T, class = void, class = void>
struct holder_type_of
    : std::integral_constant<holder_type, holder_type::none> {};

// substitution failure if no alias T::red exists
template <class T, class Void>
struct holder_type_of<T, std::void_t<typename T::red>, Void>
    : std::integral_constant<holder_type, holder_type::red> {};

// substitution failure if no alias T::blue exists
template <class T, class Void>
struct holder_type_of<T, std::void_t<typename T::blue>, Void>
    : std::integral_constant<holder_type, holder_type::blue> {};

// substitution failure if one of the aliases doesn't exist
template <class T>
struct holder_type_of<T, std::void_t<typename T::blue>, std::void_t<typename T::red>>
    : std::integral_constant<holder_type, holder_type::both> {};

However, the first and second partial specialization are redefinitions of each other. The following assertions fail to compile with clang, but succeed with GCC (as wanted).

static_assert(holder_type_of<red_holder>::value == holder_type::red);
static_assert(holder_type_of<blue_holder>::value == holder_type::blue);
<source>:36:8: error: redefinition of 'holder_type_of<T, std::void_t<typename T::blue>, Void>'
struct holder_type_of<T, std::void_t<typename T::blue>, Void>
       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:32:8: note: previous definition is here
struct holder_type_of<T, std::void_t<typename T::red>, Void>
       ^

See live code on Compiler Explorer

How do I get my code to compile on all compilers, with all assertions passing? Also, is clang wrong here, and my code should actually work according to the standard?

英文:

I want to classify the "level of feature support" for a type.
The type can contain aliases red and blue. It can contain just one, both , or neither, and I want to classify each of these cases with an enum class:

enum class holder_type {
    none,
    red,
    blue,
    both
};

This is a use-case for the "member detector idiom", and I've tried to implement it using std::void_t:

// primary template
template &lt;class T, class = void, class = void&gt;
struct holder_type_of
    : std::integral_constant&lt;holder_type, holder_type::none&gt; {};

// substitution failure if no alias T::red exists
template &lt;class T, class Void&gt;
struct holder_type_of&lt;T, std::void_t&lt;typename T::red&gt;, Void&gt;
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};

// substitution failure if no alias T::blue exists
template &lt;class T, class Void&gt;
struct holder_type_of&lt;T, std::void_t&lt;typename T::blue&gt;, Void&gt;
    : std::integral_constant&lt;holder_type, holder_type::blue&gt; {};

// substitution failure if one of the aliases doesn&#39;t exist
template &lt;class T&gt;
struct holder_type_of&lt;T, std::void_t&lt;typename T::blue&gt;, std::void_t&lt;typename T::red&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::both&gt; {};

However, the first and second partial specialization are redefinitions of each other. The following assertions fail to compile with clang, but succeed with GCC (as wanted).

static_assert(holder_type_of&lt;red_holder&gt;::value == holder_type::red);
static_assert(holder_type_of&lt;blue_holder&gt;::value == holder_type::blue);
&lt;source&gt;:36:8: error: redefinition of &#39;holder_type_of&lt;T, std::void_t&lt;typename T::blue&gt;, Void&gt;&#39;
struct holder_type_of&lt;T, std::void_t&lt;typename T::blue&gt;, Void&gt;
       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
&lt;source&gt;:32:8: note: previous definition is here
struct holder_type_of&lt;T, std::void_t&lt;typename T::red&gt;, Void&gt;
       ^

See live code on Compiler Explorer

How do I get my code to compile on all compilers, with all assertions passing? Also, is clang wrong here, and my code should actually work according to the standard?


Note: The example is intentionally artificial, but could could be used in practice for things like classifying iterators as random-access/forward/etc. I've run into this issue when trying to detect members of a trait type that the user can specialize themselves.

答案1

得分: 3

这部分内容的中文翻译如下:

这看起来像是CWG1980std::void_t<typename T::red>std::void_t<typename T::blue>被视为“等价”,因为它们都是void,所以它们是重新定义,但它们在功能上不等价,因为它们可以通过替代失败来区分)。

即使你将它修复为一个依赖的void,如下所示:

template<typename...> struct dependent_void { using type = void; };
template<typename... T> using dependent_void_t = typename dependent_void<T...>::type;

这将产生这两个部分特化:

template <class T, class Void>
struct holder_type_of<T, dependent_void_t<typename T::red>, Void>
    : std::integral_constant<holder_type, holder_type::red> {};
template <class T>
struct holder_type_of<T, dependent_void_t<typename T::blue>, dependent_void_t<typename T::red>>
    : std::integral_constant<holder_type, holder_type::both> {};

由于中间参数dependent_void_t<typename T::red>dependent_void_t<typename T::blue>是不相关的,不管哪种方式都不会更好,所以存在歧义。

... 所以你可以翻转红色的参数,使最后一个参数相同,dependent_void_t<typename T::blue>比仅仅使用Void更好。你甚至可以回到std::void_t,因为你不再比较具有不同模板参数的多个std::void_t

template <class T, class Void>
struct holder_type_of<T, Void, std::void_t<typename T::red>>
    : std::integral_constant<holder_type, holder_type::red> {};

https://godbolt.org/z/s6dPYWeao

对于多个条件,这很快变得难以管理。


基于“优先级”的SFINAE检测更容易通过函数重载完成:

// 最低优先级重载
template<typename T>
constexpr holder_type holder_type_of_impl(...) { return holder_type::none; }
template<typename T, std::void_t<typename T::red>* = nullptr>
constexpr holder_type holder_type_of_impl(void*) { return holder_type::red; }
template<typename T, std::void_t<typename T::blue>* = nullptr>
constexpr holder_type holder_type_of_impl(long) { return holder_type::blue; }
template<typename T, std::void_t<typename T::red, typename T::blue>* = nullptr>
constexpr holder_type holder_type_of_impl(int) { return holder_type::both; }
// 最高优先级重载

template <class T>
struct holder_type_of
    : std::integral_constant<holder_type, ::holder_type_of_impl<T>(0)> {};

// 如果有超过4个优先级(可能是要拆分你的检查的迹象),
// 你可以使用这个方便的模板

template<unsigned N> struct priority : priority<N-1u> {};
template<> struct priority<0> {};

// 从最差到最好的匹配顺序为`f(priority<6>{})`
void f(priority<0>);
void f(priority<1>);
void f(priority<2>);
void f(priority<3>);
void f(priority<4>);
void f(priority<5>);
void f(priority<6>);

希望这对你有所帮助。

英文:

This looks like CWG1980 (std::void_t&lt;typename T::red&gt; and std::void_t&lt;typename T::blue&gt; are deemed "equivalent" since they are both void, so they are redefinitions, but they are not functionally equivalent since they can be distinguished by substitution failure).

And even if you were to fix it by making it a dependent void, like:

template&lt;typename...&gt; struct dependent_void { using type = void; };
template&lt;typename... T&gt; using dependent_void_t = typename dependent_void&lt;T...&gt;::type;

It would make these two partial specializations:

template &lt;class T, class Void&gt;
struct holder_type_of&lt;T, dependent_void_t&lt;typename T::red&gt;, Void&gt;
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};
template &lt;class T&gt;
struct holder_type_of&lt;T, dependent_void_t&lt;typename T::blue&gt;, dependent_void_t&lt;typename T::red&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::both&gt; {};

ambiguous since the middle arguments dependent_void_t&lt;typename T::red&gt; and dependent_void_t&lt;typename T::blue&gt; are unrelated and not better one way or the other.

... So you can flip the red one's arguments so the last argument is the same and the dependent_void_t&lt;typename T::blue&gt; is better than just Void. You can even go back to std::void_t, as you aren't comparing multiple std::void_ts with different template arguments anymore:

template &lt;class T, class Void&gt;
struct holder_type_of&lt;T, Void, std::void_t&lt;typename T::red&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};

https://godbolt.org/z/s6dPYWeao

This gets unmanageable pretty quickly for multiple conditions


"priority" based SFINAE detection is more easily done with function overloads though:

// Lowest priority overload
template&lt;typename T&gt;
constexpr holder_type holder_type_of_impl(...) { return holder_type::none; }
template&lt;typename T, std::void_t&lt;typename T::red&gt;* = nullptr&gt;
constexpr holder_type holder_type_of_impl(void*) { return holder_type::red; }
template&lt;typename T, std::void_t&lt;typename T::blue&gt;* = nullptr&gt;
constexpr holder_type holder_type_of_impl(long) { return holder_type::blue; }
template&lt;typename T, std::void_t&lt;typename T::red, typename T::blue&gt;* = nullptr&gt;
constexpr holder_type holder_type_of_impl(int) { return holder_type::both; }
// Highest priority overload

template &lt;class T&gt;
struct holder_type_of
    : std::integral_constant&lt;holder_type, ::holder_type_of_impl&lt;T&gt;(0)&gt; {};

// And if you have more than 4 priorities (probably a sign to break up your checks),
// you can use this handy template

template&lt;unsigned N&gt; struct priority : priority&lt;N-1u&gt; {};
template&lt;&gt; struct priority&lt;0&gt; {};

// Ordered from worst to best match for `f(priority&lt;6&gt;{})`
void f(priority&lt;0&gt;);
void f(priority&lt;1&gt;);
void f(priority&lt;2&gt;);
void f(priority&lt;3&gt;);
void f(priority&lt;4&gt;);
void f(priority&lt;5&gt;);
void f(priority&lt;6&gt;);

答案2

得分: 1

以下是您要翻译的内容:

"你的代码问题是由多个&lt;T, void_t&lt;&gt;&gt;的特化引起的,这些特化被计算为&lt;T, void&gt;,因此导致了重定义错误。为了避免这个问题,你需要为每个特化提供一些明确的区分。基本上,我看不出有任何逃避编写has_type特化的方法。

然而,使用标签可以让生活变得更简单。
以下是一个方便的API示例,反映了您的人工示例中的用法:

#include &lt;type_traits&gt;

struct S {};

struct empty {};

struct red_holder {
    using red = S;
};

struct blue_holder {
    using blue = S;
};

struct both_holder {
    using red = S;
    using blue = S;
};

enum /*class*/ holder_type {
    none = 0,
    red = 1,
    blue = 2,
    both = 3
};

namespace tag 
{
    struct red_t {} constexpr red{};
    struct blue_t{} constexpr blue{};
}

template &lt;typename Tag, typename T, typename = void&gt;
struct _has : std::integral_constant&lt;holder_type, holder_type::none&gt;{};

template &lt;typename T&gt;
struct _has&lt;tag::red_t, T, std::void_t&lt;typename T::red&gt;&gt; 
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};

template &lt;typename T&gt;
struct _has&lt;tag::blue_t, T, std::void_t&lt;typename T::blue&gt;&gt; 
    : std::integral_constant&lt;holder_type, holder_type::blue&gt; {};

template &lt;typename T, typename ... Tags&gt;
constexpr holder_type has(Tags... tags) noexcept {
    const auto flags = (_has&lt;Tags, T&gt;{} | ...);
    return static_cast&lt;holder_type&gt;(flags);
}

template &lt;typename T&gt;
constexpr holder_type has() noexcept {
    return holder_type::none;
}


static_assert(has&lt;empty&gt;() == holder_type::none);
static_assert(has&lt;red_holder&gt;(tag::red) == holder_type::red);
static_assert(has&lt;blue_holder&gt;(tag::blue) == holder_type::blue);

static_assert(has&lt;both_holder&gt;(tag::red) == holder_type::red);
static_assert(has&lt;both_holder&gt;(tag::blue) == holder_type::blue);
static_assert(has&lt;both_holder&gt;(tag::blue, tag::red) == holder_type::both);

由于(在我看来)你需要具有类型->值映射,我建议使用标志。

使用标签可以减少必要的样板代码。基本上,对于每个成员检测,你必须编写2个类模板(基本的“false”+特化)。通过使用标签,现在只需要编写一个特化。但在我们有反射支持之前,你必须编写它。"

英文:

The problem with your code is caused by several specializations of &lt;T, void_t&lt;&gt;&gt;, which are evaluated to &lt;T, void&gt;, hence the redifinition error.
In order to circumvent this problem, you need some disambiguation for each specialization. Basically, I see no way of escaping writing the specializations for has_type.

However, life can be made easier by using tags.
Here's an example of a convenient API that reflects the usage in your artificial example:

#include &lt;type_traits&gt;

struct S {};

struct empty {};

struct red_holder {
    using red = S;
};

struct blue_holder {
    using blue = S;
};

struct both_holder {
    using red = S;
    using blue = S;
};

enum /*class*/ holder_type {
    none = 0,
    red = 1,
    blue = 2,
    both = 3
};

namespace tag 
{
    struct red_t {} constexpr red{};
    struct blue_t{} constexpr blue{};
}

template &lt;typename Tag, typename T, typename = void&gt;
struct _has : std::integral_constant&lt;holder_type, holder_type::none&gt;{};

template &lt;typename T&gt;
struct _has&lt;tag::red_t, T, std::void_t&lt;typename T::red&gt;&gt; 
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};

template &lt;typename T&gt;
struct _has&lt;tag::blue_t, T, std::void_t&lt;typename T::blue&gt;&gt; 
    : std::integral_constant&lt;holder_type, holder_type::blue&gt; {};

template &lt;typename T, typename ... Tags&gt;
constexpr holder_type has(Tags... tags) noexcept {
    const auto flags = (_has&lt;Tags, T&gt;{} | ...);
    return static_cast&lt;holder_type&gt;(flags);
}

template &lt;typename T&gt;
constexpr holder_type has() noexcept {
    return holder_type::none;
}


static_assert(has&lt;empty&gt;() == holder_type::none);
static_assert(has&lt;red_holder&gt;(tag::red) == holder_type::red);
static_assert(has&lt;blue_holder&gt;(tag::blue) == holder_type::blue);

static_assert(has&lt;both_holder&gt;(tag::red) == holder_type::red);
static_assert(has&lt;both_holder&gt;(tag::blue) == holder_type::blue);
static_assert(has&lt;both_holder&gt;(tag::blue, tag::red) == holder_type::both);

https://godbolt.org/z/qPax11GYf

Since (it seems to me) you need to have type->value mappings, I suggest using flags.


Using tags reduces the necessary boilerplate. Basically for each member detection you have to write 2 class templates (basic "false" + specialization).
By using tags, you now have to write only a specialization. But until we have a reflection support, you have to write it.

答案3

得分: 0

如其他回答中所提到的问题在于:

template &lt;class T, class Void&gt;
struct holder_type_of&lt;T, std::void_t&lt;typename T::red&gt;, Void&gt;
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};

// 如果不存在别名 T::blue 则替代失败
template &lt;class T, class Void&gt;
struct holder_type_of&lt;T, std::void_t&lt;typename T::blue&gt;, Void&gt;

这两个部分特化是相同类型。可以通过在其中一个中交换 Voidstd::void_t 来解决这个问题,但这对于更多数量的部分特化不太适用,因为我们可能需要为每个部分特化添加一个额外的模板参数。

在libstdc++中常用的一种技术是添加一个 int Bullet 模板参数,用于对部分特化进行排序:

// 主模板,从 &lt;Bullet = 0&gt; 开始,并继承自 &lt;Bullet + 1&gt;
template &lt;class T, int Bullet = 0, class Void = void&gt;
struct holder_type_of : holder_type_of&lt;T, Bullet + 1, Void&gt; {};

// &lt;Bullet = 0&gt; 的第一个尝试
// 如果替代失败则回退到 &lt;Bullet = 1&gt;
template &lt;class T&gt;
struct holder_type_of&lt;T, 0, std::void_t&lt;typename T::blue, typename T::red&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::both&gt; {};

// Bullet = 1,回退到 Bullet = 2
template &lt;class T&gt;
struct holder_type_of&lt;T, 1, std::void_t&lt;typename T::red&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};

// Bullet = 2,回退到 Bullet = 3
template &lt;class T&gt;
struct holder_type_of&lt;T, 2, std::void_t&lt;typename T::blue&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::blue&gt; {};

// Bullet = 3,替代始终成功,因此我们有一个回退情况
template &lt;class T&gt;
struct holder_type_of&lt;T, 3, void&gt;
    : std::integral_constant&lt;holder_type, holder_type::none&gt; {};

Compiler Explorer的示例中查看实际示例。

英文:

As mentioned in other answers, the problem is that in:

template &lt;class T, class Void&gt;
struct holder_type_of&lt;T, std::void_t&lt;typename T::red&gt;, Void&gt;
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};

// substitution failure if no alias T::blue exists
template &lt;class T, class Void&gt;
struct holder_type_of&lt;T, std::void_t&lt;typename T::blue&gt;, Void&gt;

These two partial specializations are the same type. It's possible to solve this by swapping Void and std::void_t around in one of them, but this doesn't scale well to larger quantities of partial specializations, because we may need one extra template parameter for each.

A technique which is commonly used in libstdc++ is to add an int Bullet template parameter which is used for ordering the partial specializations:

// primary template, start at &lt;Bullet = 0&gt; and inherit from &lt;Bullet + 1&gt;
template &lt;class T, int Bullet = 0, class Void = void&gt;
struct holder_type_of : holder_type_of&lt;T, Bullet + 1, Void&gt; {};

// first attempt for &lt;Bullet = 0&gt;
// fall back onto &lt;Bullet = 1&gt; if substitution fails
template &lt;class T&gt;
struct holder_type_of&lt;T, 0, std::void_t&lt;typename T::blue, typename T::red&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::both&gt; {};

// Bullet = 1, fall back to Bullet = 2
template &lt;class T&gt;
struct holder_type_of&lt;T, 1, std::void_t&lt;typename T::red&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::red&gt; {};

// Bullet = 2, fall back to Bullet = 3
template &lt;class T&gt;
struct holder_type_of&lt;T, 2, std::void_t&lt;typename T::blue&gt;&gt;
    : std::integral_constant&lt;holder_type, holder_type::blue&gt; {};

// Bullet = 3, substitution always succeeds, so we have a fallback case
template &lt;class T&gt;
struct holder_type_of&lt;T, 3, void&gt;
    : std::integral_constant&lt;holder_type, holder_type::none&gt; {};

See live example at Compiler Explorer

huangapple
  • 本文由 发表于 2023年6月18日 19:22:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/76500284.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定