您在 C++ 中使用哪种类型安全枚举?
-
03-07-2019 - |
题
众所周知,C++ 中的内置枚举不是类型安全的。我想知道那里使用了哪些实现类型安全枚举的类......我自己使用以下“自行车”,但它有点冗长和有限:
类型safeenum.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:将枚举变量设置为无效值应该是不可能的(编译时错误),没有例外。
优先级 2:通过单个显式函数/方法调用应该可以将枚举值转换为 int 或从 int 转换。
优先级 3:尽可能紧凑、优雅、方便的声明和使用
优先级 4:将枚举值与字符串相互转换。
优先级 5:(很高兴拥有)可以迭代枚举值。
解决方案
我目前正在使用来自 Boost Vault 的Boost.Enum提案(文件名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();
}
}
};
它满足您列出的所有五个优先级。
其他提示
一个很好的折衷方法就是:
struct Flintstones {
enum E {
Fred,
Barney,
Wilma
};
};
Flintstones::E fred = Flintstones::Fred;
Flintstones::E barney = Flintstones::Barney;
与您的版本相同,它不是类型安全的,但使用效果比标准枚举更好,并且您仍然可以在需要时利用整数转换。
我使用 C ++ 0x typesafe enums 。我使用一些帮助模板/宏来提供来自/来自字符串的功能。
enum class Result { Ok, Cancel};
我没有。太多的开销太少了。此外,能够将枚举分类到不同的数据类型以进行序列化是一个非常方便的工具。我从未见过一个<!>“类型安全<!>”的实例。枚举将值得C ++提供足够好的实现的开销和复杂性。
我的看法是你发明了一个问题然后在其上安装解决方案。我认为没有必要为枚举值做一个精心设计的框架。如果您专用让您的值只是某个集合的成员,那么您可能会破解唯一集合数据类型的变体。
我个人正在使用改编版的 typesafe enum idiom 一>。它没有提供所有五个<!>“要求<!>”;你在编辑中已经说明了,但无论如何我强烈不同意其中的一些。例如,我没有看到Prio#4(将值转换为字符串)与类型安全性有什么关系。大多数情况下,单个值的字符串表示应该与类型的定义分开(想想i18n的原因很简单)。 Prio#5(iteratio,这是可选的)是我希望在枚举中发现自然的最好的东西之一,所以我感到很难过,它显示为<!> quot; optional <! > QUOT;在您的请求中,但似乎通过单独的迭代系统更好地解决了这个问题例如begin
/ end
函数或enum_iterator,它们使它们与STL和C ++ 11 foreach无缝协作。
OTOH这个简单的成语很好地提供了Prio#3 Prio#1,这要归功于它主要只包含更多类型信息的enum
s。更不用说它是一个非常简单的解决方案,在大多数情况下不需要任何外部依赖项头,因此它很容易随身携带。它还具有使枚举成为a-la-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;
唯一的<!>引号孔<!>该解决方案提供的是它没有解决这样一个事实:它不会阻止直接比较不同类型(或int
和int)的enum class
,因为当你直接使用值时你强制隐含转换为<=>:
if (colors::salmon == fishes::salmon) { .../* Ooops! */... }
但到目前为止,我发现只需提供一个与编译器更好的比较就可以解决这些问题 - 例如,明确提供一个比较任意两种不同<=>类型的运算符,然后强制它失败:
// 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!");
}
虽然它到目前为止似乎没有破坏代码,并且它确实明确地处理了特定问题而没有做其他事情,但我不确定这样的事情是一个<!>“ <!>应 QUOT; do(我怀疑它会干扰<=>已经参与其他地方宣布的转换运营商;我很乐意接受有关此问题的评论)。
将此与上述类型安全成语相结合,可以提供与人类(可读性和可维护性)相比接近C ++ 11 <=>的东西,而不必做任何过于模糊的事情。而且我不得不承认这很有趣,我从未想过如果我正在处理<=> s或不处理问编译器...
我认为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
指定位集(它实现<=>接口,并且像集合一样工作---但使用位掩码实现)。比手写的位掩码操作更具可读性!
我给出了答案这里,关于不同的主题。这是一种不同的方法,允许大多数相同的功能,而无需修改原始枚举定义(因此允许在未定义枚举的情况下使用)。它还允许运行时范围检查。
我的方法的缺点是它没有以编程方式强制执行枚举和辅助类之间的耦合,因此它们必须并行更新。它对我有用,但YMMV。
我目前正在 https://bitbucket.org/chopsii/typesafe-编写我自己的类型安全枚举库。枚举
我不是有史以来最有经验的C ++开发人员,但由于BOOST存储库枚举的缺点,我写这篇文章。
随意检查并自行使用它们,但它们有一些(希望是次要的)可用性问题,并且可能根本不是跨平台的。
如果您愿意,请提供帮助。这是我的第一个开源事业。
使用 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 类型安全级别。
需要注意的缺点:
- 至少对于旧的 boost - 我使用的是 boost 1.33 的系统 - 你的变体中最多只能有 20 个项目;不过有一个解决方法
- 影响编译时间
- 疯狂的错误消息——但这就是适合你的 C++
更新
为了您的方便,这里是您的类型安全枚举“库”。粘贴此标题:
#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(如上所述)。