Реализация шаблона рекурсивного прокси в C++11

Предположим, у нас есть некоторый объект Foo, который позволяет:

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

Это требует шаблона проектирования прокси (подробного Скотт Мейерс здесь)

Но теперь предположим, что каждый myFoo[i] также является экземпляром Foo.

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

Я близок к реализации, но не могу избавиться от одной последней надоедливой ошибки "предварительное объявление/неполный тип".

Во-первых, давайте избавимся от простого:

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

Вот базовый шаблон нерекурсивного прокси:

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

Не уверен, нужен ли мне этот последний обработчик.

В любом случае, чтобы сделать его рекурсивным, нам потребуется:

    class Proxy : Object {
        :

А это значит, что мы больше не можем определять Proxy внутри Object, иначе получим ошибку компилятора "попытка создания базы из неполного типа".

Итак, давайте сделаем это. И нам также пришлось бы изменить конструктор, чтобы заполнить базовый класс, когда это возможно:

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!
    { }

А затем, благодаря базовому классу 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

Но тут становится хреново.

Если мы переместим определение Object::Proxy за пределы (фактически после) Object, исходное

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

... теперь выдает ошибку, потому что мы использовали неполный класс (Proxy). Обратите внимание, что простое перемещение определения наружу не устраняет тот факт, что возвращаемый тип — Proxy. Если бы только это было Proxy*, мы могли бы это сделать. Но Proxy не может.

Похоже, это Catch-22, и я не вижу никакого ясного решения.

Есть ли один?

РЕДАКТИРОВАТЬ: В ответ на комментарий, предлагающий ошибочный дизайн, имейте в виду, что Object - это легкая оболочка вокруг указателя. Он имеет только один член данных PyObject*.

РЕДАКТИРОВАТЬ: исходный код, над которым я работаю, можно найти здесь


person P i    schedule 28.12.2014    source источник
comment
Похоже, это Уловка-22... Да, и рекурсивный прокси изначально звучит странно. TL;ДР; У вас есть серьезный недостаток дизайна, как он выглядит для меня.   -  person πάντα ῥεῖ    schedule 28.12.2014
comment
@πάνταῥεῖ: Я думаю, что в фильме Primer главные герои построили машину времени, интерфейс которой был рекурсивным прокси. Я не помню, как это у них получилось в итоге.   -  person Kerrek SB    schedule 29.12.2014
comment
Управление потоком кода, который я пытаюсь переписать (здесь) напоминает мне сюжетную диаграмму для Primer< /а>.   -  person P i    schedule 29.12.2014


Ответы (2)


Ваша предпосылка кажется ошибочной. Proxy не является Object по определению; если бы это было так, то вы бы не назвали это Proxy в первую очередь. И тогда вы можете решить свою проблему без прокси, так же, как ее решают стандартные типы данных, такие как std::map: просто operator[] возвращает ссылку на вновь созданный Object, когда это необходимо.

Вы ищете что-то вроде шаблона прокси std::vector<bool>: operator[] возвращает Proxy с operator= и неявным преобразованием в непрокси Object (для случаев, когда вы действительно хотите использовать значение, а не присваивать ему).

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}; }
};
person Quuxplusone    schedule 28.12.2014
comment
Проблема, которую я получаю, заключается в том, что myobj[i].ptr() выдает ошибку Нет члена с именем 'ptr' в 'Py::Object::Proxy'. то есть myobj[i] не может преобразоваться в Object до применения .. Object содержит метод PyObject* ptr(){return p;}. - person P i; 29.12.2014
comment
Тогда реализуйте метод ptr() для Proxy. Верно? - person Quuxplusone; 29.12.2014
comment
Но в этом и заключается проблема — у Object есть десятки методов, и кажется очень уродливым необходимость вручную маршрутизировать каждый из них через Proxy. - person P i; 29.12.2014
comment
Что ж, вам придется каким-то образом обернуть каждый метод Object, даже если это просто установка this->p = PyObject_GetItem(this->container, this->key), потому что весь смысл использования прокси в том, что может быть никакого значимого значения для this->p. Теперь я сделал это более ясным выше, дав operator Object() кодовый путь броска. - person Quuxplusone; 29.12.2014

В конце концов я решил это.

Хитрость заключается в том, чтобы просто использовать класс как его собственный прокси.

Итак, если изначально объект Proxy обеспечивает преобразования, чтобы отличить доступ к Lvalue от Rvalue, я просто перемещаю эти преобразования обратно в свой исходный класс Object:

    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;
    }
person P i    schedule 18.01.2015