本地C++属性的可移植性

30
在Visual Studio中,有一个名为__declspec(property)的功能可以创建类似C#属性的属性。Borland C++提供了具有完全相同功能的__property关键字。在C++0x中,提到了一个implicit关键字,可以扩展实现相同的功能。但它没有被列入规范。
我正在寻找一种可移植且相对清晰的声明语法糖属性的方法,该方法将在Windows、OSX和Linux的最新编译器中编译。我不关心编译器兼容性,只需要每个平台一个编译器。
我不寻找需要使用括号来获取或设置属性(例如用于区分getter和setter的重载方法)的属性替代方法。
以下是在Visual Studio 2010中编译的理想用法:
#define _property(_type, _name, _get, _put) __declspec(property(get=_get, put=_put)) _type _name
#define _property_readonly(_type, _name, _get) __declspec(property(get=_get)) _type _name

class Window
{
public:
    _property_readonly(void*, Handle, GetHandle);
    _property(bool, Visible, GetVisible, SetVisible);

    void* GetHandle();
    bool GetVisible();
    void SetVisible(bool);
}

void main()
{
    Window MainWindow;
    if (!MainWindow.Visible)
        MainWindow.Visible = true;
}

2
C++0x 中没有 implicit 关键字。不过我无法访问你发布的链接(它要求输入用户名和密码)。 - James McNellis
1
我更新了链接。看起来这似乎没有被包含在C++0x规范中。 - Josh Brown
13
我的想法是,如果你的目标是可移植性,那么最好远离这些扩展功能。没错。 - sehe
2
“可移植性方面呢?这是指‘熟悉该语言的程序员应该能够理解我的代码’。这种可移植性非常重要,而您却毫无好处地放弃了它。为什么?” - jalf
1
这更多是关于我能做什么,而不是为什么我不应该这样做。我有很多理由不这样做,但我想这样做。C++已经是一种混乱的语言,具有旧和新实现的混合,这取决于供应商,例如for-each实现。 - Josh Brown
显示剩余2条评论
4个回答

31

这与您所问的问题类似,而且(我希望)是标准的C++...

#include <iostream>

template<typename C, typename T, T (C::*getter)(), void (C::*setter)(const T&)>
struct Property
{
    C *instance;

    Property(C *instance)
        : instance(instance)
    {
    }

    operator T () const
    {
        return (instance->*getter)();
    }

    Property& operator=(const T& value)
    {
        (instance->*setter)(value);
        return *this;
    }

    template<typename C2, typename T2,
             T2 (C2::*getter2)(), void (C2::*setter2)(const T2&)>
    Property& operator=(const Property<C2, T2, getter2, setter2>& other)
    {
        return *this = (other.instance->*getter2)();
    }

    Property& operator=(const Property& other)
    {
        return *this = (other.instance->*getter)();
    }
};

//////////////////////////////////////////////////////////////////////////

struct Foo
{
    int x_, y_;

    void setX(const int& x) { x_ = x; std::cout << "x new value is " << x << "\n"; }
    int getX() { std::cout << "reading x_\n"; return x_; }

    void setY(const int& y) { y_ = y; std::cout << "y new value is " << y << "\n"; }
    int getY() { std::cout << "reading y_\n"; return y_; }

    Property<Foo, int, &Foo::getX, &Foo::setX> x;
    Property<Foo, int, &Foo::getY, &Foo::setY> y;

    Foo(int x0, int y0)
        : x_(x0), y_(y0), x(this), y(this)
    {
    }
};

int square(int x)
{
    return x*x;
}

int main(int argc, const char *argv[])
{
    Foo foo(10, 20);
    Foo foo2(100, 200);
    int x = foo.x; std::cout << x << "\n";
    int y = foo.y; std::cout << y << "\n";
    foo.x = 42; std::cout << "assigned!\n";
    x = foo.x; std::cout << x << "\n";
    std::cout << "same instance prop/prop assign!\n";
    foo.x = foo.y;
    std::cout << "different instances prop/prop assign\n";
    foo.x = foo2.x;
    std::cout << "calling a function accepting an int parameter\n";
    std::cout << "square(" << foo.x << ") = " <<  square(foo.x) << "\n";
    return 0;
}

