最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Is there a way in C++ to have a scoped enum containing multiple representations? - Stack Overflow

programmeradmin2浏览0评论

Is there a way to declare an enumeration that has multiple representations, for example a numeric and a string?

The intent here is to create an enum class with multiple values, each of which has 2 different representations, a numeric and a string. Either representation is valid and unique across the enumeration.

A private constructor is included to prevent the addition of new enumerated values.

The attributes numericCode and alphaCode represent the 2 representations of the enum.

    enum class Foo {
        Value01 { 001, "aa" },
        Value02 { 002, "bb" },
         .....
        ValueN. { nn, "zz" };
    private:
        Foo( int numeric, const string alpha ) : numericCode( numeric ), alphaCode( alpha ) {}
    private:
        int    numericCode;
        string alphaCode;
    };

Rationale for this is to avoid a common construct of performing string or numeric comparisons for when determining an enumerated case; avoid multiple symbols for magic numbers across situationally convenient compilation units, or hard to maintain hard coded magic numbers throughout the codebase, etc.

ISO 3166 is a real world example of this. There are multiple codes that represent a country; a 2-character alpha, a 3-character alpha, and a 3-digit number. Each code uniquely represents a specific enumerated value, while multiple representations may uniquely identify a single enumerated value.

This is the goal, but in Java:

public enum Foo {
    Value01 ( 001, "aa" ),
    Value02 ( 002, "bb" ),
    ValueNN ( 999, "zz" );

    private final int    numericCode;
    private final String alphaCode;

    Foo( final int numeric, final String alpha ) {
        numericCode = numeric;
        alphaCode   = alpha;
    }

    public String getAlpha() {
        return alphaCode;
    }

    public int getNumeric() {
        return numericCode;
    }
}

I attempted the pseudo code in the question. I have implemented this in Java a number of times.

Is there a way to declare an enumeration that has multiple representations, for example a numeric and a string?

The intent here is to create an enum class with multiple values, each of which has 2 different representations, a numeric and a string. Either representation is valid and unique across the enumeration.

A private constructor is included to prevent the addition of new enumerated values.

The attributes numericCode and alphaCode represent the 2 representations of the enum.

    enum class Foo {
        Value01 { 001, "aa" },
        Value02 { 002, "bb" },
         .....
        ValueN. { nn, "zz" };
    private:
        Foo( int numeric, const string alpha ) : numericCode( numeric ), alphaCode( alpha ) {}
    private:
        int    numericCode;
        string alphaCode;
    };

Rationale for this is to avoid a common construct of performing string or numeric comparisons for when determining an enumerated case; avoid multiple symbols for magic numbers across situationally convenient compilation units, or hard to maintain hard coded magic numbers throughout the codebase, etc.

ISO 3166 is a real world example of this. There are multiple codes that represent a country; a 2-character alpha, a 3-character alpha, and a 3-digit number. Each code uniquely represents a specific enumerated value, while multiple representations may uniquely identify a single enumerated value.

This is the goal, but in Java:

public enum Foo {
    Value01 ( 001, "aa" ),
    Value02 ( 002, "bb" ),
    ValueNN ( 999, "zz" );

    private final int    numericCode;
    private final String alphaCode;

    Foo( final int numeric, final String alpha ) {
        numericCode = numeric;
        alphaCode   = alpha;
    }

    public String getAlpha() {
        return alphaCode;
    }

    public int getNumeric() {
        return numericCode;
    }
}

I attempted the pseudo code in the question. I have implemented this in Java a number of times.

Share Improve this question edited Feb 16 at 0:20 Remy Lebeau 597k36 gold badges500 silver badges844 bronze badges asked Feb 15 at 18:56 Kevin HannanKevin Hannan 1011 silver badge7 bronze badges New contributor Kevin Hannan is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct. 3
  • 2 C++ enums (scoped or not) do not support what you ask for out-of-the-box. You will need to implement some infrastructure. You can start with a class for a pair of int and string. Then maybe have an array of all possible values. Finally have a class that supplies the interface you need for using these pre-defined pairs (the interface should contain all the methods you have in mind for this "enum"). – wohlstad Commented Feb 15 at 19:02
  • 1 001, 002 are octal numbers. Isn't that true in Java? – 3CxEZiVlQ Commented Feb 15 at 19:23
  • 1 C, C++, Java, etc do in fact use a leading zero to denote Octal. Those were more to illustrate that in ISO 3166-1 the numeric representation of a country code is a 3 digit number. It was unintentional to cloud the issue with octal representations. – Kevin Hannan Commented Feb 15 at 22:01
