-->

Incorrect assignment of values in char enum

2020-03-08 09:18发布

问题:

I was playing with enums and tried to reproduce some examples from this page. Initial examples worked as intended, however I got some interesting results with following code:

#include <iostream>

enum num : char {
    zero = '0',
    one = '1',
    two = '2',
    three = '3',
    four = '4',
    five = '5',
    six = '6'
};

int main()
{
    const char two = '2';
    std::cout << two << std::endl;
    std::cout << num::two;
    return 0;
}

The output is:

2
50

I expected both outcomes to be the same, but the num::two seems to print some other value. Also this value doesn't changes(50), so I assume this isn't a random/garbage value & there is some sort of char/int parsing being done that I don't understand? Here is the ideone link.

I know that I can make it work by assigning like this zero = 0, without single quotes and it works. However, I want to know what is happening behind the scenes and how could I control what non-single digits value I can print via single quotes assignments.

回答1:

This should actually go to the char overload now; unfortunately none of the compilers at issue implement DR 1601.

[conv.prom]/4:

A prvalue of an unscoped enumeration type whose underlying type is fixed ([dcl.enum]) can be converted to a prvalue of its underlying type.

This means num can be promoted to char.

Moreover, if integral promotion can be applied to its underlying type, a prvalue of an unscoped enumeration type whose underlying type is fixed can also be converted to a prvalue of the promoted underlying type.

So num can be promoted to int, too.

The relevant candidates are:

template <class Traits >
basic_ostream<char,Traits>& operator<<( basic_ostream<char,Traits>& os,
                                        char ch );
template<class charT, class Traits>
basic_ostream<charT, Traits>& basic_ostream<charT, Traits>::operator<<(int);

For both candidates, the first argument is an identity conversion and the second is a promotion. Both num to char and num to int have promotion rank.

Pre-DR1601, these are equally as good, so the template/non-template tiebreaker comes in. The first one is a function template; the second one is a plain member function, so the second one wins.

DR1601 added a rule that says:

A conversion that promotes an enumeration whose underlying type is fixed to its underlying type is better than one that promotes to the promoted underlying type, if the two are different.

This means that num to char is now better than num to int, so the first overload is now a better match and should be selected.



回答2:

According to the C++ Standard (4.5 Integral promotions)

4 A prvalue of an unscoped enumeration type whose underlying type is fixed (7.2) can be converted to a prvalue of its underlying type. Moreover, if integral promotion can be applied to its underlying type, a prvalue of an unscoped enumeration type whose underlying type is fixed can also be converted to a prvalue of the promoted underlying type.

So the integral promotion is applied and the operator << for objects of type int is called.



回答3:

When you say enum num : char, then you express the fact that num is internally implemented in terms of a char but can still be automatically converted to an integer value, which is not necessarily char.

As the page you cite says:

Values of unscoped enumeration type are implicitly-convertible to integral types.

See Why does a value of an enum with a fixed underlying type of char resolve to fct(int) instead of fct(char)? for an interesting discussion on problems in the C++ standard's wording regarding the combination of integral promotion and fixed underlying types.

In any case, you can imagine this whole thing like a class with a private char member variable and a public int conversion operator:

// very similar to your enum:
class num {
private:
    char c;

public:
    num(char c) : c(c) {}
    operator int() const {
        return c;
    }
};

num two { '2' };
std::cout << two; // prints 50

To increase type safety and make the std::cout line a compilation error, just turn the enum into an enum class:

enum class num : char

This is again similar to the imagined class num above, but without the conversion operator.

When you feed an instance of num to std::cout, then you are a client of num and are not logically supposed to think that the output format will take its internal char implementation into account.

To get more control over the output format, you should instead do like with any other custom type, and overload operator<< for std::ostream. Example:

#include <iostream>

enum class num : char {
    zero = '0',
    one = '1',
    two = '2',
    three = '3',
    four = '4',
    five = '5',
    six = '6'
};

std::ostream& operator<<(std::ostream& os, num const& n)
{
    switch (n)
    {
        case num::zero: os << "Zero"; break;
        case num::one: os << "One"; break;
        case num::two: os << "Two"; break;
        case num::three: os << "Three"; break;
        // and so on
    }
    return os;
}

int main()
{
    std::cout << num::two; // prints "Two"
}

Of course, the specific char values of the enum instances have now become pretty useless, so you may as well get rid of them completely:

enum class num : char {
    zero,
    one,
    two,
    three,
    four,
    five,
    six
};

This may strike you as strange, but keep in mind that an enum which represents nothing but generic numbers from zero to six is not a realistic use case.



回答4:

Because the two call two different operator overloads:

  • the first calls the non-member operator<< for std::ostream and char. This prints the character.

  • The second example calls the member operator<< for int due to integer promotions, as explained by other answers.



回答5:

The reason is that your enum : char is not the same as char (which is exactly what we want - we don't want enum to be the same as other types, even if they are assignment compatible - we want void func(num n) to be distinct from void func(char n), right?).

So, since enum num isn't a char, the operator<<(int) will be used, and prints the integer value, even if the underlying type is char. Not entirely sensible, but I'm sure this is what happens.