2

I'd like to check at compile-time if various enums contain a given value, so I've written the following:

#include <optional>

enum class test_enum : int {
    VALUE_0 = 0,
    VALUE_1 = 1
};

// Template function to perform check
template<typename T>
constexpr std::optional<T> from_int(int value)
{
    static_assert(false, __FUNCTION__ " not implemented for this type; see build output");
    return std::optional<T>();
}

// Specialization for test_enum
template<>
constexpr std::optional<test_enum> from_int(int value)
{
    switch (value) {
        case static_cast<int>(test_enum::VALUE_0) :
            return test_enum::VALUE_0;
        case static_cast<int>(test_enum::VALUE_1):
            return test_enum::VALUE_1;
        default:
            return std::optional<test_enum>();
    }
}

int main(int argc, char* argv[])
{
    static_assert(from_int<test_enum>(1));

    return 0;
}

Using Visual Studio 2017 (version 15.8.6), the code compiles successfully with no errors in the output. However, the error window shows

E0028: expression must have a constant value" at line 30. (the first line of main)

and

"std::_Optional_construct_base<test_enum>::_Optional_construct_base(std::in_place_t, _Types &&..._Args) [with _Types=<test_enum>]" (declared implicitly) is not defined)".

Any hints as to why this is? I can ignore E0028, but I'd prefer not to if possible.

EDIT: Removing the static_assert from from_int does not change the error.

tstevens
  • 63
  • 8

3 Answers3

3

It seems that the Standard defines such a code as ill-formed, no diagnostics required. Take a look at the following statements:

[The validity of a template may be checked prior to any instantiation. [ Note: Knowing which names are type names allows the syntax of every template to be checked in this way. — end note ] The program is ill-formed, no diagnostic required, if:

<...>

(8.4) a hypothetical instantiation of a template immediately following its definition would be ill-formed due to a construct that does not depend on a template parameter...] 1

To make it well-formed, do not use static_assert(false). Instead, use the following trick (compiles with GCC 7 and CLang 7):

#include <optional>

enum class test_enum : int {
    VALUE_0 = 0,
    VALUE_1 = 1
};

template<typename T> 
constexpr bool false_t = false;

// Template function to perform check
template<typename T>
constexpr std::optional<T> from_int(int value)
{
    static_assert(false_t<T>, "Not implemented for this type; see build output");
    return std::optional<T>();
}

// Specialization for test_enum
template<>
constexpr std::optional<test_enum> from_int<test_enum>(int value)
{
    switch (value) {
        case static_cast<int>(test_enum::VALUE_0) :
            return test_enum::VALUE_0;
        case static_cast<int>(test_enum::VALUE_1):
            return test_enum::VALUE_1;
        default:
            return std::optional<test_enum>();
    }
}

int main()
{
    static_assert(from_int<test_enum>(1));
}
Community
  • 1
  • 1
Igor R.
  • 14,716
  • 2
  • 49
  • 83
  • Unfortunately, that (and static_asserting with std::false_type) result in the same error in Visual Studio. Did the original code compile under GCC 7? EDIT: I see from StoryTeller's comment that Visual Studio should technically not accept the original template. – tstevens Oct 03 '18 at 18:44
  • @tstevens MSVC is another story... Typically, it has a poor compliance with the standard. – Igor R. Oct 03 '18 at 18:49
  • Yeah, I believe it. MSDN claims that Visual Studio 2017 is fully compliant with std::optional, so I guess this may be a bug. – tstevens Oct 03 '18 at 18:53
  • @tstevens - To be exact, MSVC doesn't have to reject it. The standard doesn't require a diagnostic, so it's not exactly non-conformant on MSVC's behalf. One can argue it's a poor QoI, however. – StoryTeller - Unslander Monica Oct 03 '18 at 19:06
  • @StoryTeller I wasn't very clear -- I meant the fact that Visual Studio rejects the call to from_int() in main (even after removing from_int()'s static assert) seems like a bug. – tstevens Oct 03 '18 at 19:08
  • @tstevens - Oh, I was referring to your first comment. Apologies for any and all confusion. – StoryTeller - Unslander Monica Oct 03 '18 at 19:16
  • @StoryTeller No worries, thanks for looking at this! – tstevens Oct 03 '18 at 19:23
