C++ のどの Typesafe Enum を使用していますか?
-
03-07-2019 - |
質問
C++ の組み込み列挙型がタイプセーフではないことは周知の事実です。タイプセーフ列挙型を実装するクラスがどのクラスで使用されているのか疑問に思っていました...私自身も次の「自転車」を使用していますが、やや冗長で限定的です。
typesafeenum.h:
struct TypesafeEnum
{
// Construction:
public:
TypesafeEnum(): id (next_id++), name("") {}
TypesafeEnum(const std::string& n): id(next_id++), name(n) {}
// Operations:
public:
bool operator == (const TypesafeEnum& right) const;
bool operator != (const TypesafeEnum& right) const;
bool operator < (const TypesafeEnum& right) const;
std::string to_string() const { return name; }
// Implementation:
private:
static int next_id;
int id;
std::string name;
};
typesafeenum.cpp:
int TypesafeEnum::next_id = 1;
bool TypesafeEnum::operator== (const TypesafeEnum& right) const
{ return id == right.id; }
bool TypesafeEnum::operator!= (const TypesafeEnum& right) const
{ return !operator== (right); }
bool TypesafeEnum::operator< (const TypesafeEnum& right) const
{ return id < right.id; }
使用法:
class Dialog
{
...
struct Result: public TypesafeEnum
{
static const Result CANCEL("Cancel");
static const Result OK("Ok");
};
Result doModal();
...
};
const Dialog::Result Dialog::Result::OK;
const Dialog::Result Dialog::Result::CANCEL;
追加:要件をもっと具体的にするべきだったと思います。それらを要約してみます。
優先度 1:enum 変数を無効な値に設定することは、例外なく不可能 (コンパイル時エラー) である必要があります。
優先度 2:enum 値と int 間の変換は、1 回の明示的な関数/メソッド呼び出しで可能である必要があります。
優先度 3:可能な限りコンパクトでエレガントかつ便利な宣言と使用法
優先度 4:enum 値と文字列の変換。
優先度 5:(あれば便利です) enum 値を反復処理する可能性。
解決
現在、 Boost Vault (ファイル名enum_rev4.6.zip
)。 Boostに含めるために公式に提出されたことはありませんが、そのまま使用できます。 (ドキュメントはありませんが、明確なソースコードと適切なテストで補われています。)
Boost.Enumを使用すると、次のような列挙型を宣言できます。
BOOST_ENUM_VALUES(Level, const char*,
(Abort)("unrecoverable problem")
(Error)("recoverable problem")
(Alert)("unexpected behavior")
(Info) ("expected behavior")
(Trace)("normal flow of execution")
(Debug)("detailed object state listings")
)
そして自動的にこれに展開します:
class Level : public boost::detail::enum_base<Level, string>
{
public:
enum domain
{
Abort,
Error,
Alert,
Info,
Trace,
Debug,
};
BOOST_STATIC_CONSTANT(index_type, size = 6);
Level() {}
Level(domain index) : boost::detail::enum_base<Level, string>(index) {}
typedef boost::optional<Level> optional;
static optional get_by_name(const char* str)
{
if(strcmp(str, "Abort") == 0) return optional(Abort);
if(strcmp(str, "Error") == 0) return optional(Error);
if(strcmp(str, "Alert") == 0) return optional(Alert);
if(strcmp(str, "Info") == 0) return optional(Info);
if(strcmp(str, "Trace") == 0) return optional(Trace);
if(strcmp(str, "Debug") == 0) return optional(Debug);
return optional();
}
private:
friend class boost::detail::enum_base<Level, string>;
static const char* names(domain index)
{
switch(index)
{
case Abort: return "Abort";
case Error: return "Error";
case Alert: return "Alert";
case Info: return "Info";
case Trace: return "Trace";
case Debug: return "Debug";
default: return NULL;
}
}
typedef boost::optional<value_type> optional_value;
static optional_value values(domain index)
{
switch(index)
{
case Abort: return optional_value("unrecoverable problem");
case Error: return optional_value("recoverable problem");
case Alert: return optional_value("unexpected behavior");
case Info: return optional_value("expected behavior");
case Trace: return optional_value("normal flow of execution");
case Debug: return optional_value("detailed object state listings");
default: return optional_value();
}
}
};
リストした5つの優先順位すべてを満たします。
他のヒント
良い妥協方法はこれです:
struct Flintstones {
enum E {
Fred,
Barney,
Wilma
};
};
Flintstones::E fred = Flintstones::Fred;
Flintstones::E barney = Flintstones::Barney;
バージョンと同じ意味でタイプセーフではありませんが、使用方法は標準の列挙型よりも優れており、必要に応じて整数変換を利用できます。
C ++ 0x型保証列挙を使用しています。 to / from文字列機能を提供するヘルパーテンプレート/マクロを使用します。
enum class Result { Ok, Cancel};
しない。オーバーヘッドが多すぎて、ほとんどメリットはありません。また、シリアル化のために列挙を異なるデータ型にキャストできることは非常に便利なツールです。 <!> quot; Type safe <!> quot;のインスタンスを見たことはありません。列挙は、C ++がすでに十分な実装を提供しているオーバーヘッドと複雑さの価値があります。
私の考えは、あなたが問題を発明し、その解決策を当てはめているということです。値の列挙のために複雑なフレームワークを行う必要はないと思います。特定のセットのメンバーのみに値を設定することに専念している場合は、一意のセットデータ型のバリアントをハッキングできます。
私は個人的に、 typesafe enum イディオム. 。あなたが編集で述べた 5 つの「要件」すべてが提供されているわけではありませんが、いずれにせよ、そのうちのいくつかには強く反対します。たとえば、Prio#4 (値の文字列への変換) がタイプ セーフとどのように関係しているのかわかりません。ほとんどの場合、個々の値の文字列表現は型の定義とは別にする必要があります (理由は簡単ですが、i18n を考えてください)。Prio#5 (オプションの iteratio) は私が見てみたい最も素晴らしいものの 1 つです 当然 列挙型で起こっているため、リクエストで「オプション」として表示されるのは悲しいですが、それは、 個別の反復システム のような begin
/end
関数または enum_iterator を使用すると、STL および C++11 の foreach とシームレスに連携できます。
OTOH この単純なイディオムは、ほとんどラップのみであるという事実のおかげで、Prio#3 Prio#1 をうまく提供します。 enum
s には、より多くの型情報が含まれます。言うまでもなく、これは非常にシンプルなソリューションであり、ほとんどの場合、外部依存関係ヘッダーを必要としないため、持ち運びが非常に簡単です。また、列挙を C++11 のスコープにできるという利点もあります。
// This doesn't compile, and if it did it wouldn't work anyway
enum colors { salmon, .... };
enum fishes { salmon, .... };
// This, however, works seamlessly.
struct colors_def { enum type { salmon, .... }; };
struct fishes_def { enum type { salmon, .... }; };
typedef typesafe_enum<colors_def> colors;
typedef typesafe_enum<fishes_def> fishes;
このソリューションが提供する唯一の「穴」は、それが防止できないという事実に対処していないことです。 enum
さまざまなタイプの (または enum
値を直接使用すると、暗黙的な変換が強制的に行われるため、int) が直接比較されなくなります。 int
:
if (colors::salmon == fishes::salmon) { .../* Ooops! */... }
しかし、これまでのところ、そのような問題は、コンパイラに対してより適切な比較を提供するだけで解決できることがわかりました。たとえば、2 つの異なる値を比較する演算子を明示的に提供するなどです。 enum
タイプして強制的に失敗させます。
// I'm using backports of C++11 utilities like static_assert and enable_if
template <typename Enum1, typename Enum2>
typename enable_if< (is_enum<Enum1>::value && is_enum<Enum2>::value) && (false == is_same<Enum1,Enum2>::value) , bool >
::type operator== (Enum1, Enum2) {
static_assert (false, "Comparing enumerations of different types!");
}
今のところコードが壊れているようには見えませんし、他のことをせずに特定の問題に明示的に対処していますが、そのようなことが本当に問題なのかどうかはわかりません。」すべき" します (干渉するのではないかと思います) enum
■ 他の場所で宣言された変換演算子にすでに参加している。これについてのコメントを喜んで受け取ります)。
これを上記のタイプセーフなイディオムと組み合わせると、比較的 C++11 に近いものになります。 enum class
あまりわかりにくいことをすることなく、人間性 (読みやすさと保守性) を向上させることができます。そして、それが楽しかったことは認めざるを得ません、実際にそんなことを考えたこともありませんでした 聞く 私が扱っていた場合はコンパイラ enum
そうかどうか…
Java enum
は従うべき良いモデルだと思います。基本的に、Javaフォームは次のようになります。
public enum Result {
OK("OK"), CANCEL("Cancel");
private final String name;
Result(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
Javaアプローチで興味深いのは、OK
とCANCEL
が不変で、Result
のシングルトンインスタンスであるということです(表示されるメソッドを使用)。 EnumSet
のインスタンスをさらに作成することはできません。それらはシングルトンなので、ポインタ/参照で比較できます----非常に便利です。 :-)
ETA:Javaでは、手動でビットマスクを行う代わりに、Set
を使用してビットセットを指定します(<=>インターフェイスを実装し、セットのように動作しますが、ビットマスクを使用して実装されます)。手書きのビットマスク操作よりもはるかに読みやすい!
このこちら、別のトピック。これは異なるスタイルのアプローチであり、元の列挙定義を変更することなく同じ機能のほとんどを可能にします(したがって、列挙を定義しない場合でも使用できます)。また、実行時の範囲チェックも可能です。
私のアプローチの欠点は、enumクラスとヘルパークラス間の結合をプログラムで強制しないため、並行して更新する必要があることです。それは私のために動作しますが、YMMV。
現在、 https://bitbucket.org/chopsii/typesafe-で独自のタイプセーフ列挙ライブラリを書いています。列挙
私はこれまでで最も経験のあるC ++開発者ではありませんが、BOOST vault列挙型の欠点のためにこれを書いています。
自由にチェックアウトして自分で使用してください。ただし、いくつかの(できればマイナーな)ユーザビリティの問題があり、おそらくクロスプラットフォームではありません。
必要に応じて貢献してください。これが私の最初のオープンソース事業です。
使用 boost::variant
!
上記のアイデアをたくさん試してみて、それらに欠けていることがわかった後、私はこのシンプルなアプローチを思いつきました。
#include <iostream>
#include <boost/variant.hpp>
struct A_t {};
static const A_t A = A_t();
template <typename T>
bool isA(const T & x) { if(boost::get<A_t>(&x)) return true; return false; }
struct B_t {};
static const B_t B = B_t();
template <typename T>
bool isB(const T & x) { if(boost::get<B_t>(&x)) return true; return false; }
struct C_t {};
static const C_t C = C_t();
template <typename T>
bool isC(const T & x) { if(boost::get<C_t>(&x)) return true; return false; }
typedef boost::variant<A_t, B_t> AB;
typedef boost::variant<B_t, C_t> BC;
void ab(const AB & e)
{
if(isA(e))
std::cerr << "A!" << std::endl;
if(isB(e))
std::cerr << "B!" << std::endl;
// ERROR:
// if(isC(e))
// std::cerr << "C!" << std::endl;
// ERROR:
// if(e == 0)
// std::cerr << "B!" << std::endl;
}
void bc(const BC & e)
{
// ERROR:
// if(isA(e))
// std::cerr << "A!" << std::endl;
if(isB(e))
std::cerr << "B!" << std::endl;
if(isC(e))
std::cerr << "C!" << std::endl;
}
int main() {
AB a;
a = A;
AB b;
b = B;
ab(a);
ab(b);
ab(A);
ab(B);
// ab(C); // ERROR
// bc(A); // ERROR
bc(B);
bc(C);
}
おそらく定型文を生成するマクロを思いつくことができるでしょう。(もしそうなら知らせてください。)
他のアプローチとは異なり、これは実際にはタイプセーフであり、古い C++ で動作します。次のようなクールなタイプも作成できます boost::variant<int, A_t, B_t, boost::none>
, たとえば、A、B、整数、または何もない値を表す場合、これはほぼ Haskell98 レベルのタイプ セーフです。
注意すべき欠点:
- 少なくとも古いブーストでは -- 私はブースト 1.33 のシステムを使用しています -- バリアント内のアイテムは 20 個に制限されています。ただし回避策はあります
- コンパイル時間に影響を与える
- 非常識なエラー メッセージ -- しかし、それはあなたにとって C++ です
アップデート
ここでは、便宜上、typesafe-enum の「ライブラリ」を示します。このヘッダーを貼り付けます。
#ifndef _TYPESAFE_ENUMS_H
#define _TYPESAFE_ENUMS_H
#include <string>
#include <boost/variant.hpp>
#define ITEM(NAME, VAL) \
struct NAME##_t { \
std::string toStr() const { return std::string( #NAME ); } \
int toInt() const { return VAL; } \
}; \
static const NAME##_t NAME = NAME##_t(); \
template <typename T> \
bool is##NAME(const T & x) { if(boost::get<NAME##_t>(&x)) return true; return false; } \
class toStr_visitor: public boost::static_visitor<std::string> {
public:
template<typename T>
std::string operator()(const T & a) const {
return a.toStr();
}
};
template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
inline static
std::string toStr(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
return boost::apply_visitor(toStr_visitor(), a);
}
class toInt_visitor: public boost::static_visitor<int> {
public:
template<typename T>
int operator()(const T & a) const {
return a.toInt();
}
};
template<BOOST_VARIANT_ENUM_PARAMS(typename T)>
inline static
int toInt(const boost::variant<BOOST_VARIANT_ENUM_PARAMS(T)> & a) {
return boost::apply_visitor(toInt_visitor(), a);
}
#define ENUM(...) \
typedef boost::variant<__VA_ARGS__>
#endif
そしてそれを次のように使用します:
ITEM(A, 0);
ITEM(B, 1);
ITEM(C, 2);
ENUM(A_t, B_t) AB;
ENUM(B_t, C_t) BC;
言わなければならないことに注意してください A_t
の代わりに A
の中に ENUM
一部の魔法を破壊するマクロ。しかたがない。また、現在、 toStr
関数と toInt
文字列と整数への単純な変換という OP の要件を満たす関数。私が理解できない要件は、項目を反復処理する方法です。このような書き方をご存知でしたら教えてください。
この投稿が遅すぎるかどうかはわかりませんが、GameDev.netには5番目のポイント(列挙子を反復処理する機能)以外のすべてを満たす記事があります。 http://www.gamedev.net/reference/snippets/features/cppstringizing/
この記事で説明されている方法では、コードを変更することなく、既存の列挙の文字列変換をサポートできます。ただし、新しい列挙のサポートのみが必要な場合は、Boost.Enum(前述)を使用します。