【问题标题】:Inherit Without Virtual Destructor没有虚拟析构函数的继承
【发布时间】:2015-01-12 18:05:06
【问题描述】:

我有两个在项目中使用的类。一类Callback 负责保存来自回调的信息。另一个类UserInfo 是向用户公开的信息。基本上,UserInfo 应该是一个非常薄的包装器,它读取 Callback 数据并将其提供给用户,同时还提供一些额外的东西。

struct Callback {
  int i;
  float f;
};

struct UserInfo {
  int i;
  float f;

  std::string thekicker;
  void print();
  UserInfo& operator=(const Callback&);
};

问题是向Callback 添加成员需要在UserInfo 中进行相同的更改,以及更新operator= 和类似的依赖成员函数。为了使它们自动保持同步,我想这样做:

struct Callback {
  int i;
  float f;
};

struct UserInfo : Callback{
  std::string thekicker;
  void print();
  UserInfo& operator=(const Callback&);
};

现在UserInfo 保证拥有与Callback 相同的所有数据成员。事实上,最重要的是数据成员thekickerCallback 中没有声明虚拟析构函数,我相信其他编码人员希望它保持这种状态(他们强烈反对虚拟析构函数的性能损失)。但是,如果UserInfo 类型通过Callback* 销毁,thekicker 将被泄露。应该注意的是,UserInfo 并不打算通过Callback* 接口使用,因此这些类首先是分开的。另一方面,为了修改一个结构而不得不以相同的方式修改三段或更多段代码,感觉不雅且容易出错。

问题:有没有办法允许UserInfo 公开继承Callback(用户必须能够访问所有相同的信息)但不允许分配Callback 引用UserInfo 特别是因为缺少虚拟析构函数?我怀疑这是不可能的,因为它首先是继承的基本目的。我的第二个问题,有没有办法通过其他方法使这两个类保持同步?我想让Callback 成为UserInfo 的成员而不是父类,但我希望使用user.i 而不是user.call.i 直接读取数据成员。

我认为我要求的是不可能的,但我总是对 stackoverflow 答案的巫术感到惊讶,所以我想我只是想看看是否真的有解决这个问题的方法。

【问题讨论】:

  • 当关系看起来像一个容器时为什么要继承?
  • 也许:让Callback 成为UserInfo 的成员并提供转换运算符?
  • @KarolyHorvath “我想让Callback 成为UserInfo 的成员而不是父类,但我希望使用user.i 而不是user.call.i 直接读取数据成员。”
  • 您是否测量虚拟析构函数的性能损失?它会很小。更好的是,当您不使用对象的指针或引用时,编译器可能会完全忽略虚拟查找。
  • @MarkRansom 如果我要使析构函数虚拟化,我必须首先通过测量证明它对性能的影响最小。我毫不怀疑这不会产生影响,但举证责任在我身上。我们正在编译的东西非常复杂,以至于在许多情况下要证明它是一件相当繁琐的事情。

标签: c++ c++11


【解决方案1】:

您始终可以通过使析构函数在基类中受到保护来强制执行您提到的“不能通过基类指针删除”约束(在某种程度上):

// Not deletable unless a derived class or friend is calling the dtor.
struct Callback {
  int i;
  float f;
protected:
  ~Callback() {}
};

// can delete objects of this type:
struct SimpleCallback : public Callback {};


struct UserInfo : public Callback {
  std::string thekicker;
  // ...
};

正如其他人所提到的,您可以删除赋值运算符。对于 c++11 之前的版本,只需将该函数的未实现原型设为私有:

private:
  UserInfo& operator=(const Callback&);

【讨论】:

  • 完美。我可以用这个。
  • 这是我的第一个想法,但它使得销毁 real Callback 对象变得相当复杂。
  • 我将Callback重新定义为_Callback,然后将struct Callback : _Callback {};struct UserInfo: _Callback {...};。只要他们不深入头文件并积极使用_Callback(在这种情况下他们试图在脚上开枪),它应该有我要求的一切。如果我错了,请纠正我。
  • 名称_Callback 为编译器保留。像CallbackBase 这样的东西虽然可以 - 我假设属性更改只会发生在基类中?
  • 是的,他们应该使用不会为 _Callback 类型编译的智能指针。如果他们没有删除'c',那么他们还有其他问题。当然,他们仍然可以使用placement new 而忽略dtor..
【解决方案2】:
struct UserInfo : Callback {
  ...
  // assignment from Callback disallowed
  UserInfo& operator=(const Callback&) = delete;
  ...
};

请注意,STL 具有很多没有虚拟析构函数的继承。文档明确指出这些类并非旨在用作基类。

一些例子是vector、set、map ....

另一种方法是考虑私有继承,同时提供访问器方法来显示回调(在这种情况下,您不妨使用更简洁的封装)。

【讨论】:

    【解决方案3】:

    是的,您可以使用一些技巧来保持成员同步并自动更新operator=。虽然它很丑,涉及宏和使用包含文件的不寻常方式。

    CallBackMembers.h:

    MEMBER(int, i)
    MEMBER(float, f)
    

    其他地方:

    struct Callback {
      #define MEMBER(TYPE,NAME) TYPE NAME;
      #include "CallbackMembers.h"
      #undef MEMBER
    };
    
    struct UserInfo {
      #define MEMBER(TYPE,NAME) TYPE NAME;
      #include "CallbackMembers.h"
      #undef MEMBER
    
      std::string thekicker;
    
      void print();    // you can use the macro trick here too
    
      UserInfo& operator=(const Callback& rhs)
      {
        #define MEMBER(TYPE,NAME) NAME = rhs.NAME;
        #include "CallbackMembers.h"
        #undef MEMBER
        return *this;
      }
    };
    

    【讨论】:

    • 哦哇哈哈。虽然我的上级不会欣赏这种事情,但我当然可以。有趣的解决方法!
    • @Aggieboy 如果是我,我会选择虚拟析构函数。
    【解决方案4】:

    没有办法满足你想要的所有标准。

    就个人而言,我认为您的想法是使其成为会员然后使用user.call.i 是最好和最明确的选择。请记住,您编写的代码只使用了一次,但是您在可维护性(因为您的 UserData 永远不必更改)和可读性(因为它对最终用途是 100% 透明)是哪个属性的一部分回调数据和辅助数据)。

    唯一可能有意义的其他选项是使用私有继承,并将using 属性或函数放入UserData。有了这个,当新数据添加到回调时,您仍然需要添加一个 using,但您会为客户端获得所需的 user.i 语法。

    【讨论】:

    • using 语法是 c++11 特有的东西,对吧?我知道 c++11 的特性,但我经常混淆什么是 c++0x 和什么是 c++11。
    • 自 C++98 起,using 的此语法不可用。
    猜你喜欢
    • 1970-01-01
    • 2016-08-14
    • 2011-01-12
    • 2015-06-21
    • 2020-03-10
    • 2012-04-13
    • 2012-06-18
    • 2012-04-18
    • 2014-10-12
    相关资源
    最近更新 更多