main中可以看出,只要您将类型为T(这里是int)或隐式可转换为T的值分配给属性,并且在读取时将它们转换回T值,使用方式就是透明的。

但是,如果您例如将foo.x传递给模板函数,则行为将有所不同,因为foo.x的类型不是int,而是Property<Foo,int,...>

您还可能遇到非模板函数的问题...调用接受T值的函数将正常工作,但是例如一个T&参数将是一个问题,因为基本上该函数要求变量直接使用地址访问。出于同样的原因,您当然不能将属性的地址传递给接受T*参数的函数。


类似于a simple meta-accessor;唯一的区别在于选择重载operator()而不是operator=operator T,并且(有点)区分POD和非POD类型。 - Jon Purdy
1
最大的缺点是它消耗每个属性多一个指针,但为了方便肯定是可以接受的。如果做了内联优化,开销非常小。相比使用每个编译器专有的定义和方法,这也更加简洁,而且我可以拥有私有的getter和setter以保持IntelliSense的整洁。这使得将基于结构体的代码转换为事件驱动的类非常容易。谢谢! - Josh Brown
昨晚我刚试验了这个Property实现,立刻就感到很兴奋(希望像C#的属性一样)。然后我尝试将该属性传递给非模板函数参数,并遇到了你在答案末尾提到的“不是int而是Property<Foo,int,...>”难题。因此,对于任何其他有兴趣尝试的人,请记住这些属性不能像它们的基础类型那样传递。它们“仅仅”是语法糖,用于函数调用。没有更多,也没有更少。 - Bret Kuhns
1
@BretKuhns:我已经添加了关于引用参数的注释。对于类型为T的值,我不期望出现问题... - 6502

9
Clang现在已经完全实现了Microsoft的__declspec(property...),并且它的优化效果非常好。因此,您可以在所有平台上使用属性,并与基于gcc或c99的代码混合使用等。
我已经使用它一年多了,并等待着它出现普及已经超过了五年。
它是最强大的C++工具之一,用于抽象结构和重构代码。我经常使用它来快速构建一个结构,然后根据需要进行性能或重构。
它是无价的,我真的不明白为什么C++标准没有早就采用它。但话说回来,他们有太多复杂和臃肿的boost方法来使用C++和模板。
Clang现在在每个平台上都非常便携,拥有这个功能非常棒。
在Visual Studio中使用clang进行开发(免费或付费版本),几乎是无缝的,并且您可以获得令人难以置信的调试开发工具集,这使得使用其他工具集和平台变得痛苦。
我现在专门使用clang进行所有的C++开发。
另请参阅:此交叉引用帖子

3
我正在寻找一种轻便且相对干净的方法来声明语法糖属性,以便在最新的Windows、OSX和Linux编译器中编译。
您正在描述“元对象”类型的功能,比如在编译时或运行时定义属性,例如那些可能通过“Java beans”或“.NET反射”等方式实现的属性,或者使用高级脚本语言(如Python和Perl)的任何数量的方式。
例如,您所描述的(编译时和/或运行时属性)在Qt(C++)库中通过QMetaObject实现。您可以直接实例化它,在您的类中使用它作为“成员”,或从QObject派生以“自动”获取该元对象行为(以及一些其他东西,如“转换”帮助,以及跨线程的信号/槽)。当然,这些是非常跨平台的(例如,Win、Mac、Posix)。
我不是__declspec()的忠实拥护者,除非是一些非常特定于平台的用途,例如通过“Microsoft Extension DLL”显式导出类型(如果可能的话,我通常会避免使用)。我认为没有任何办法使这种用法“跨平台”(因为该特定用法仅适用于MS DLL)。
同样,编写自己的"MyMetaObject"类型类并不难,它基本上是一个"字典"或"哈希"或"关联数组",您的对象使用它,在运行时动态填充,甚至包括您的内部类型(例如MyColorMyTimeMyFilePath等)。我已经做过几次了,它不需要太多的工作量,并且可以相当优雅地工作。(QMetaObject通常比这些简单方法更强大,但它需要"moc"编译步骤,这是一步非常强大的步骤,用于生成其属性的快速查找代码,并启用信号/槽)。

最后,您开始轻松接触“动态C ++”领域,这意味着更轻,几乎类似于脚本的使用C ++语法。以下是一个提案,深入探讨了这种动态使用方式,在其中您可以使用这些属性进行脚本编写,而无需重新编译。(这个特定的提案恰好基于QMetaObject类型行为,但还有其他提案具有类似的使用思路):

http://www.codeproject.com/KB/cpp/dynamic_cpp.aspx

如果你在谷歌上搜索“Dynamic C++”或“C++脚本”,你可能会得到更多的想法。其中有一些非常聪明的思路。

2
我喜欢6502的回答。它既使用了更少的内存,也比我将要呈现的解决方案更快。只是我的解决方案会有一点点语法糖。
我想要能够编写像这样使用PIMPL习惯用法的代码:
class A {
private:
    class FImpl;
    FImpl* Impl;

public:
    A();
    ~A();

    Property<int> Count;
    Property<int> Count2;
    Property<UnicodeString> Str;
    Property<UnicodeString> Readonly;
};

以下是完整的代码(我非常确定它符合标准):

template <typename value_t>
class IProperty_Forward {
public:
    virtual ~IProperty_Forward() {}
    virtual const value_t& Read() = 0;
    virtual void Set(const value_t& value) = 0;
};

template <typename value_t, typename owner_t, typename getter_t, typename setter_t>
class TProperty_Forwarder: public IProperty_Forward<value_t>
{
private:
    owner_t* Owner;
    getter_t Getter;
    setter_t Setter;
public:
    TProperty_Forwarder(owner_t* owner, getter_t& getter, setter_t& setter)
    :Owner(owner), Getter(getter), Setter(setter)
    { }

    const value_t& Read()
        { return (Owner->*Getter)(); }

    void Set(const value_t& value)
        { (Owner->*Setter)(value); }
};

template <typename value_t>
class Property {
private:
    IProperty_Forward<value_t>* forward;
public:
    Property():forward(NULL) { }

    template <typename owner_t, typename getter_t, typename setter_t>
    Property(owner_t* owner, getter_t getter, setter_t setter)
        { Init(owner, getter, setter); }

    ~Property()
        { delete forward; }

    template <typename owner_t, typename getter_t, typename setter_t>
    void Init(owner_t* owner, getter_t getter, setter_t setter)
    {
        forward = new TProperty_Forwarder<value_t, owner_t, getter_t, setter_t>(owner, getter, setter);
    }

    Property& operator=(const value_t& value)
    {
        forward->Set(value);
        return *this;
    }

    const value_t* operator->()
    { return &forward->Read(); }

    const value_t& operator()()
        { return forward->Read(); }

    const value_t& operator()(const value_t& value)
    {
        forward->Set(value);
        return forward->Read();
    }

    operator const value_t&()
        { return forward->Read(); }
};    

还有一些实现细节:

class A::FImpl {
    public:
        FImpl():FCount(0),FCount2(0),FReadonly("Hello") { }

        UnicodeString FReadonly;
        const UnicodeString& getReadonly()
            { return FReadonly; }
        void setReadonly(const UnicodeString& s)
            { }

        int FCount;
        int getCount()
            { return FCount; }
        void setCount(int s)
            { FCount = s; }

        int FCount2;
        int getCount2()
            { return FCount2; }
        void setCount2(int s)
            { FCount2 = s; }

        UnicodeString FStr;
        const UnicodeString& getStr()
            { return FStr; }
        void setStr(const UnicodeString& s)
            { FStr = s; }
};

A::A():Impl(new FImpl)
{
    Count.Init(Impl, &FImpl::getCount, &FImpl::setCount);
    Count2.Init(Impl, &FImpl::getCount2, &FImpl::setCount2);
    Str.Init(Impl, &FImpl::getStr, &FImpl::setStr);
    Readonly.Init(Impl, &FImpl::getReadonly, &FImpl::setReadonly);
}

A::~A()
{
    delete Impl;
}

我正在使用C++ Builder,如果有人想了解UnicodeString类,请注意。 希望这能帮助其他人实验符合标准的C++属性。 基本机制与6502相同,具有相同的限制。


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