-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
<type_traits>
: Implement is_scoped_enum
#1950
Conversation
<type_traits>
: Implement is_scoped_enum
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to reconsider the implementation strategy. From what I can see it should be much simpler
inline constexpr bool is_scoped_enum_v<_Ty, false> = false; | ||
|
||
template <class _Ty> | ||
struct is_scoped_enum : bool_constant<is_scoped_enum_v<_Ty>> {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am a bit torn, usually we always follow the convention to define the variable template and then derive the struct from it.
In this case we require the short circuiting from conjunction
, so maybe the "best" implementation would be to do
struct is_scoped_enum : bool_constant<is_scoped_enum_v<_Ty>> {}; | |
struct is_scoped_enum : conjunction<is_enum<_Ty>, negation<is_convertible<_Ty, int>>> {}; |
And then derive the variable template from that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was originally under is_unbounded_array
as that's where is_scoped_enum
is depicted in the standard, and that implementation doesn't do that. (It was moved as is_enum and is_convertible are defined below).
Is there a general rule about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figure most people would be using the suffix version and doing it the traditional way would mean an extra template instantiation, wouldn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our modern convention is to follow the Standard order when it's easy to do so - however, the type traits are fairly jumbled, partly due to history, partly due to their dependencies. I wouldn't worry about the ordering.
As for whether the struct or the variable template should be "fundamental" - it's the struct that's costly for throughput, whereas variable templates are cheap. The compiler has built-in optimizations for conjunction_v
, negation
, is_convertible
, and I think is_enum
too. So, the code should have good throughput as-is.
While it's possible to duplicate the implementation to improve the struct throughput slightly, as @miscco depicted, I don't think that's necessary here (because I don't think that this type trait will be used in a massive number of specializations - whereas the traits like is_convertible
are highly used). Additionally, note that if we did duplicate the implementation, we would want to "smash" the base class to true_type
or false_type
- i.e. derive from conjunction<ARGS>::type
(or bool_constant<conjunction_v<ARGS>>
) and not conjunction<ARGS>
directly. (If you search, you'll see a few occurrences of this throughput the STL.) This simplifies the actual inheritance.
@@ -629,6 +632,10 @@ void test_function_type() { | |||
STATIC_ASSERT(!is_bounded_array_v<T>); | |||
STATIC_ASSERT(!is_unbounded_array_v<T>); | |||
#endif // _HAS_CXX20 | |||
|
|||
#if _HAS_CXX23 | |||
STATIC_ASSERT(!is_scoped_enum_v<T>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am missing some positive tests
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hence the draft PR and text body. Didn't know where the best place to put them in.
* Simplify is_scoped_enum_v thanks to miscco. * Add missing yvals_core.h macro comment. * Format with clang-format 11.
I suggest |
Was kinda hoping for something a little more specific 😅 so I'll take that as "anywhere" then. Going with just after these enums STL/tests/std/tests/VSO_0000000_type_traits/test.cpp Lines 856 to 860 in 696c536
And am I correct in thinking that these other tests don't need the new addition? (probably would have been helpful of me to have included these questions in the OP...) |
@@ -851,6 +858,13 @@ enum LLEnum : long long { xLongExample, yLongExample }; | |||
enum class ExampleEnumClass { xExample, yExample }; | |||
enum class LLEnumClass : long long { xLongExample, yLongExample }; | |||
|
|||
#if _HAS_CXX23 | |||
STATIC_ASSERT(!is_scoped_enum<ExampleEnum>::value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I remove this?
I'm not seeing the structs used much.
Also I kinda forgot about the test on line 80, should I just create some enums up top (or move these enums?) and move this test block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's good to have minimal coverage that the struct form works.
The location of this test block seems fine to me - the previous locations in this file are doing different things.
I'd be fine with not updating the old tests. (There's a weak argument for updating them so they keep mentioning all type traits, but for example we stopped adding As long as the new tests cover the "interesting" cases (e.g. |
@@ -851,6 +858,13 @@ enum LLEnum : long long { xLongExample, yLongExample }; | |||
enum class ExampleEnumClass { xExample, yExample }; | |||
enum class LLEnumClass : long long { xLongExample, yLongExample }; | |||
|
|||
#if _HAS_CXX23 | |||
STATIC_ASSERT(!is_scoped_enum<ExampleEnum>::value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's good to have minimal coverage that the struct form works.
The location of this test block seems fine to me - the previous locations in this file are doing different things.
inline constexpr bool is_scoped_enum_v<_Ty, false> = false; | ||
|
||
template <class _Ty> | ||
struct is_scoped_enum : bool_constant<is_scoped_enum_v<_Ty>> {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our modern convention is to follow the Standard order when it's easy to do so - however, the type traits are fairly jumbled, partly due to history, partly due to their dependencies. I wouldn't worry about the ordering.
As for whether the struct or the variable template should be "fundamental" - it's the struct that's costly for throughput, whereas variable templates are cheap. The compiler has built-in optimizations for conjunction_v
, negation
, is_convertible
, and I think is_enum
too. So, the code should have good throughput as-is.
While it's possible to duplicate the implementation to improve the struct throughput slightly, as @miscco depicted, I don't think that's necessary here (because I don't think that this type trait will be used in a massive number of specializations - whereas the traits like is_convertible
are highly used). Additionally, note that if we did duplicate the implementation, we would want to "smash" the base class to true_type
or false_type
- i.e. derive from conjunction<ARGS>::type
(or bool_constant<conjunction_v<ARGS>>
) and not conjunction<ARGS>
directly. (If you search, you'll see a few occurrences of this throughput the STL.) This simplifies the actual inheritance.
Co-authored-by: Stephan T. Lavavej <[email protected]>
#if _HAS_CXX23 | ||
// STRUCT TEMPLATE is_scoped_enum | ||
template <class _Ty> | ||
inline constexpr bool is_scoped_enum_v = conjunction_v<is_enum<_Ty>, negation<is_convertible<_Ty, int>>>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enum E {
e = std::is_scoped_enum_v<E>
};
static_assert(!e);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a diabolical brilliant brilliantly diabolical example. I think this means we need a compiler builtin.
The Standardese is WG21-N4885 [dcl.enum]/6 "An enumeration whose underlying type is not fixed is an incomplete type until the closing }
of its enum-specifier, at which point it becomes a complete type." and [meta.rqmts]/5 "Unless otherwise specified, an incomplete type may be used to instantiate a template specified in 20.15." There are no additional preconditions for is_scoped_enum
, so the incomplete enum E
is permitted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I'm a little confused. Is that not supposed to compile?
enum E {
e1 = std::is_scoped_enum_v<E>,
e2 = std::is_enum_v<E>
};
static_assert(!e1);
static_assert(e2);
This compiles just fine. If is_scoped_enum_v
isn't supposed to work here then why does is_enum_v
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC
enum E {
a = std::is_convertible_v<E, int>, // UB because E is incomplete
e = std::is_scoped_enum_v<E> // should be ok; value is false
};
But I think it's valid to rely on the implementation-specific knowledge that std::is_convertible_v
works in such case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, that makes sense. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That second example is quite the eyebrow raiser...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so it sounds like (a) is_convertible_v
is theoretically an issue here, but (b) in practice, MSVC's nonconformance allows this to compile and get the right answer. In any event, (c) this is an extreme corner case.
I think we can proceed with the current implementation then. If we discover that the issue is worse than previously thought, we can always request a compiler builtin and replace the implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can proceed with the current implementation then. If we discover that the issue is worse than previously thought, we can always request a compiler builtin and replace the implementation.
I'm inclined to request intrinsics now so the library can be prepared for MSVC's enumeration bugs to be fixed. When some compiler dev goes to audit what things break with the bug fixed, I'd rather we don't show up on that list.
In practice, this still looks like "go ahead and merge this implementation", the only difference would really be that we may update it to use intrinsics a bit sooner.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can proceed with the current implementation then. If we discover that the issue is worse than previously thought, we can always request a compiler builtin and replace the implementation.
FWIW, CWG-2516 has been accepted as a DR, so a scoped enum type is always complete when its name is declared, and hence no intrinsic is needed for is_scoped_enum
. But we may still have to replace the current implementation once MSVC's bug (or non-conforming extension) is fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that MSVC has implemented /Zc:enumTypes
(Enable enum type deduction), but it seems that despite this option, MSVC still believes that enumeration types are always complete, and that the underlying type is int
before the closing }
.
#include <type_traits>
template<class T>
constexpr std::underlying_type_t<T> f() {
T t;
return 1;
}
enum E {
a = f<E>(), // should be ill-formed
b = sizeof(E), // should be ill-formed
c = 0x1'0000'0000,
};
static_assert(b == sizeof(E)); // fires under /Zc:enumTypes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like one more test case, otherwise this LGTM.
There's a libcxx test for |
Every time a PR merges, it takes us one step closer to the vision: A type trait on every desk and in every home. Thank you for your lasting contribution to STL history! 😺 |
I was looking for a place to put the new tests and got a bit lost in it all... so I think I need a bit of guidance there.
fixes: #1449