Implementing Recursive Proxy pattern in C++11

荒凉一梦 提交于 2020-01-05 12:30:42

问题


Suppose we have some Foo object that allows:

cout << myFoo[3];
myFoo[5] = "bar";

This calls for a proxy design-pattern (detailed by Scott Meyers here)

But now let us suppose every myFoo[i] is also a Foo instance.

myFoo[7] = Foo{...};
myFoo[5] = "bar"; // Foo has a Foo(std::string) non-explicit constructor

I'm close to having an implementation, but I can't get rid of one final pesky "forward declaration/ incomplete type" error.

Firstly, let's get the easy one out of the way:

// x = someConstObject[4], so this must be Rvalue access
//  i.e. someConstObject[4] = ... would be a contradiction / const violation
const Object  operator[] (const Object& key)  const { 
    return Object{ PyObject_GetItem(p, key.p) };
}

Here is the basic non-recursive proxy pattern:

Proxy operator [] ( const Object& key ) { return Proxy{ *this, key }; }

class Proxy {
private:
    const Object& container;
    const Object& key;

public:
    // at this moment we don't know whether it is 'container[key] = x' or 'x = container[key]'
    Proxy( const Object& c, const Object& k ) : container{c}, key{k}
    { }

    // Rvalue
    // e.g. cout << myList[5] 
    operator Object() const {
        return container[key]; // <-- invokes the original const [] overload
    }

    // Lvalue
    // e.g. myList[5] = foo
    const Object&  operator= (const Object& rhs_ob) {
        PyObject_SetItem( container.p, key.p, rhs_ob.p );
        return rhs_ob; // allow daisy-chaining a = b = c etc.
    }

    #if 0 
    // I think this should come for free, as the above Rvalue handler 
    //     ... collapses a Proxy into an Object

    // e.g. myList[5] = someOtherList[7]
    const Proxy&  operator= (const Proxy& rhs) {
        // Force resolution of rhs into Object
        PyObject_SetItem( pContainerObj->p, pKeyObject->p, static_cast<Object>(rhs).p /* rhs.value->p*/ );
        return rhs;
    }
    #endif
    // ^ Note: allows:
    // e.g. x = y[1] = z[2];  // <-- y[1] must return an Object
    // e.g. if( y[1] = z[2] ) // <-- assigns and then checks that y[1] evaluates to true
};

Not sure if I need that last handler.