Add a comment  | 

8 Answers 8

Reset to default 6

The thing you want to achieve does not work with enums in C++. An enum in C++ is just an integer, which has symbolic names for the different values it can take.

You can create something that behaves essentially the same as Java's enum like this:

// Put this is a header file
struct FooValue_ {
  int numeric;
  const char* text;
private:
  constexpr FooValue_(int n, const char* s) : numeric(n), text(s) {}
  FooValue_(const FooValue_&) = delete;
  friend class Foo;
};

class Foo {
public:
  typedef const FooValue_* Value;
private:
  static const constexpr FooValue_ Value1_{1, "aa"};
  static const constexpr FooValue_ Value2_{2, "bb"};
public:
  static const constexpr Value Value1 = &Value1_;
  static const constexpr Value Value2 = &Value2_;
};

// put this in a source file (and include the header file)
#include <ostream>   // operator<< for std::ostream
#include <iostream>  // std::cout

int main(void)
{
  Foo::Value v1 = Foo::Value1;
  Foo::Value v2 = Foo::Value2;
  std::cout << v1->numeric << ' ' << v2->text << '\n';

  // switch via unique numeric member
  switch (v1->numeric)
  {
    case Foo::Value1->numeric:
      std::cout << "Is Value1\n";
      break;
    case Foo::Value2->numeric:
      std::cout << "Is Value2\n";
      break;
  }

  // if/else via identity (this is likely what happens with Java enums)
  if (v2 == Foo::Value1) {
      std::cout << "Is Value1\n";
  } else if (v2 == Foo::Value2) {
      std::cout << "Is Value2\n";
  }
}

The idea is to have the numeric and the text value grouped in a struct (the type called FooValue_), and limit the creation of instances of that struct to one specific region, namely the class Foo itself. I do this by making the constructor of FooValue_ private and making the class Foo a friend. That's also the reason why Foo needs to be a class instead of a namespace, even though no instances of foo are ever created (you might want to add a deleted constructor to Foo).

To use this "fake Java Enum", you use the address of the instances created inside the Foo class as pointer values. To hide the gory details from the user, a typedef Foo::Value is provided, and the enumerators that are exposed to the user are not the structures themselves, but pointers to them. You still see the pointer-ness of this approach by the need to use -> instead of . to access the members.

While you can't switch on this kind of "fake Java enum", you can switch either on the numeric value, or use an if/else chain, as shown in the code snippet.

Here is a std::multimap version based on what is in the question.

It provides unique values that can represent multiple string texts. All are const, so the list is immutable.

Additional methods can be added to meet requirements.

#include <map>
struct Cat {
   enum class CatSize { kitten, adult };

   Cat(CatSize const cat_size, std::string str): mCatSize(cat_size), mCatStr(std::move(str)) { }


   [[nodiscard]] auto catSize() const -> ::Cat::CatSize {
      return mCatSize;
   }
   [[nodiscard]] auto catStr() const -> std::string {
      return mCatStr;
   }

private:
   using CatList = std::multimap<CatSize, std::string>;
   CatList const mCatSizes{
      {CatSize::kitten, "kitten"},
      {CatSize::kitten, "small_cat"},
      {CatSize::adult, "adult cat"},
      {CatSize::adult, "fat cat"},
   };
   CatSize const mCatSize;
   std::string const mCatStr;
};

I am not familiar how such enums are used in Java. Limiting myself by what I can see in the question, the similarity can be achieved in C++ like

#include <iostream>

class Value {
 public:
  constexpr Value(int code, const char* alpha) : code_(code), alpha_(alpha) {}
  const char* alpha() const { return alpha_; }
  int code() const { return code_; }
  constexpr operator int() const { return code(); }

 private:
  const int code_;
  const char* const alpha_;
};

