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