【问题标题】:Derive from std:string to add typedefs and enums从 std:string 派生以添加 typedef 和枚举
【发布时间】:2015-07-05 14:23:53
【问题描述】:

对于客户端和服务器之间交换的 UDP 包,我想支持两种字符串字段:

  • 以空结尾的 c 字符串命名为 cstring
  • 字符串前面有uint8_t 大小字段名为vstring

为了自我记录我们包的布局,我想使用简单的结构声明:

struct ABC {
  vstring a;
  cstring b;
}

并在反序列化函数中调用重载函数get(char*, vstring& v)get(char*, cstring&),如下所示:

void deserialize(const char* bytes, ABC& msg) {
  get(msg.a);
  get(msg.b);
}

void serialize(char* bytes, const ABC& msg) {
  put(msg.a);
  put(msg.b);
}

但是,对于用户 vstringcstring 来说,理想情况下应该像正常的 std::string 一样工作。

我的第一个想法是简单地将std::string 设为vstringcstring 的公共基础,以便在重载解析期间可以区分这两个类,但对用户而言行为相同。但由于不鼓励从std::string 派生,我不确定该怎么做。

【问题讨论】:

  • 我不太明白你为什么需要新类型?为什么string 不好?它公开了const char* 并具有size() 方法,因此您似乎在复制功能。你能澄清一下吗?这有点像 XY 问题。
  • 交易是你必须序列化 vstringcstring 因为通过网络发送对象并不总是有效。
  • 从技术上讲,在我的 serialize()deserialize() 函数中,我只需要 vstringcstring 是不同的类型。对于struct ABC 的用户来说没关系。
  • 为什么不使用合成?

标签: c++ c++11 inheritance stdstring


【解决方案1】:

std::string派生的危险在于析构函数不是虚拟的,所以有人可能会这样做:

std::string* p = new vstring;
delete p;

你会有未定义的行为。如果你认为这样的代码没有机会在你的环境/系统中编写,那就把自己搞砸并从 std::string 派生。

指南:

  • 如果 vstringcstring 类仅在非常有限的受控环境中使用 - 最好由一个或少数开发人员与您沟通预期 - 并监控实际 -使用,或者很明显不会滥用动态分配和多态性来处理它们,一切都很好。

  • 在另一个极端 - 如果它们在您的接口中与未指定数量的不受控制的代码,甚至通知所有潜在的客户开发人员这个问题是不切实际的 - 这不好,而且是一个场景在这种情况下,您不应该从没有虚拟析构函数的类型派生。


也就是说,你真的需要在类型中编码序列化样式吗?无论如何,他们必须编写序列化和反序列化例程,列出要序列化的字段,并且他们也可以在其中指定格式(NUL 终止与长度前缀或其他)。

【讨论】:

  • 那么,如果我要向struct vstring 添加一个自定义析构函数,显式调用基类析构函数,我会安全吗?
  • @Arne 不——这并不容易。如果直接调用基类的析构函数(例如使用delete p;,如我的示例代码)。 C++ 提供了虚拟分派,让基类在这种情况下支持自定义销毁,但是一旦编写了基类,您就不能回顾性地使函数虚拟化。请注意,如果 vstringABC 的成员、堆栈上的对象等 - 在编译时就知道调用 vstring 析构函数的需要:没问题。
  • @Arne 我比托尼迟到了 4 秒。我将在 C++ 中通常惯用地添加任何预期从 should 派生的类定义一个虚拟析构函数。即使是一个空的,只是作为一种预防措施。否则,即使您始终可以将Derived 转换为Base,您也无法在标准容器中安全地混合BaseDerived 的对象,如果Derived 具有析构函数,则不会在容器运行时调用它们超出范围。
  • 我将添加到 luk'32 评论中,尽管从 OO 正确性的角度来看它很有吸引力,但 C++ 没有将虚拟析构函数设为默认值是有充分的实际原因的:虚拟调度有性能开销,防止按位复制/移动,每个对象需要一个(n个额外)指针,可能会阻止或限制对象在共享内存中的放置或使用指向类对象的指针处理已知地址处的现有数据等。所以,C++ 让程序员选择。
【解决方案2】:

从思想上讲,序列化的方式不应该影响数据的类型,所以我最好是这样:

void serializeNullTerminated(char* bytes, const std::string& msg);
void deserializeNullTerminated(const char* bytes, std::string& msg);

void serializeWithSize(char* bytes, const std::string& msg);
void deserializeWithSize(const char* bytes, std::string& msg);

或者给函数传递一个额外的参数:

void serialize(SerializationType st, char* bytes, const std::string& msg);
void deserialize(SerializationType st, const char* bytes, std::string& msg);

或者您可以将它们设为模板:

template<SerializationType st>
void serialize(char* bytes, const std::string& msg);
template<SerializationType st>
void deserialize(const char* bytes, std::string& msg);

关键是,用户不必处理不同类型的字符串,在他们的代码中,他们只需要选择序列化/反序列化的方式。

【讨论】:

    【解决方案3】:

    您必须将计算与消息分离。所以在你的程序中你使用std::string,当你需要发送/接收字符串时,你传递的消息符合你的协议。

    所以你的库/应用程序/任何东西的唯一公共接口应该是这样的:

    int send_message(std::string const &s, int mode, ... destination ... etc);
    int receive_message(std::string &s, int mode, ... source ... etc);
    

    (对于schar *c(空终止)可能重载)。

    mode 是 MODE_CSTRINGMODE_VSTRING 的标志。

    您在内部发送/接收:

    • 空终止字符的向量或
    • 字符的大小和向量。

    您甚至不需要为cstringvstring 创建类。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-01-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多