class Foo : public Value {
 public:
  constexpr Foo(const Value& v) : Value(v) {}
  using Value::alpha;
  using Value::code;
  static constexpr Value aa{1, "aa"};
  static constexpr Value bb{2, "bb"};
};

void print(Foo value) {
  std::cout << value.alpha() << "\n";
  
  /* Impossible in C++
  switch (value) {
    case Foo::aa:
      break;
  }
  */
}

int main() { print(Foo::aa); }

IMO, it looks quite awful, and using Java approaches in C++ is not worthy, it's better to design a code for C++ straightforwardly.

Enums in C and C++ are just names assigned to constants. If you want extra data associated with it, you need to create a free function which takes that enum and returns a value.

Enums cannot have member functions either, though they can have overloaded operators, so I have written code like this before:

enum class color { red, green, blue };

const char* get_color_name(color c) {
    switch (c) {
        case color::red: return "red";
        case color::green: return "green";
        case color::blue: return "blue";
        default: return nullptr;
     }
}

const char* operator*(color c) {
    return get_color_name(c);
}

static_assert(*color::red == "red"sv);

You should consider though if such a shorthand will be useful enough to offset potential confusion of anyone who stumbles on it the first time, as is the general case for operator overloading. Unary operator* is probably safest for such use.

Personally given the problem statement, if you need to ensure and keep the code clean and readable while supporting code generation, then there seems to be only one simple trick:-

  1. Using Code Generation to Generate the Code
  2. Having a dedicated function to obtain the numeric code
  3. Note that the Code Generation is perfectly optional and ideally only necessary if and only if the enum is to change on a regular basis. Otherwise, the same class can be maintained manually as well
namespace Foo{
        enum class Foo{
                Value01,
                Value02 ,
        };
        static constexpr auto GetNumericCode(Foo const foo){
                switch(foo){
                        case Foo::Value01:
                                return 1;
                        case Foo::Value02 :
                                return 2;
                }
        }
}

The above is an example of code-gen'd code. It can be manually maintained of course

Below is the example of usage

int main()
{
    Foo::Foo foo = Foo::Foo::Value01;
    switch(foo)
    {
        case Foo::Foo::Value01:
            return GetNumericCode(foo);
    }
}

The code used to generate it:-

from dataclasses import dataclass
from typing import Sequence


@dataclass
class Foo:
    key: str
    numericCode: int
    alphaCode: str


def generate(foos: list[Foo]):
    output = "namespace Foo{\n"
    output += "\tenum class Foo{\n"
    for foo in foos:
        output += "\t\t" + foo.key + ",\n"
    # Remove the last comma
    output = output.rstrip(",\n")
    output += "\n"
    output += "\t};\n"

    # Create the Numeric Code function
    output += "\tstatic constexpr auto GetNumericCode(Foo const foo){\n"
    output += "\t\tswitch(foo){\n"
    for foo in foos:
        output += "\t\t\tcase Foo::" + foo.key + ":\n"
        output += "\t\t\t\treturn " + str(foo.numericCode) + ";\n"
    output += "\t\t}\n"
    output += "\t}\n"
    output += "}\n"
    return output


if __name__ == "__main__":
    print(
        generate(
            foos=[
                Foo(key="Value01", numericCode=1, alphaCode="aa"),
                Foo(key="Value02", numericCode=2, alphaCode="bb"),
            ]
        )
    )

It could be made a lot nicer using Jinja and f-strings of course.

Personally I would recommend code generation only when there are going to be a lot of enums like this

So lets discuss the negatives of the approach:-

  1. It does have an enum but accessing a value means having to use the helper function. This is on account of enums not supporting helper functions nor do I know of compilers implementing the same as a language extension
  2. Manual maintenance is going to be difficult which is why I recommended code generation in the first place.
    Code Generation will only be worthwhile if there are a lot of examples as such.
  3. If the enums are to not be regularly worked upon, you could manually generate the class in this design or use code-generation to write the boiler plate for you and reduce risk of bugs. But future maintenance will not be easy.

You are massively overthinking and over-engineering it.

Make a clean cut between efficient canonical internal representation (an enum) and numerous / less efficient / ambiguous external representation(s).

For that, you need a single central definition (single source of truth) of the enum, and the requisite set of encoding/decoding functions for conversion.

