Is there a way to specify the default constructor of an enum class?

I am using an enum class to specify a set of values which are allowable for a particular datatype in a library: in this case, it's the GPIO pin id numbers of a Raspberry Pi. It looks something like this:

enum class PinID : int {N4 = 4, N17 = 17, /* ...etc... */ }

The point of me doing this instead just of using, say, an int is to ensure that code is safe: I can static_assert (or otherwise compile-time ensure -- the actual method used is not important to me) things like that someone hasn't made a spelling error (passing a 5 instead of a 4, etc), and I get automatic error messages for type mismatches, etc.

The problem then is that enum class has a default constructor that -- for compatibility's sake with C's enums I assume (since they have the same behaviour) -- initializes to the enum class equivalent of 0. In this case, there is no 0 value. This means that a user making a declaration/definition like:

PinID pid = PinID();

is getting an enumerator that isn't explicitly defined (and doesn't even seem to "exist" when one looks at the code), and can lead to runtime errors. This also means that techniques like switching over the values of explicitly defined enumerators is impossible without having an error/default case -- something I want to avoid, since it forces me to either throw or do something like return a boost::optional, which are less amenable to static analysis.

I tried to define a default constructor to no avail. I (desperately) tried to define a function which shares the name of the enum class, but this (rather unsurprisingly) resulted in strange compiler errors. I want to retain the ability to cast the enum class to int, with all N# enumerators mapping to their respective #, so merely "defining", say, N4 = 0 is unacceptable; this is for simplicity and sanity.

I guess my question is two-fold: is there a way to get the kind of static safety I'm after using enum class? If not, what other possibilities would one prefer? What I want is something which:

  1. is default constructable
  2. can be made to default construct to an arbitrary valid value
  3. provides the "finite set of specified" values afforded by enum classes
  4. is at least as type safe as an enum class
  5. (preferably) doesn't involve runtime polymorphism

The reason I want default constructability is because I plan to use boost::lexical_cast to reduce the syntactic overhead involved in conversions between the enum class values, and the actual associated strings which I output to the operating system (sysfs in this case); boost::lexical_cast requires default constructability.

Errors in my reasoning are welcome -- I am beginning to suspect that enum classes are the right object for the wrong job, in this case; clarification will be offered if asked. Thank you for your time.

有帮助吗?

解决方案

A type defined with enum class or enum struct is not a a class but a scoped enumeration and can not have a default constructor defined. The C++11 standard defines that your PinID pid = PinID(); statement will give a zero-initialization. Where PinID was defined as a enum class. It also allows enum types in general to hold values other than the enumerator constants.

To understand that PinID() gives zero initialization requires reading standard sections 3.9.9, 8.5.5, 8.5.7 and 8.5.10 together:

8.5.10 - An object whose initializer is an empty set of parentheses, i.e., (), shall be value-initialized

8.5.7 - To value-initialize an object of type T means: ... otherwise, the object is zero-initialized.

8.5.5 - To zero-initialize an object or reference of type T means: — if T is a scalar type (3.9), the object is set to the value 0 (zero), taken as an integral constant expression, converted to T;

3.9.9 - States that enumeration types are part of the set of types known as scalar types.

A possible solution:

To meet your points 1 to 5 you could write a class along the lines of:

class PinID
{
private:
    PinID(int val)
    : m_value(val)
    {}

    int m_value;

public:
    static const PinID N4;
    static const PinID N17;
    /* ...etc... */ 

    PinID() 
    : m_value(N4.getValue())
    {}

    PinID(const PinID &id)
    : m_value(id.getValue())
    {}

    PinID &operator = (const PinID &rhs)
    {
        m_value = rhs.getValue();
        return *this;
    }

    int getValue() const
    {
        return m_value;
    }

    // Attempts to create from int and throw on failure.
    static PinID createFromInt(int i);

    friend std::istream& operator>>(std::istream &is, PinID &v)
    {
        int candidateVal(0);
        is >> candidateVal;
        v = PinID::createFromInt(candidateVal);
        return is;
    }
};

const PinID PinID::N4 = PinID(4);
/* ...etc... */

That can give you something that you would have to make specific efforts to get an invalid values into. The default constructor and stream operator should allow it to work with lexical_cast.

Seems it depends how critical the operations on a PinID are after it's creation whether it's worth writing a class or just handling the invalid values everywhere as the value is used.

其他提示

An enum class is just a strongly-typed enum; it's not a class. C++11 just reused the existing class keyword to avoid introducing a new keyword that would break compatibility with legacy C++ code.

As for your question, there is no way to ensure at compile time that a cast involves a proper candidate. Consider:

int x;
std::cin >> x;
auto p = static_cast<PinID>(x);

This is perfectly legal and there is no way to statically ensure the console user has done the right thing.

Instead, you will need to check at runtime that the value is valid. To get around this in an automated fashion, one of my co-workers created an enum generator that builds these checks plus other helpful routines given a file with enumeration values. You will need to find a solution that works for you.

I know that this question is dated and that it already has an accepted answer but here is a technique that might help in a situation like this with some of the newer features of C++

You can declare this class's variable either non static or static, it can be done in several ways permitted on support of your current compiler.


Non Static:

#include <iostream>
#include <array>

template<unsigned... IDs>
class PinIDs {
private:
    const std::array<unsigned, sizeof...(IDs)> ids { IDs... };
public:
    PinIDs() = default;
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

Static: - There are 3 ways to write this: (First One - C++11 or 14 or higher) last 2 (c++17).

Don't quote me on the C++11 part; I'm not quite sure when variadic templates or parameter packs were first introduced.

template<unsigned... IDs>
class PinIDs{
private:        
    static const std::array<unsigned, sizeof...(IDs)> ids;
public:    
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

template<unsigned... IDs>
const std::array<unsigned, sizeof...(IDs)> PinIDs<IDs...>::ids { IDs... };

template<unsigned... IDs>
class PinIDs{
private:
    static constexpr std::array<unsigned, sizeof...(IDs)> ids { IDs... }; 
public:   
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

template<unsigned... IDs>
class PinIDs{
private:
    static inline const std::array<unsigned, sizeof...(IDs)> ids { IDs... };
public:    
    PinIDs() = default;    
    const unsigned& operator[]( unsigned idx ) const {
        if ( idx < 0 || idx > ids.size() - 1 ) {
            return -1;
        }
        return ids[idx];
    }
};

All examples above either non-static or static work with the same use case below and provide the correct results:

int main() {
    PinIDs<4, 17, 19> myId;

    std::cout << myId[0] << " ";
    std::cout << myId[1] << " ";
    std::cout << myId[2] << " ";

    std::cout << "\nPress any key and enter to quit." << std::endl;
    char c;
    std::cin >> c;

    return 0;
}

Output

4 17 19
Press any key and enter to quit.

With this type of class template using a variadic parameter list, you don't have to use any constructor but the default. I did add bounds checking into the array so that the operator[] doesn't exceed bounds of its size; I could of threw an error but with unsigned type I just simply returned -1 as an invalid value.

With this type, there is no default as you have to instantiate this kind of object via template parameter list with a single or set of values. If one wants to they can specialize this class with a single parameter of 0 for a default type. When you instantiate this type of object; it is final as in it can not be changed from its declaration. This is a const object and still holds to be default constructible.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top