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;
}