Also, don't try to code $language in C++.

TL;DR: No, but close enough.

First of all, no, an enum in C++ is not like one in Java.

In C++, or in C, an enum is simply an integral in fancy clothing. In C++, you even get the pick in the underlying integral type. But that's it.

On the other hand, mapping from a contiguous integer range [0, N) to a value is very efficient, while the reverse can get a bit more complicated, so you can still use a C++ enum as the core representation, and derive various mappings (from to integral code, from to short-string, from to long-string) as necessary.

Quick & Dirty: X-Macros

If you've never heard of them, you may want to take a look at X-Macros:

#define COUNTRIES(M) \
    M(Afghanistan,    4, AF, AFG) \
    M(AlandIslands, 284, AX, ALA) \
    ...

The macro is itself invoked with a macro, which takes 4 arguments, and does something with them (though perhaps not them all):

//  Don't fet the comma at the end.
#define COUNTRIES_DEF_ENUM(Enumerator_, Num_, Short_, Long_) Enumerator_,

enum class CountryCode: std::uin16_t {
    COUNTRIES(COUNTRIES_DEF_ENUM)
};

And of course, the macro can be invoked with a LOT more of different macros:

#define COUNTRIES_CASE_TO_NUM(Enumerator_, Num_, Short_, Long_) \
    case Enumerator_: return Num_;

//  Wouldn't have to be inlined, but constexpr is cool!
inline constexpr auto to_numeric_code(CountryCode code) -> std::uint16_t {
    //  Could also be an array indexed by `code`.
    switch code {
        COUNTRIES(COUNTRIES_CASE_TO_NUM)
    }
}

X-Macros are really quite useful... though they may not be as good for reverse mapping. Mapping an "arbitrary" integer or string to an enum value semi efficiently is relatively easy: switch for an integer, static const hash-map for a string. There's more efficient ways, if performance is a concern, such as using a Perfect Hash Function (PHF), a switch over string prefixes, etc... however computing those requires full-blown code generation, not just dirty macro hacks.

Not everything that is enumerable should be an enum

For the specific case of ISO Country Codes, however... I would NOT recommend an enum.

Code is good for encoding structure, but not so much for encoding data: because data changes.

An ISO Country Code will always be an ISO Country Code, it's certainly worth a class. But the exact list of ISO Country Codes? Well, that changes over time.

Do you want the current list? The list 50 years ago? A merged list of everything from 50 years ago to now?

Even if your business requirements only care about the most current list, there's an advantage to defining that list in a configuration file. In fact, even if said configuration file is bundled straight into the binary (with #embed or equivalent), it'll still save build time. And it'll make loading the definition dynamically (later on) easier too.

In core C++ an enum cannot store multiple representations both numeric and string values. You need to take alternative approaches to acheive it. like write a Function to Map Enum to String or have a struct in enum. Here are some alternate solutions:

  1. Use a Struct with an Enum and a String

    struct EnumEntry { enum class Code { Success, Failure, Pending }; Code code; const char* name; };

    static const std::vector statusList = { { EnumEntry::Code::Success, "Success" }, { EnumEntry::Code::Failure, "Failure" }, { EnumEntry::Code::Pending, "Pending" } };

    std::string getEnumName(EnumEntry::Code code) { for (const auto& entry : statusList) { if (entry.code == code) return entry.name; } return "Unknown"; }

  2. Use a Helper Function to Map Enum to String

    enum class Status { Success, Failure, Pending };

    std::string toString(Status status) { switch (status) { case Status::Success: return "Success"; case Status::Failure: return "Failure"; case Status::Pending: return "Pending"; default: return "Unknown"; } }

  3. Use a Class with Overloaded Operators

    class Status { Code code; public: enum class Code { Success, Failure, Pending };

     Status(Code c) : code(c) {}
    
     std::string toString() const {
         static const std::unordered_map<Code, std::string> names = {
             {Code::Success, "Success"},
             {Code::Failure, "Failure"},
             {Code::Pending, "Pending"}
         };
         return names.at(code);
     }
    
     int toInt() const {
         return static_cast<int>(code);
     }
    

    };

发布评论

评论列表(0)

  1. 暂无评论