Anyways, to make it recursive, we would require:

    class Proxy : Object {
        :

And this means that we can no longer define Proxy within Object, otherwise we will get an "attempting to base from incomplete type" compiler error.

So let's do that. And we would also have to modify the constructor to fill in the base class when possible:

class Object::Proxy : public Object {
private:
    const Object& container;
    const Object& key;

public:
    // at this moment we don't know whether it is 'c[k] = x' or 'x = c[k]'
    // If it's 'c[k] = x', setting the base class to c[k] is going to 
    //     either set it to the old value of c[k]
    //     or a None object (if it didn't have any value previously)
    // we had better be certain to make sure the original c[k] overload 
    //     returns None if unsuccessful
    Proxy( const Object& c, const Object& k ) 
        : container{c}, key{k}, Object{c[k]} // <-- might fail!
    { }

And then, due to the Object base class, we would no longer need to manually handle typecast-to-object:

    // Rvalue
    // e.g. cout << myList[5] hits 'const Object operator[]'
    #if 0
    // it looks as though we don't need to do this given that 
    //    we now have Object as base class
    operator Object() const {
        return container[key];
    }
    #endif

But this is where it gets gnarly.

If we move Object::Proxy's definition outside of (after, in fact) Object, the original

    Proxy operator [] ( const Object& key ) { return Proxy{ *this, key }; }

... now gives us an error because we have made use of an incomplete class (Proxy). Note that simply moving the definition outside doesn't fix the fact that the return type is Proxy. If only it were Proxy* we could do it. But Proxy cannot.

It appears to be a Catch-22, and I can't see any clean solution.

Is there one?

EDIT: In response to the comment suggesting flawed design, please bear in mind that Object is a lightweight wrapper around a pointer. It has only a single PyObject* data member.

EDIT: The original code I'm working from to be found here


回答1:


Your premise appears flawed. A Proxy is not an Object, by definition; if it were, then you wouldn't call it a Proxy in the first place. And then you could solve your problem without proxies, the same way that standard datatypes like std::map solve it: just have operator[] return a reference to a newly created Object when necessary.

You're looking for something like std::vector<bool>'s proxy pattern: operator[] returns a Proxy with an operator= and an implicit conversion to the non-proxy Object (for the cases when you really want to use the value, instead of assigning into it).

class Object {
    struct Proxy {
        PyObject *container;
        PyObject *key;
        Proxy(PyObject *c, PyObject *k): container(c), key(k) {}
        Proxy& operator= (const Object& value) { 
            PyObject_SetItem(container, key, value.p); 
            return *this; 
        }
        operator Object() const {
            PyObject *p = PyObject_GetItem(container, key);
            if (p == nullptr) throw "proxy was not backed by a real object";
            return p;
        }
    };

    PyObject *p;
    Object(PyObject* p): p(p) {}

public:
    Object operator[] (const Object& key) const { 
        return PyObject_GetItem(p, key.p); 
    }
    Proxy operator[] (const Object& key) { return {p, key.p}; }
};



回答2:


I eventually solved this.

The trick is to simply use the class as its own proxy.

So where originally the Proxy object provides conversions to differentiate Lvalue from Rvalue access, I just move these conversions back into my original Object class:

    mutable bool m_resolve_me{false};
    PyObject* m_container{nullptr};
    PyObject* m_key{nullptr};

public:
    // Rvalue (e.g. x = ob[42];)
    const Object operator[] (const Object& key)  const { 
        return Object{ PyObject_GetItem( p, key.p ) }; 
    } 

    // Don't know yet
    Object operator[] (const Object& key) { 
        return Object{ *this, key }; 
    }

    // notice we set the m_resolve_me flag
    // as we don't yet know L/Rvalue-ness
    Object( const Object& c, const Object& k )
        : m_container{c.p}, m_key{k.p}, m_resolve_me{true}
    {
        // for all but lvalue access (ob[idx]=...), ob[idx] will be valid
        p = PyObject_GetItem( m_container, m_key ); 

        if( p == nullptr ) {
            // ... However in the case of lvalue access, 
            // PyObject_GetItem will set Python's error indicator
            // so we must flush that error, as it was expected!
            PyErr_Clear();
            p = charge(Py_None);
        }
        // ^ either way, p ends up charged
    }

public:
    // this will attempt to convert ANY rhs to Object, which takes advantage of ALL the above constructor overrides
    Object& operator=( const Object& rhs )
    {
        /*
            1) normal situation
            2) this object is m_resolve_me, and we are assigning 
                 a normal object to it
            3) this object is m_resolve_me, and we are assigning 
                 a m_resolve_me object to it
            4) this object is normal, and we are assigning a m_resolve_me object to it

            1) we need to charge p
            2) same
            3) same
            4) same

            The only important thing is: we have to be neutral to rhs.p
            That means we have to charge it, as we will be 
               subsequently neutralising it in the destructor
         */
        if( &rhs != this )
            *this = charge(rhs.p);

        return *this;
    }

    // (Always) assume charged pointer
    Object& operator=( PyObject* pyob )
    {
        if( m_resolve_me ) {
            PyObject_SetItem( m_container, m_key, pyob );
            m_resolve_me = false;
        }

        set_ptr( pyob );

        return *this;
    }


来源:https://stackoverflow.com/questions/27680299/implementing-recursive-proxy-pattern-in-c11

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!