C++中类似于Objective-C的分类构造或技术?

11

Objective-C的类别(category)特性允许程序员添加原始类定义中未定义的新方法。

我能在C++上实现类似的功能(语言结构或某些技术)吗?

主要问题是统一的方法调用语法(.->运算符)。


是的,如果您可以访问类定义。否则不行。 - Alok Save
在运行时,您无法获得任何语言支持。但是,您可以使用包含函数指针的映射来实现它。但那样会变得很麻烦且容易出错。 - Michael Wild
@AlokSave 你是指直接修改源代码吗?这可能是一个选项。 - eonil
@MichaelWild 不需要在运行时完成。我正在使用Poco,我不想修改其源代码,因为我不想增加外部依赖维护的复杂性。如果有一种方法可以在不修改现有类代码的情况下添加新方法,那也很好。 - eonil
1
这是不可能的。最好的方法是编写非成员函数,将实例指针作为它们的第一个参数。 - leemes
4个回答

7

让我们考虑以下需要扩展的类:

struct A {
    int x, y;
    A(int x, int y) : x(x), y(y) {}
};

您可以继承此类或编写一个包含此类实例的包装器类。在大多数情况下,继承是最好的选择,因为包装器类不是A,而是包装(包含)A。
使用C++11移动语义,将实例A提升为子类B(继承A)将是有效的,不需要复制实例A。
class B : public A {
public:
    B (A &&a) : A(a), someOtherMember(a.x + a.y) {}

    // added public stuff:
    int someOtherFunction() const { return someOtherMember; }

private:
    // added private stuff:
    int someOtherMember;
};

完整代码示例:http://ideone.com/mZLLEu 当然,我添加的函数有点傻(而且成员更傻,因为它不考虑原始成员x和y的进一步更改),但你应该能够了解我想要演示的内容。请注意构造函数B(A && a),这是我称之为“提升构造函数”的东西(这不是一个标准术语)。通常,B(B && b)是一个移动构造函数,它将提供的B实例的内容移动到即将构建的新B中。我使用移动语义将A的实例(由另一个函数返回)移动到B的超类A中,并将其转换为B。
实际上,您可以将A提升为B,同时使得使用B作为A成为可能。
与Soonts的答案相比,我的解决方案还适用于已添加虚拟表的情况,因为它不依赖于不安全的指针转换。

我理解移动构造函数在语义上创建一个新对象,编译器可能会尽可能地重用现有对象。而您的提升构造函数也具有相同的编译器优化机会。我的理解正确吗? - eonil
1
A(A &&a)A的移动构造函数。这将把旧的A移动到新的A中。从语义上讲,它是一个新对象,但由于移动语义,它可以“窃取”旧A的内容。我的方法是将旧的A移动到B中。由于B继承自A,因此必须初始化一个A。我在B的某个构造函数中使用A的移动构造函数,我称之为提升构造函数(我创造了这个词,因为它有效地将现有的A提升为B,只要它的移动构造函数编写得很好,就不会触及A的内容)。 - leemes
这里有一个微妙之处——考虑一下你还有一个例如class C : public A的情况,其中有一个从A移动构造函数。在这种情况下,你拥有的对象可以是ABC中的任何一个,但不能同时是BC - al45tair

5

另一种选项,虽然某些人可能不认为它是“干净”的选择(但在我看来却是),但仍然可以实现同样的功能,即使用静态类。需要记住的重要一点是,当我们创建成员函数时,实际上发生的是编译器生成一个函数,其中对象(也称为“this”)是第一个参数。因此,我们可以通过相同的方式来扩展我们类的功能,而无需从中导出。

class Something
{
public:
   Something()
   ~Something()
}

// In objective-c you may call this category Something+Utils
class SomethingUtils
{
   // You can use a pointer, or a reference here, your call.
   static int GetSomethingElse(Something *something, int parameter);
}

这将实现与类别相同的意图:扩展类对象的功能,而不必创建新的派生类。您将无法访问私有或受保护的成员函数和变量,但是在Objective-C中也无法这样做,因此在这方面没有任何损失(如果您正在尝试使用私有或受保护的成员状态,则已完全错过了类别的要点)。 您将无法使用.和->运算符,但在我看来,这比派生新类型仅添加一些实用方法要好得多。


这个语法看起来很丑,但我不得不承认这是最实用的选项。即使我采用了@leemes的解决方案,但我会编写一些实用类似这样的东西,并将其封装起来。 - eonil
5
在某些方面更具风格吸引力的轻微变化是使用命名空间而不是其他方式。 - Nate Chandler
我还想补充一点,Eonil,虽然你可能认为它在语法上很丑陋(我个人并不这样认为),但它在语法上是清晰的。它明显地表明这是一个类扩展,而不是核心方法,这是 Objective-C 类别所遭受的问题(你必须跟随方法到头文件才能意识到它是一个类别)。至于丑陋程度,我不会争论,因为我们可能永远无法达成共识 :) - Adam Eskreis

1

C++有继承来实现这个功能。此外,我已经多次使用以下技巧来扩展由#import "progid:..."指令生成的类:

// This one is part of external framework, or auto-generated, or dllimport, or #import, etc..
class A
{
    protected double m_x;
};

// This one is the extension class. Make sure you only add non-virtual methods here.
// Static methods and static data members are OK as well.
class __declspec( novtable ) B: public A
{
public:
    double getSquare(){ return m_x * m_x; }
    __declspec( property( get = getSquare ) ) double square;
};

// Usage example
double someFunc( A& arg )
{
    B& b = static_cast<B&>( arg ); // Note we've not constructed any instance of B, just casted.
    return b.square;
}

哦,实际上我刚刚想出了类似(或者说相同?)的技巧,并发布了一个新的问题。我可以认为这种技巧是符合标准的吗? - eonil
5
__declspec е’Ң property дёҚжҳҜ ISO ж ҮеҮҶ C++ зҡ„дёҖйғЁеҲҶгҖӮ - Bo Persson
我非常确定它在Visual Studio 2005和2008中可以工作。我不知道它是否标准,说实话我也不关心。“标准C ++”只是另一个漏洞百出的抽象概念。为了确保您的C ++代码可以与编译器A、B和C一起构建,您必须以这种方式开发和测试它。 - Soonts
@Soonts 我同意你的观点,如果某个功能X被编译器A、B、C支持,但不在标准中,最好使用它,而不是一个在标准中但在你想要支持的编译器中没有实现的功能Y。然而,你代码中的功能只有MSVC支持,这是一个巨大的限制。 - leemes
很不幸,我正在为Linux/BSD家族的操作系统工作,并且我的代码预计要在尽可能多的平台上具有最大的兼容性... - eonil
3
这个 property 看起来只是一个方便的包装器,用于封装 getter 和 setter。实际上,当读取 b.square 时,b.square 只是 b.getSquare() 的语法糖。另一件事情(novtable)似乎是断言不应该使用 vtable;如果你写了虚函数,我猜你会得到编译器错误,因为它们需要一个 vtable。这种方法的问题在于,B 的内存布局必须以 A 开头,如果添加了 vtables 或成员(技术上,vtable 指针就像是一个成员),那么这种情况就不成立了。 - leemes

0

我使用了一种几乎一致的调用约定,与我大约一年前在闪电演讲中提到的一个想法相似:

(如果没有评论,介绍就没那么有意义 - 等到C++部分再看)。

请注意,这些材料并不是要认真对待的 - 尽管不可避免地有些人会这样做;-)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接