Correct way to use enums with pass-by-value and re

2019-05-11 01:53发布

I understand the question title is mightily vague, hence the body text :)

I have several enums I use for identifying file types and other stuff that needs easy differentiating. My former approach was this:

namespace my_namespace
{
    namespace fileType
    {
        enum fileType
        {
            SOURCE,
            HEADER,
            RESOURCE
        };
    }
}
using namespace my_namespace::fileType;

Which allowed me to define a function:

fileType someFunction( const std::string &someFile, const fileType someType )
{
    //...
    return fileType::HEADER;
}

And I could define and compare variables like this:

fileType type = fileType::SOURCE;

Which is awesome. Though there were some caveats. Headers (without any using directives) required doubling the intended enum name to let the compiler know you're using the type, not namespace:

my_namespace::fileType::fileType soeFunction( const std::string &someFile, const my_namespace::fileType::fileType someType );

Which does look silly, is hard to read and painful to understand. Additionally, MSVC complains at warning level one about a non-standard extension used (due to the doule fileType in the example). Strange that GCC does not complain at the strictest settings, but hey, that's a different story.

I now want to rewrite my enums in a way that they are (anonymously) enclosed in a struct instead of a namespace, allowing for the single qualification when declaring functions, thus shutting up MSVC's warning. But How do I write the return statement in this case. Is it absolutely necessary to provide a constructor/conversion operator or is there a way around this I did not see?

Example:

// enum definition
namespace my_namespace
{
    struct fileType
    {
        enum
        {
            SOURCE,
            HEADER,
            RESOURCE
        };
    }
}
using my_namespace::fileType;

// function declaration in header
my_namespace::fileType someFunction( const std::string &s, const my_namespace::fileType type );

// function implementation in .cpp file
using my_namespace::fileType;

fileType someFunction( const string &s, const fileType type )
{
    //...(problem is situated below)
    return fileType::SOURCE;
}

This illustrated what I'd like to do. I'd like to avoid explicitely calling the enum struct's constructor: fileType(fileType::SOURCE) which would leave me with a double fileType use.

Thanks for the help!

PS: if this question has been answered before, I apologize, but I didn't find a good alternative with google or on SO's previous questions on this subject.

3条回答
做个烂人
2楼-- · 2019-05-11 02:27

From my point of view having a namespace or a struct that does not contain anything but a single enum has already proven that namespace/struct to be useless. Thus you should simply drop it.

You should try if

namespace my_namespace
{
    enum fileType
    {
        SOURCE,
        HEADER,
        RESOURCE
    };
}

suits you better. Often enough though in that case programmers like to prefix the actual enum values with the name of the enum thus resulting in

namespace my_namespace
{
    enum fileType
    {
        FILE_TYPE_SOURCE,
        FILE_TYPE_HEADER,
        FILE_TYPE_RESOURCE
    };
}

here you may even choose to rename the enum itself to

    enum FILE_TYPE
    { ...

Also enums are better used for group of values that are in itself incomplete and never to be extended in the future, e.g. an enum for weekdays.
If you consider that an enum fileType is likely to be eternally incomplete (since there are hundreds of files types out there) you may want to take yet another approach - use constant groups. Using constant groups has the advantage over enum that if you extend that constant group you need only recompile modules/files that make use of the new added values, whereas if you add a value to an enum you may be required to recompile all sources that uses that enum.

A constant group may look like this:

namespace nms
{
    typedef const unsigned short fileType_t;
    namespace fileType
    {
        fileType_t SOURCE   = 1;
        fileType_t HEADRE   = 2;
        fileType_t RESOURCE = 3;
    }
}

Of course if you prefer you can move the typedef statement into the fileType namespace or get rid of it completely and use 'const unsigned short' directly everywhere.

Since I don't have compiler at home the above code might not compile from scratch though. But I'm confident that you'll get the idea.

With this approach your source code should read like this:

using nms;

nms::fileType_t someFunction( const string &s, const nms::fileType_t type )
{
    return nms::fileType::SOURCE;
}
查看更多
SAY GOODBYE
3楼-- · 2019-05-11 02:34

True, if you want different enums in the same scope to have a 'last' member it can not be done. In that case you would need to prefix that 'last' value with the name of the enum for example (similar to the second enum example.

As for having a 'last' entry for argument checking I may have missed your point. For example if you pass the enum as argument to a function e.g.

void f( enum fileType type )  

It does not provide more 'type safety' than fileType has without a 'last' element.

The only reason I can see for a last element (and then for a first element as well!) is if you do want to iterate over the elements of the enum elements in a loop. For example:

enum E
{
    first,
    e1 = first,
    e2,
    e3,
    last
}  

for (int i = first; i < last; ++i)
    ...  

if that is the case isn't it rather artificial to want to have two different enums have the same named 'last' element listed? Where is the advantage of having one 'last' in two different enums?
Does it improve readability compared to e.g. having E_last and F_last?
Or does it help to save writing even a single line of code?
Also what is using 'last' in scoped enums for? Scoped enums do not convert to int by default. (But maybe I misunderstood what you wanted to do...)

About "you can't keep the enum values short..." I'm afraid I did not get what you wanted to say here. Was it about me using 'const unsigned short' in the constant group example?

Also don't get me wrong, in your case (wanting to handle file types) I advocated to use constant groups. And with them it is easy and natural to use the same 'first' and 'last' member (but with different values) in different groups. See

namespace nms
{
    typedef const unsigned short G1_t
    namespace G1
    {
        G1_t  first = 1;
        G1_t  type1 = 1;
        G1_t  type2 = 2;
        G1_t  type3 = 3;
        G1_t  last  = 3;
    }

    typedef const long G2_t
    namespace G2
    {
        G1_t  first = -1;
        G1_t  obj1  = -1;
        G1_t  obj2  =  0;
        G1_t  obj3  =  1;
        G1_t  last  =  1;
    }
}

As for 'namespace - namespace - enum' I dislike the additional indirection without having substantial benefits. As for 'namespace - struct/class - enum', that is more philosophical:

C is, so to speak, a slick and fast motorbike that made writing assembler code virtually unnecessary. This is also pretty much owned to the C cast operator which allows to completely drop type safety and do what you want without getting the compiler in your way.

C++ still likes to be a super set of C. It added classes and object oriented programming but still wanted to keep the motorbike. Which was a good. Now adding more 'type safe' scoped enums while still keeping cast operators looks to me like wanting to add airbags and safety belts to the motorbike.

I am not against revising enums to get better enums (if needs really be), but if one does so one should also occasionally (e.g every 5 years) drop the stuff now recognized as old, outdated and evil. Otherwise the language just gets more and more cluttered and ever so harder to learn by beginners. And like every tool programming languages need to be easy to use and that includes not having a dozen ways to do the same thing.

查看更多
beautiful°
4楼-- · 2019-05-11 02:40

Personally I use a very simple trick:

struct EnumName
{
  enum type {
    MemberOne,
    MemberTwo,
    ...
  };
};

typedef EnumName::type EnumName_t;

// Usage
EnumName_t foo = EnumName::MemberOne;

In C++0x, you can have scoped enumerators directly:

enum struct EnunName // note: struct and class equivalent here
{
  MemberOne,
  MemberTwo,
  ...
};

// Usage
EnumName foo = EnumName::MemberOne;

Which will be really great :)

Note: scoped enum are also not subject to integral promotion, which is really great

查看更多
登录 后发表回答