【问题标题】:Is it possible to subclass a C struct in C++ and use pointers to the struct in C code?是否可以在 C++ 中对 C 结构进行子类化并在 C 代码中使用指向该结构的指针?
【发布时间】:2023-08-17 02:28:01
【问题描述】:

这样做有副作用吗:

C 代码:

struct foo {
      int k;
};

int ret_foo(const struct foo* f){ 
    return f.k; 
}

C++ 代码:

class bar : public foo {

   int my_bar() { 
       return ret_foo( (foo)this ); 
   }

};

C++ 代码周围有一个extern "C",每个代码都在自己的编译单元中。

这可以跨编译器移植吗?

【问题讨论】:

  • 我猜你的意思是 (foo*)this,或者更好:static_cast​​(this)
  • 我很好奇 extern "C" 对带有方法的类的影响...不过,您想要这样做的原因将有助于我们提供信息或替代解决方案。跨度>
  • 当您说“跨编译器移植”时,您的意思是用一个编译器编译 C 代码,用另一个编译器编译 C++,然后将两者链接在一起吗?

标签: c++ c gcc extern-c


【解决方案1】:

这是完全合法的。在 C++ 中,类和结构是相同的概念,除了所有结构成员默认情况下都是公共的。这是唯一的区别。因此,询问是否可以扩展结构与询问是否可以扩展类没有什么不同。

这里有一个警告。 不保证从编译器到编译器的布局一致性。因此,如果您使用与 C++ 代码不同的编译器编译 C 代码,您可能会遇到与成员布局相关的问题(尤其是填充)。当使用来自同一供应商的 C 和 C++ 编译器时,甚至会发生这种情况。

在 gcc 和 g++ 中发生过这种情况。我参与了一个使用多个大型结构的项目。不幸的是,g++ 对结构的打包比 gcc 松散得多,这导致在 C 和 C++ 代码之间共享对象时出现严重问题。我们最终不得不手动设置打包和插入填充,以使 C 和 C++ 代码对结构进行相同的处理。但是请注意,无论子类化如何,都可能发生此问题。事实上,在这种情况下,我们并没有继承 C 结构体。

【讨论】:

  • 其实结构体和类还有一个区别:基类的默认可见性在结构体中是公开的,而在类中是私有的。
【解决方案2】:

我当然不推荐使用这种奇怪的子类化。最好将您的设计更改为使用组合而不是继承。 只做一个成员

foo* m_pfoo;

在酒吧类中,它会做同样的工作。

您可以做的另一件事是再创建一个类 FooWrapper,它本身包含结构和相应的 getter 方法。然后你可以子类化包装器。这样虚拟析构函数的问题就解决了。

【讨论】:

  • 这是最好的答案。与公认的答案不同,这里指出了一些选项可以帮助您避免 vtables 和结构布局的问题。
【解决方案3】:

“永远不要从具体类派生。” — 萨特

“使非叶类抽象。” — 迈耶斯

子类化非接口类是完全错误的。你应该重构你的库。

从技术上讲,你可以做你想做的事,只要你不调用未定义的行为,例如。 g.,通过指向其基类子对象的指针删除指向派生类的指针。对于 C++ 代码,您甚至不需要 extern "C"。是的,它是便携式的。但这是糟糕的设计。

【讨论】:

  • 我完全不同意 Meyer 和 Sutter 的明确声明。通过从具体类继承可以做很多事情,特别是如果您只在派生类中添加方法,而不是数据成员。这样就没有切片的问题了。
  • 我认为你没有抓住重点。我的猜测是 OP 有现有的 C 模块,并希望将一些结构“包装”到类中,同时仍为遗留系统提供 C 接口。如果他在结构上坚持使用 C,那么 Sutter 和 Meyer 将毫无帮助。
【解决方案4】:

这是完全合法的,尽管它可能会让其他程序员感到困惑。

您可以使用继承来扩展具有方法和构造函数的 C 结构。

示例:

struct POINT { int x, y; }
class CPoint : POINT
{
public:
    CPoint( int x_, int y_ ) { x = x_; y = y_; }

    const CPoint& operator+=( const POINT& op2 )
    { x += op2.x; y += op2.y; return *this; }

    // etc.
};

扩展结构可能“更”邪恶,但不是你被禁止做的事情。

【讨论】:

  • 我不太确定。我相信(至少在当前版本的标准中)作为基类子对象的类的布局可能与您拥有该类的实例不同。 (参见 ISO C++ 10/5 下的注释)
  • 如果派生类没有添加数据,也没有添加虚方法,那么应该不会发生任何不好的事情。现在,这在设计上可能是邪恶的,但如果需要添加默认构造函数和一些辅助函数,那么……嗯……这不是设计。它只是纠正一个缺失的功能,仅此而已。
【解决方案5】:

哇,太邪恶了。

这可以跨编译器移植吗?

绝对不是。考虑以下几点:

foo* x = new bar();
delete x;

为了让它工作,foo 的析构函数必须是虚拟的,而它显然不是。不过,只要您不使用 new 并且只要派生对象没有自定义析构函数,您就很幸运了。

