What is the C/C++ equivalence of java.io.Serializable?
java.io.Serializable的C / C等效项是什么?
在以下位置引用了序列化库:
- 在C中序列化数据结构
还有:
- http://troydhanson.github.io/tpl/index.html
- http://www.boost.org/doc/libs/1_41_0/libs/serialization/doc/index.html
- https://developers.google.com/protocol-buffers/docs/cpptutorial#optimization-tips
但是是否存在这样的对等关系?
因此,如果我在Java中具有如下的抽象类,那么C / C中的可序列化类会是什么样?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | import java.io.Serializable; public interface SuperMan extends Serializable{ /** * Count the number of abilities. * @return */ public int countAbility(); /** * Get the ability with index k. * @param k * @return */ public long getAbility(int k); /** * Get the array of ability from his hand. * @param k * @return */ public int[] getAbilityFromHand(int k); /** * Get the finger of the hand. * @param k * @return */ public int[][] getAbilityFromFinger(int k); //check whether the finger with index k is removed. public boolean hasFingerRemoved(int k); /** * Remove the finger with index k. * @param k */ public void removeFinger(int k); } |
是否可以像Java中那样继承任何可序列化的C / C对象?
没有标准库类实现与
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | class MyType { int value; double factor; std::string type; public: MyType() : value(0), factor(0.0), type("none") {} MyType(int value, double factor, const std::string& type) : value(value), factor(factor), type(type) {} // Serialized output friend std::ostream& operator<<(std::ostream& os, const MyType& m) { return os << m.value << ' ' << m.factor << ' ' << m.type; } // Serialized input friend std::istream& operator>>(std::istream& is, MyType& m) { return is >> m.value >> m.factor >> m.type; } }; int main() { std::vector<MyType> v {{1, 2.7,"one"}, {4, 5.1,"two"}, {3, 0.6,"three"}}; std::cout <<"Serialize to standard output." << '\ '; for(auto const& m: v) std::cout << m << '\ '; std::cout <<"\ Serialize to a string." << '\ '; std::stringstream ss; for(auto const& m: v) ss << m << '\ '; std::cout << ss.str() << '\ '; std::cout <<"Deserialize from a string." << '\ '; std::vector<MyType> v2; MyType m; while(ss >> m) v2.push_back(m); for(auto const& m: v2) std::cout << m << '\ '; } |
输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Serialize to standard output. 1 2.7 one 4 5.1 two 3 0.6 three Serialize to a string. 1 2.7 one 4 5.1 two 3 0.6 three Deserialize from a string. 1 2.7 one 4 5.1 two 3 0.6 three |
序列化格式完全由程序员决定,您有责任确保要序列化的类的每个成员本身都是可序列化的(已定义了插入/提取运算符)。您还必须处理字段如何分隔(空格或换行符或零终止?)。
所有基本类型都有预定义的序列化(插入/提取)运算符,但是您仍然需要注意
对此没有统一的标准。实际上,每个库都可以以不同的方式实现它。以下是一些可以使用的方法:
-
类必须从通用基类派生并实现
read() 和write() 虚拟方法:1
2
3
4
5
6class SuperMan : public BaseObj
{
public:
virtual void read(Stream& stream);
virtual void write(Stream& stream);
}; -
类应该实现特殊的接口-在C语言中,这是通过从特殊的抽象类派生类来实现的。这是先前方法的变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Serializable
{
public:
virtual Serializable() {}
virtual void read(Stream& stream) = 0;
virtual void write(Stream& stream) = 0;
};
class SuperMan : public Man, public Serializable
{
public:
virtual void read(Stream& stream);
virtual void write(Stream& stream);
}; -
库可以允许(或要求)为给定类型注册" serializers "。可以通过从特殊的基类或接口创建类,然后将其注册为给定类型来实现它们:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21#define SUPERMAN_CLASS_ID 111
class SuperMan
{
public:
virtual int getClassId()
{
return SUPERMAN_CLASS_ID;
}
};
class SuperManSerializer : public Serializer
{
virtual void* read(Stream& stream);
virtual void write(Stream& stream, void* object);
};
int main()
{
register_class_serializer(SUPERMAN_CLASS_ID, new SuperManSerializer());
} -
序列化器也可以使用函子来实现,例如lambdas:
1
2
3
4
5
6int main
{
register_class_serializer(SUPERMAN_CLASS_ID,
[](Stream&, const SuperMan&) {},
[](Stream&) -> SuperMan {});
} -
除了将序列化程序对象传递给某些函数,还可以将其类型传递给特殊的模板函数:
1
2
3
4int main
{
register_class_serializer<SuperManSerializer>();
} -
类应提供重载的运算符,例如\\'<< \\'和\\'>> \\'。它们的第一个参数是某个流类,第二个参数是out类实例。 Stream可以是
std::stream ,但是这会导致与这些运算符的默认用法冲突-转换为用户友好的文本格式或从中转换为用户友好的文本格式。因为此流类是专用的(它可以packagestd :: stream),否则,如果还必须支持<< ,则库将支持替代方法。1
2
3
4
5
6class SuperMan
{
public:
friend Stream& operator>>(const SuperMan&);
friend Stream& operator<<(const SuperMan&);
}; -
对于我们的类类型,应该有一些专门的类模板。此解决方案可以与
<< 和>> 运算符一起使用-库首先将尝试使用此模板,如果不专门使用,则还原为运算符(可以将其实现为默认模板版本,也可以使用SFINAE)<铅>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// default implementation
template<class T>
class Serializable
{
public:
void read(Stream& stream, const T& val)
{
stream >> val;
}
void write(Stream& stream, const T& val)
{
stream << val;
}
};
// specialization for given class
template<>
class Serializable<SuperMan>
{
void read(Stream& stream, const SuperMan& val);
void write(Stream& stream, const SuperMan& val);
} -
代替类模板库也可以使用具有全局重载函数的C样式接口:
1
2
3
4
5
6
7
8
9template<class T>
void read(Stream& stream, const T& val);
template<class T>
void write(Stream& stream, const T& val);
template<>
void read(Stream& stream, const SuperMan& val);
template<>
void write(Stream& stream, const SuperMan& val);
C语言是灵活的,因此上面所列内容肯定是不完整的。我相信有可能发明另一种解决方案。
正如其他答案所提到的,C几乎不具备Java(或其他托管语言)所具有的内置序列化/反序列化功能。这部分是由于C中可用的最小运行时类型信息(RTTI)。 C本身没有反射,因此每个可序列化的对象必须完全负责序列化。在Java和C#等托管语言中,该语言包含足够的RTTI,以便外部类能够枚举对象上的公共字段以执行序列化。
幸运的是... C没有强加用于序列化类层次结构的默认机制。 (我不介意在标准库中提供由特殊基本类型提供的可选机制,但总的来说,这可能会限制现有的ABI)。
是序列化在现代软件工程中非常重要且功能强大。每当我需要在某种形式的运行时消耗性数据之间来回转换类层次结构时,都可以使用它。我一直选择的机制是基于某种形式的反思。有关此内容的更多信息。
您可能还希望在这里了解要考虑的复杂性,如果您确实想根据标准进行验证,则可以在此处购买副本。看起来下一个标准的工作草案在github上。
专用系统
C / C允许应用程序的作者自由选择人们认为较新的语言(通常是更高层次的语言)所支持的许多技术背后的机制。反射(RTTI),异常,资源/内存管理(垃圾收集,RAII等)。这些系统都可能影响特定产品的整体质量。
我从事从实时游戏,嵌入式设备,移动应用程序到Web应用程序的所有工作,并且特定项目的总体目标因人而异。
通常对于实时高性能游戏,您将显式禁用RTTI(老实说,它在C语言中不是很有用),甚至可能会异常(许多人也不希望在此处产生开销,如果您对于我来说,真的很疯狂,您可以通过跳远来实现自己的形式。对我而言,异常创建了一个看不见的接口,该接口通常会创建人们甚至都无法期望的错误,因此我总是避免使用它们,而希望使用更明确的逻辑。 )。
默认情况下,C中也不包含垃圾收集,这在实时游戏中是一件幸事。当然,您可以使用增量GC和其他一些我在许多游戏中都使用过的优化方法(通常,它是对现有GC的修改,例如Mono for C#中使用的GC)。许多游戏使用池化,并且通常使用由智能指针驱动的C RAII。拥有可以使用不同方式进行优化的不同系统,不同内存使用模式的情况并不少见。关键是某些应用程序比其他应用程序更关心细节。
类型层次结构自动序列化的一般思想
类型层次结构自动序列化系统的一般思想是使用反射系统,该系统可以在运行时从通用接口查询类型信息。我下面的解决方案依靠在宏的帮助下扩展某些基本类型的接口来构建通用接口。最后,您基本上得到了一个动态的vtable,可以通过索引或通过成员/类型的字符串名称进行查询来进行迭代。
我还使用基本的反射读取器/写入器类型,该类型公开一些iostream接口,以允许派生的格式化程序被覆盖。我目前有一个BinaryObjectIO,JSONObjectIO和ASTObjectIO,但是添加其他对象很简单。这样做的目的是负责任地从层次结构中删除对特定数据格式进行序列化,然后将其放入序列化程序中。
语言层面的反思
在许多情况下,应用程序知道要序列化哪些数据,因此没有理由将其构建到语言中的每个对象中。许多现代语言甚至在系统的基本类型中都包括RTTI(如果它们是基于类型的,则通用内在函数将是int,float,double等)。这需要为系统中的所有内容存储额外的数据,而不管应用程序的使用情况如何。我敢肯定,许多现代编译器有时都可以通过摇晃树之类的方法来优化一些,但是您也不能保证做到这一点。
声明式方法
已经提到的方法都是有效的用例,尽管它们通过使层次结构处理实际的序列化任务而缺乏灵活性。这也可以通过在层次结构上进行样板流操作使代码膨胀。
我个人更喜欢通过反思的更具声明性的方法。我过去做过的并且在某些情况下会继续做的事情是在系统中创建一个基本的Reflectable类型。我最终使用模板元编程来帮助一些样板逻辑以及字符串连接宏的预处理器。最终结果是我派生的基本类型,一个用于暴露接口的可反射宏声明,以及一个用于实现内胆的可反射宏定义(诸如将注册成员添加到该类型的查询表之类的任务。)。
所以我通常会在h:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class ASTNode : public Reflectable { ... public: DECLARE_CLASS DECLARE_MEMBER(mLine,int) DECLARE_MEMBER(mColumn,int) ... }; |
然后在cpp中这样的内容:
1 2 3 4 5 6 7 8 9 10 | BEGIN_REGISTER_CLASS(ASTNode,Reflectable); REGISTER_MEMBER(ASTNode,mLine); REGISTER_MEMBER(ASTNode,mColumn); END_REGISTER_CLASS(ASTNode); ASTNode::ASTNode() : mLine( 0 ) , mColumn( 0 ) { } |
然后我可以通过一些方法直接使用反射接口,例如:
1 2 |
但更常见的是,我只是迭代一些我用另一个接口公开的" Traits "数据:
1 | ReflectionInfo::RefTraitsList::const_iterator it = info->getReflectionTraits().begin(); |
当前traits对象看起来像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class ReflectionTraits { public: ReflectionTraits( const uint8_t& type, const uint8_t& arrayType, const char* name, const ptrType_t& offset ); std::string getName() const{ return mName; } ptrType_t getOffset() const{ return mOffset; } uint8_t getType() const{ return mType; } uint8_t getArrayType() const{ return mArrayType; } private: std::string mName; ptrType_t mOffset; uint8_t mType; uint8_t mArrayType; // if mType == TYPE_ARRAY this will give the type of the underlying data in the array }; |
我实际上对我的宏进行了改进,使我可以简化一下……但是这些取自我目前正在从事的实际项目。我正在使用Flex,Bison和LLVM开发一种可编译为C ABI和Webassembly的编程语言。我希望能够尽快对其进行开源,因此,如果您对详细信息感兴趣,请告诉我。
这里要注意的是" Traits "信息是元数据,可在运行时访问该元数据并描述该成员,并且对于一般的语言级别反射而言通常更大。我在此处包含的信息是我的可反射类型所需的全部信息。
序列化任何数据时要记住的另一个重要方面是版本信息。在开始更改内部数据结构之前,上述方法将对数据进行反序列化。但是,您可以在序列化系统中包括后期数据序列钩子机制,也可以在其之前加入序列化钩子机制,以便可以修正数据以使其符合新版本的类型。我已经用这样的设置做了几次,而且效果很好。
关于此技术的最后一点是,您在此处明确控制要序列化的内容。您可以选择要序列化的数据以及可能只是跟踪某些瞬时对象状态的数据。
C Lax保证
要注意的一件事...由于C对于数据的实际外观非常松懈。您通常必须做出一些特定于平台的选择(这可能是未提供标准系统的主要原因之一)。实际上,在使用模板元编程进行编译时,您可以做很多工作,但是有时候,假设您的
我使用的方法还进行了一些非标准的NULL指针转换来确定内存布局(出于我的目的,这也是野兽的本质)。以下是其中一种宏实现的示例代码片段,用于计算由宏提供CLASS的类型中的成员偏移量。
1 | (ptrType_t)&reinterpret_cast<ptrType_t&>((reinterpret_cast<CLASS*>(0))->member) |
关于反射的一般警告
反射的最大问题是反射的力量。您可以使用过多的反射用法来将易于维护的代码库迅速变成一团糟。
我个人为低级系统保留反射(主要是序列化),并避免将其用于业务逻辑的运行时类型检查。使用诸如虚拟功能之类的语言构造的动态调度应优先于反射类型检查条件跳转。
如果语言也继承了对反射的全部或全部支持,则问题甚至更难追踪。例如,在C#中,给定随机代码库,您不能保证仅通过允许编译器向您发出警告就不会使用该函数。您不仅可以通过代码库中的字符串或从网络数据包中调用字符串来调用该方法,还可以破坏反映在目标程序集中的其他一些不相关程序集的ABI兼容性。因此,请再次一致且谨慎地使用反射。
结论
当前没有等效于C中可序列化类层次结构的通用范例的标准,但是可以像在较新语言中看到的任何其他系统一样添加它。毕竟,所有一切最终都转化为简单的机器代码,可以用CPU芯片中包含的令人难以置信的晶体管阵列的二进制状态来表示。
我并不是说每个人都应该以任何方式将自己摆在这里。它很复杂并且容易出错。我只是真的很喜欢这个主意,无论如何现在对这种事情一直很感兴趣。我确定人们在进行此类工作时会使用一些标准的后备广告。如上所述,查找C的第一个地方将是boost。
如果您搜索" C Reflection ",您将看到几个其他人如何获得相似结果的示例。
以快速搜索为例。