2
\$\begingroup\$

I've finally got a working Proxy pattern. The original task is presented here. My test case is on coliru.

It appears to flow through the correct pathways, however I am grateful for feedback and possible improvements.

EDIT: I've reworked the code to provide sensible trace output, so it is possible to examine the flow under various situations. I'm not sure I've caught every possible situation though.

EDIT: I think I've now trapped all possible scenarios. Complex cases like (a[u] = b[v]) = foo degenerate into a sequence of steps. And I've isolated all the possible steps. The only thing I still need to work out is whether there is any possibility of invalid pointers causing trouble. In the original code every PyObject* is wrapped in an Object, and the Object's destructor takes care of decrementing the reference count. In order to simplify the proxy logic, I'm using bare pointers -- otherwise the complexity is just horrific, with const considerations creeping in. So I need to make sure this doesn't introduce any danger.

Finally, I'm moving away from the idea of using ->. mainly because I'm realising that if ob[idx] is to behave like an object, the proxy class must contain all the initialisers and converters and methods that object contains. -> provides a way of avoiding duplicating the methods, but there is no way to avoid duplicating the initialisers and converters. Hence introducing a new syntax to solve one third of the problems is a bad plan. Especially when that syntax is a compromise in the first place. It should be ob[idx].foo not ->foo.

#include <iostream>
#include <string>
#include <vector>
#include <memory>

using namespace std;

using PyObject = string;

// Dummies for Python C-API functions
void      PyObject_SetItem( PyObject* pyob, PyObject* key, PyObject* value ) { 
    cout << "PyObject_SetItem                           " << *pyob << "[" << *key << "]=" << *value << endl; 
}

PyObject* PyObject_GetItem( PyObject* pyob, PyObject* key )                  { 
    cout << "PyObject_GetItem                           " << *pyob << "[" << *key << "]"            << endl;
    return new string( "item@" + *pyob + "[" + *key + "]" ); 
}

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

    Object(PyObject* _p) : p{_p} { cout << "Object{ " << (p==nullptr?0:*p) << " }" << endl; }

    void someMethod() { cout << "Object::someMethod()" << endl; }
    PyObject* ptr() { return p; }

    Object& operator= (const Object& rhs) {
        cout << "const Object& operator= " << endl;
        p = rhs.p;
        return *this;
    }

    class Proxy {
    private:
        PyObject* container;
        PyObject* key;

    public:
        // at this moment we don't know whether it is 'c[k] = x' or 'x = c[k]'
        Proxy( PyObject* c, PyObject* k ) : container{c}, key{k} { 
            cout << "Proxy(" << *c << ", " << *k << ")" << endl; 
        }

        ~Proxy(){ cout << "~Proxy()" << endl; }

        // Rvalue
        // e.g. fooObject = myList[5]
        operator Object() const {
            cout << "Proxy::operator Object() const             R-VALUE ACCESS" << endl; 
            return { PyObject_GetItem(container,key) };
        }

        // Lvalue
        // e.g. (something = ) myList[5] = foo
        Proxy&  operator= (const Object& rhs_ob) {
            cout << "Proxy& Proxy::operator= (const Object&)    L-VALUE ACCESS" << endl; 
            PyObject_SetItem( container, key, rhs_ob.p ); 
            return *this; // allow daisy-chaining a = b = c etc
        }

        /*
         This is to handle ... = ob[a] = ob[b] = ... .  
         The compiler provides an automatic 'Proxy operator=', unless I provide one. 

         My first idea is to eliminate this 'Proxy operator=', and hope that when the compiler encounters 
         'fooProxy=barProxy' it's overload resolution will notice Proxy provides conversion to Object, 
         and produce 'fooProxy=Object(barProxy)'.

         Unfortunately, the deleted function still participates in overload resolution. (Why??)
         And consequently the compiler produces a 'Proxy operator=' error instead.
         */
        Proxy&  operator= (const Proxy& rhs) {
            cout << "Proxy& Proxy::operator= (const Proxy&)     PROXY-PROXY" << endl; 
            cout << "getting rhs... " << endl;
            PyObject* val = rhs->ptr();
            cout << "... = " << *val << endl;

            PyObject_SetItem( container, key, val );            
            return *this; // allow daisy-chaining a = b = c etc
        }

        Object operator->() const { 
            cout << "Object Proxy::operator->()" << endl; 
            return { PyObject_GetItem(container,key) }; 
        }
    };

    /* 
     This overload is unnecessary, and is just for efficiency. 
     If ob is const, we know that we won't be doing ob[idx]=...
     And so we can bypass having to engage the proxy mechanism.
    */
    const Object  operator[] (const Object& key)  const { 
        cout << "const Object Object::operator[]            setting " << *p << "[" << *(key.p) << "] CONST-SHORTCUT" << endl; 
        return { PyObject_GetItem( p, key.p ) }; 
    }

    Proxy operator[] (const Object& key) {
        cout << "Proxy Object::operator[]                   creating proxy obj for " << *p << "[" << *(key.p) << "]" << endl; 
        return { p, key.p };
    }


    const Object* operator -> () const { 
        cout << "const Object* Object::operator -> " << endl; 
        return this; 
    }

    Object* operator -> () { 
        cout << "Object* Object::operator ->" << endl; 
        return this; 
    }        
};

int main()
{
    PyObject a="ob",b="idx",c="targ";
    Object ob=Object(&a), idx=Object(&b), targ=Object(&c);
    Object result;

    // std::endl flushes
    #define DO(x) cout << "- - - - - - - - - - - - - - - - \n" "EXECUTING: " #x << endl; x;

    DO( 
        PyObject pyob_in_const_ob = "pyob_in_const_ob";
        const Object const_ob(&pyob_in_const_ob); 
        //const_ob[idx] = targ; <-- can't assign to const obviously!
        result = const_ob[idx]; 
        )

    DO( Object out = ob[idx]; )

    DO( ob[idx] = targ )

    DO( auto x = ob[idx] )

    DO( 
        PyObject d="idy";
        Object idy=Object(&d);
        ob[idx] = ob[idy]; 
        )

    DO( ob[idx] = ob[idy] = targ )

    ob[idx]->someMethod();

    DO( PyObject* w = ob[idx]->p; );

    DO( /* avoid "variable not used" warnings */ )
    cout << w;
    cout << &out;
} 
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.