英文:
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 <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?
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
这部分内容的中文翻译如下:
这看起来像是CWG1980(std::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<typename T::red>
and std::void_t<typename T::blue>
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<typename...> struct dependent_void { using type = void; };
template<typename... T> using dependent_void_t = typename dependent_void<T...>::type;
It would make these two partial specializations:
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> {};
ambiguous since the middle arguments dependent_void_t<typename T::red>
and dependent_void_t<typename T::blue>
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<typename T::blue>
is better than just Void
. You can even go back to std::void_t
, as you aren't comparing multiple std::void_t
s with different template arguments anymore:
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
This gets unmanageable pretty quickly for multiple conditions
"priority" based SFINAE detection is more easily done with function overloads though:
// Lowest priority overload
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; }
// Highest priority overload
template <class T>
struct holder_type_of
: std::integral_constant<holder_type, ::holder_type_of_impl<T>(0)> {};
// And if you have more than 4 priorities (probably a sign to break up your checks),
// you can use this handy template
template<unsigned N> struct priority : priority<N-1u> {};
template<> struct priority<0> {};
// Ordered from worst to best match for `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>);
答案2
得分: 1
以下是您要翻译的内容:
"你的代码问题是由多个<T, void_t<>>
的特化引起的,这些特化被计算为<T, void>
,因此导致了重定义错误。为了避免这个问题,你需要为每个特化提供一些明确的区分。基本上,我看不出有任何逃避编写has_type
特化的方法。
然而,使用标签可以让生活变得更简单。
以下是一个方便的API示例,反映了您的人工示例中的用法:
#include <type_traits>
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 <typename Tag, typename T, typename = void>
struct _has : std::integral_constant<holder_type, holder_type::none>{};
template <typename T>
struct _has<tag::red_t, T, std::void_t<typename T::red>>
: std::integral_constant<holder_type, holder_type::red> {};
template <typename T>
struct _has<tag::blue_t, T, std::void_t<typename T::blue>>
: std::integral_constant<holder_type, holder_type::blue> {};
template <typename T, typename ... Tags>
constexpr holder_type has(Tags... tags) noexcept {
const auto flags = (_has<Tags, T>{} | ...);
return static_cast<holder_type>(flags);
}
template <typename T>
constexpr holder_type has() noexcept {
return holder_type::none;
}
static_assert(has<empty>() == holder_type::none);
static_assert(has<red_holder>(tag::red) == holder_type::red);
static_assert(has<blue_holder>(tag::blue) == holder_type::blue);
static_assert(has<both_holder>(tag::red) == holder_type::red);
static_assert(has<both_holder>(tag::blue) == holder_type::blue);
static_assert(has<both_holder>(tag::blue, tag::red) == holder_type::both);
由于(在我看来)你需要具有类型->值映射,我建议使用标志。
使用标签可以减少必要的样板代码。基本上,对于每个成员检测,你必须编写2个类模板(基本的“false”+特化)。通过使用标签,现在只需要编写一个特化。但在我们有反射支持之前,你必须编写它。"
英文:
The problem with your code is caused by several specializations of <T, void_t<>>
, which are evaluated to <T, void>
, 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 <type_traits>
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 <typename Tag, typename T, typename = void>
struct _has : std::integral_constant<holder_type, holder_type::none>{};
template <typename T>
struct _has<tag::red_t, T, std::void_t<typename T::red>>
: std::integral_constant<holder_type, holder_type::red> {};
template <typename T>
struct _has<tag::blue_t, T, std::void_t<typename T::blue>>
: std::integral_constant<holder_type, holder_type::blue> {};
template <typename T, typename ... Tags>
constexpr holder_type has(Tags... tags) noexcept {
const auto flags = (_has<Tags, T>{} | ...);
return static_cast<holder_type>(flags);
}
template <typename T>
constexpr holder_type has() noexcept {
return holder_type::none;
}
static_assert(has<empty>() == holder_type::none);
static_assert(has<red_holder>(tag::red) == holder_type::red);
static_assert(has<blue_holder>(tag::blue) == holder_type::blue);
static_assert(has<both_holder>(tag::red) == holder_type::red);
static_assert(has<both_holder>(tag::blue) == holder_type::blue);
static_assert(has<both_holder>(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 <class T, class Void>
struct holder_type_of<T, std::void_t<typename T::red>, Void>
: std::integral_constant<holder_type, holder_type::red> {};
// 如果不存在别名 T::blue 则替代失败
template <class T, class Void>
struct holder_type_of<T, std::void_t<typename T::blue>, Void>
这两个部分特化是相同类型。可以通过在其中一个中交换 Void
和 std::void_t
来解决这个问题,但这对于更多数量的部分特化不太适用,因为我们可能需要为每个部分特化添加一个额外的模板参数。
在libstdc++中常用的一种技术是添加一个 int Bullet
模板参数,用于对部分特化进行排序:
// 主模板,从 <Bullet = 0> 开始,并继承自 <Bullet + 1>
template <class T, int Bullet = 0, class Void = void>
struct holder_type_of : holder_type_of<T, Bullet + 1, Void> {};
// <Bullet = 0> 的第一个尝试
// 如果替代失败则回退到 <Bullet = 1>
template <class T>
struct holder_type_of<T, 0, std::void_t<typename T::blue, typename T::red>>
: std::integral_constant<holder_type, holder_type::both> {};
// Bullet = 1,回退到 Bullet = 2
template <class T>
struct holder_type_of<T, 1, std::void_t<typename T::red>>
: std::integral_constant<holder_type, holder_type::red> {};
// Bullet = 2,回退到 Bullet = 3
template <class T>
struct holder_type_of<T, 2, std::void_t<typename T::blue>>
: std::integral_constant<holder_type, holder_type::blue> {};
// Bullet = 3,替代始终成功,因此我们有一个回退情况
template <class T>
struct holder_type_of<T, 3, void>
: std::integral_constant<holder_type, holder_type::none> {};
在Compiler Explorer的示例中查看实际示例。
英文:
As mentioned in other answers, the problem is that in:
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>
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 <Bullet = 0> and inherit from <Bullet + 1>
template <class T, int Bullet = 0, class Void = void>
struct holder_type_of : holder_type_of<T, Bullet + 1, Void> {};
// first attempt for <Bullet = 0>
// fall back onto <Bullet = 1> if substitution fails
template <class T>
struct holder_type_of<T, 0, std::void_t<typename T::blue, typename T::red>>
: std::integral_constant<holder_type, holder_type::both> {};
// Bullet = 1, fall back to Bullet = 2
template <class T>
struct holder_type_of<T, 1, std::void_t<typename T::red>>
: std::integral_constant<holder_type, holder_type::red> {};
// Bullet = 2, fall back to Bullet = 3
template <class T>
struct holder_type_of<T, 2, std::void_t<typename T::blue>>
: std::integral_constant<holder_type, holder_type::blue> {};
// Bullet = 3, substitution always succeeds, so we have a fallback case
template <class T>
struct holder_type_of<T, 3, void>
: std::integral_constant<holder_type, holder_type::none> {};
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论