Ben
Ben

Reputation: 9703

Creating a constexpr enum-like type

I've been using enum class FooEnabled : bool { no, yes }; as a way to create type-safe bools. It works well, except I'd like to add explicit conversion to bool, Boolean operators like operator!, etc. I can do that like this:

template <typename Tag>
class TypedBool {
    bool value;
    explicit constexpr TypedBool(bool b) noexcept : value(b) {}
public:
    static inline TypedBool no{false};
    static inline TypedBool yes{true};
    
    explicit constexpr operator bool() const noexcept { return value; }
    constexpr TypedBool operator!() const noexcept { return TypedBool{!value}; }
    // ...
};

using FooEnabled = TypedBool<struct FooEnabledTag>;

That works great, however no and yes aren't constexpr, so I can't do if constexpr (FooEnabled::yes) { for example. If I make no and yes be instead static constexpr, clang is upset because TypedBool is not a literal type. That appears to be because TypedBool is incomplete at that point.

The simplest example of this is struct S { static constexpr S s; }; which gives

error: constexpr variable cannot have non-literal type 'const S'
struct S { static constexpr S s; };
                              ^
note: incomplete type 'const S' is not a literal type
note: definition of 'S' is not complete until the closing '}'
struct S { static constexpr S s; };

Is there any way around this? I could make no and yes be a different type that implicitly converts to TypedBool<Tag>, but that seems weird, because then auto x = FooEnabled::yes; would make x not be a FooEnabled, so

auto x = FooEnabled::yes;
[](FooEnabled& x) { x = !x; }(x);

would fail.

Is there any way to have a class contain static constexpr members that are its own type? The solutions I'm starting to think of all seem too ugly to mention (and those also have constexpr limitations).

Upvotes: 0

Views: 418

Answers (3)

parktomatomi
parktomatomi

Reputation: 4079

I think you can accomplish your goals (supporting ! operators and explicit conversion to bool) without changing your scoped enumerations.

All scoped enumerations appear to support explicit conversion to bool, even if bool isnt' the underlying type

enum class NotBool : int { No, Yes };
constexpr bool bad = NotBool::Yes; // compile error
constexpr bool yes = bool(NotBool::Yes);

You can overload the ! operator for all scoped enums that have underlying booleans with a template and std::enable_if:

template <typename T>
constexpr bool HasUnderlyingBool = std::is_same_v<std::underlying_type_t<T>, bool>;

template <typename T>
constexpr std::enable_if_t<HasUnderlyingBool<T>, T> operator !(const T& value) {
    return T(!bool(value));
}

enum class Bool : bool { No, Yes };
static_assert(!Bool::Yes == Bool::No);
static_assert(!Bool::No == Bool::Yes);

Upvotes: 0

Quimby
Quimby

Reputation: 19123

Is there any way to have a class contain static constexpr members that are its own type?

Yes, there is, just split the declaration from the definition, only the definition needs to contain constexpr.

struct Foo {
    constexpr Foo(bool b): value(b){}

    static const Foo yes;
    static const Foo no;

    constexpr explicit operator bool() const noexcept{return value;}
    bool value;
};
// Mark inline if in a header.
inline constexpr const Foo Foo::yes{true};
inline constexpr const Foo Foo::no{false};

int main(){

    if constexpr(Foo::yes){
        return 5;
    };
}

Isn't this different declaration vs definition ?

All three compilers g++,clang++,MSCV 19 accept the code above.

But if Foo is a template, clang++ doesn't compile the code anymore, as discovered in comments.

There is a question about this hinting the standard does not forbid this. Unfortunately, C++17, C++20 standards are no more explicit either, stating: The final C++17 draft requires of [dcl.constexpr][Empahis mine]

The constexpr specifier shall be applied only to the definition of a variable or variable template or the declaration of a function or function template. The consteval specifier shall be applied only to the declaration of a function or function template. A function or static data member declared with the constexpr or consteval specifier is implicitly an inline function or variable ([dcl.inline]). If any declaration of a function or function template has a constexpr or consteval specifier, then all its declarations shall contain the same specifier.

So my take from this is this is allowed but maybe due to omission rather than deliberation. I did not manage to find any examples in the Standard that would validate this approach.

Upvotes: 1

Pepijn Kramer
Pepijn Kramer

Reputation: 12891

This is the closest syntax I know works

class TypedBool 
{
public:    
    explicit constexpr TypedBool(bool value) noexcept : 
        m_value{ value }
    {
    }

    static constexpr TypedBool no()
    {
        constexpr TypedBool value{ false };
        return value;
    }
 
    static constexpr TypedBool yes()
    {
        constexpr TypedBool value{ true };
        return value;
    }

    explicit constexpr operator bool() const noexcept { return m_value; }

private:
    bool m_value;


};

int main()
{
    constexpr TypedBool value{ true };
    static_assert(value);
    static_assert(TypedBool::yes());
    return 0;
}

Upvotes: 0

Related Questions