/EDIT:另一方面,如果代码仅用于问题中,则继承与组合没有优势。听从 m_pGladiator 的建议即可。

【讨论】:

  • 但这不是我打算写的代码。 bar 只能强制转换为 foo 用于函数调用,并且所有对 foo 进行操作的 C 函数都是 const。 (就像在示例中一样。)
  • 是的,在这种情况下,代码还可以。它只是乞求滥用。 ;-)
  • 似乎 foo* 接口仅由 C 使用。 C 代码无论如何都无法删除它:[1] 它不是所有者,[2] 它不是 C++。
  • 应该注意,任何具有非虚拟析构函数的 C++ 类都会带来同样的问题。这不是可移植性问题,而是在同一种语言 (C++) 和同一种编译器中发生,如果基类的设计不是继承自。
【解决方案6】:

这是完全合法的,您可以在 MFC CRect 和 CPoint 类的实践中看到它。 CPoint 派生自 POINT(在 windef.h 中定义),而 CRect 派生自 RECT。您只是用成员函数装饰一个对象。只要您不使用更多数据扩展对象,就可以了。事实上,如果您有一个复杂的 C 结构,默认初始化很麻烦,那么使用包含默认构造函数的类对其进行扩展是处理该问题的一种简单方法。

即使你这样做:

foo *pFoo = new bar;
delete pFoo;

那么你就没事了,因为你的构造函数和析构函数是微不足道的,而且你没有分配任何额外的内存。

您也不必用'extern "C"' 包装您的C++ 对象,因为您实际上并未将C++ 类型 传递给C 函数。

【讨论】:

  • 你错了。通过指向基类的指针删除指向派生类的指针是未定义的行为。期间。
【解决方案7】:

我认为这不一定是个问题。行为是明确定义的,只要您小心处理生命周期问题(不要在 C++ 和 C 代码之间混合和匹配分配),就会做您想做的事情。它应该可以完美地跨编译器移植。

析构函数的问题是真实存在的,但在基类析构函数不是虚拟的任何时候都适用,不仅适用于 C 结构。这是您需要注意的事情,但不排除使用此模式。

【讨论】:

  • 行为没有很好的定义——至少在我阅读标准时。你有提到它是合法的文字吗?
  • 没有参考...但我不完全理解您在下面使用的报价。这不会阻止任何有用的向下转换,我看不出它是如何特定于基类是 C 结构的情况
  • 很遗憾,我找不到规范性参考。但是 ISO C++ 委员会成员向我指出,只有当对象是 POD 类型的具体实例时,它才是 POD。 IE。没有像 POD 基础子对象这样的东西。我将在下面的答案中添加更多细节。
【解决方案8】:

它可以工作,并且可以移植,但是你不能使用任何虚函数(包括析构函数)。

我建议您不要这样做,而是让 Bar 包含一个 Foo。

class Bar
{
private:
   Foo  mFoo;
};

【讨论】:

    【解决方案9】:

    我不明白您为什么不简单地将 ret_foo 设为成员方法。您当前的方式使您的代码非常难以理解。首先使用带有成员变量和 get/set 方法的真实类有什么困难?

    我知道可以在 C++ 中对结构进行子类化,但危险在于其他人将无法理解您编写的代码,因为很少有人真正做到这一点。我会选择一个强大且通用的解决方案。

    【讨论】:

    • 只是因为 ret_foo 是用纯 C 编写的,我没有成员方法。另外,我没有ret_foo的代码,它是为我提供的API。
    • 不要子类化你无法控制的东西。使它成为一个成员变量并在那里处理它。如果有用,请将其包裹在一个对象中。组合在这里比推导好得多。
    【解决方案10】:

    它可能会起作用,但我不相信它一定会起作用。以下是来自 ISO C++ 10/5 的引用:

    基类子对象的布局 (3.7) 可能与同类型的最派生对象的布局不同。

    很难看出在“现实世界”中实际情况如何。

    编辑:

    底线是标准没有限制基类子对象布局可以不同于具有相同基类型的具体对象的位置的数量。结果是您可能拥有的任何假设,例如 POD 特性等,对于基类子对象不一定是正确的。

    编辑:

    另一种方法,其行为被明确定义的方法是使 'foo' 成为 'bar' 的成员,并在必要时提供转换运算符。

    class bar {
    public:    
       int my_bar() { 
           return ret_foo( foo_ ); 
       }
    
       // 
       // This allows a 'bar' to be used where a 'foo' is expected
       inline operator foo& () {
         return foo_;
       }
    
    private:    
      foo foo_;
    };
    

    【讨论】:

    • 我怀疑这是由于虚拟继承造成的。
    • 问题是,我不相信标准实际上限制了它可能发生的地方。
    • Richard,无论您使用的是结构体还是类,还是一些时髦的组合,这句话都适用。这就是说编译器可以更改对象的底层布局,使其看起来合适。这不会破坏任何东西。
    • 但是一个POD结构必须有一个非常具体的布局。如果编译器更改了该布局,那么 C 代码可能无法工作?我错过了什么吗?
    最近更新 更多