3

It is far better to use tag dispatching than template specialziation in 99/100 cases.

#include <optional>

enum class test_enum : int {
    VALUE_0 = 0,
    VALUE_1 = 1
};

template<class T> struct tag_t {};

namespace from_int_details {
  template<class T>
  std::optional<T> from_int_impl( tag_t<T>, int value ) = delete;
}
template<class T>
std::optional<T> from_int( int value ) {
  using namespace from_int_details;
  return from_int_impl( tag_t<T>{}, value );
}

// Overload for test_enum, same namespace as test_enum *or* in from_int_details:
constexpr std::optional<test_enum> from_int_impl(tag_t<test_enum>, int value)
{
    switch (value) {
        case static_cast<int>(test_enum::VALUE_0) :
            return test_enum::VALUE_0;
        case static_cast<int>(test_enum::VALUE_1):
            return test_enum::VALUE_1;
        default:
            return std::optional<test_enum>();
    }
}

int main()
{
    static_assert(from_int<test_enum>(1));
}

here people extend from_int by writing a constexpr optional<the_enum_type> from_int_impl( tag_t<the_enum_type>, int ) either in the namespace of the_enum_type (so it can be found via ADL), or for enums where this isn't possible (like enums in std), in the from_int_details namespace. Or the namespace of tag_t.

Here is this code compiling in MSVC 2017, compiler version 19.10.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Why is tag dispatching better? To me, specializing from_int() is more readable because it doesn't require writing from_int_impl(), which is what I was trying to avoid in the first place. I'm using this in a library where I don't want the library user to have to consider the differences between from_int() and from_int_impl() -- I'd rather them only see from_int(). – tstevens Oct 03 '18 at 21:53
  • Because it allows customization to occur in the namespace of the enum. Because the `=delete` is legal, while `static_assert(false` is not. Because workarounds of `static_assert(false` are only arguably legal (depending on interpretation of standard requirements that require templates functions to have a valid body for some type). Because `_impl` is optional; remove it and the code still works. Because specialization of template functions is athema that introducrs a highly counterintuitive counterpoint to already complex C++ overload rules. Why do you ask? – Yakk - Adam Nevraumont Oct 03 '18 at 22:50
  • Appreciate the followup. I should have specified in my original question that the test_enum is in namespace A::B, where from_int is in namespace A. I've been able to specialize the template just fine in different namespaces. From the end user's perspective, using tag_t just changes the function call from from_int() to from_int(tag_t). How is that thematically different from specializing the template function directly? – tstevens Oct 04 '18 at 18:01
  • Also, I'm inclined to keep the static assert because it allows me to provide a simple, customized error message to the user (you've seen the usual template compiler errors -- they're not exactly easy to read if your user isn't super familiar with C++). – tstevens Oct 04 '18 at 18:02
  • 1
    @tstevens The user never has to use `tag_t` unless adding a new overload; user-code just does `some_ns::from_int`.. Template specialization only works in the namespace where the template was originally declared. I understand why you want to use `static_assert( false, ` or a hack to do so; I am saying that doing so while justifying your program is well formed requires a lot of faith in a specific reading of the standard that your compiler may disagree with (if not today, eventually). Ie, your code could break in arbitrary ways in future compiler updates. – Yakk - Adam Nevraumont Oct 04 '18 at 18:23
  • If your template is in `namespace A`, specializations must be in `namespace A`. If you aren't in `namespace A` you might think you are specializing while you are actually overloading, or something else strange. In any case, it is your code base, good luck. – Yakk - Adam Nevraumont Oct 04 '18 at 18:25
  • Ah you're right, I definitely had to declare the specializations in namespace A. I'll keep looking template specialization vs overloading -- if I can initially define from_int() in namespace A and overload in namespace B, I'll definitely switch (and I'd forgotten about the automatic inference of tag_t based on from_int). Thanks again for your comments. – tstevens Oct 04 '18 at 18:46
0

The code in question compiles without warnings or errors using cl v19.15.26726 (Visual Studio version 15.9.0-pre.1.0)

Swordfish
  • 12,971
  • 3
  • 21
  • 43
  • Sweet, thank you. I'll see if I can suppress the error for now and make a note to remove the workaround after updating. – tstevens Oct 03 '18 at 19:22