Шаблон адаптера: поддержка базовых данных, которые могут быть const или non-const, элегантно

Как сделать класс адаптера соответствующим образом поддерживающим как const, так и не const const?

Пример бетона

RigidBody - класс, описывающий физическое свойство объекта.
Вот его очень упрощенная версия (1D): -

class RigidBody{
    float position=1;
    public: float getPosition()const{ return position;}
    public: void setPosition(float ppos){ position=ppos;}
};

Adapter инкапсулирует RigidBody.
Он обеспечивает мало искаженную функциональность get/set position: -

class Adapter{
    public: RigidBody* rigid; int offset=2;
    public: float getPosition(){
        return rigid->getPosition()+offset;     //distort
    }
    public: void setPosition(float ppos){
        return rigid->setPosition(ppos-offset); //distort
    }
};

Я могу установить положение RigidBody косвенно, используя Adapter: -

int main() {
    RigidBody rigid;
    Adapter adapter;  //Edit: In real life, this type is a parameter of many function
    adapter.rigid=&rigid;
    adapter.setPosition(5);
    std::cout<<adapter.getPosition();//print 5
    return 0;
}

Все работает (демонстрация).

Цель

Я хочу создать новую функцию, которая получит const RigidBody* rigid.
Я должен уметь читать с него (например, getPosition()) с помощью адаптера.

Однако я не знаю, как это сделать элегантно.

void test(const RigidBody* rigid){
    Adapter adapter2; 
    //adapter2.rigid=rigid; //not work, how to make it work?
    //adapter2.setPosition(5); //should not work
    //adapter2.getPosition();  //should work 
}

Мои плохие решения

Решение A1 (2 адаптера + 1 виджет)

Создайте виджет: -

class AdapterWidget{
    public: static Adapter createAdapter(RigidBody* a);
    public: static AdapterConst createAdapter(const RigidBody* a);
};

AdapterConst может только getPosition(), а AdapterConst может быть и получен и установлен.

Я могу использовать его как: -

void test(const RigidBody* rigid){
    auto adapter=AdapterWidget::createAdapter(rigid);

Легко использовать.

Недостаток: Код AdapterConst и Adapter будет очень продублирован.

Решение A2 (+ наследование)

Это улучшение предыдущего решения.
Пусть Adapter (имеет setPosition()) получается из AdapterConst (имеет getPosition()).

Недостаток: Это не краткий. Я использую 2 класса для одной задачи!
Это может показаться тривиальным, но в большей базе кода это совсем не забавно.

В частности, расположение getPosition() будет далеким от setPosition(), например, в разных файлах.
Это вызывает проблему ремонтопригодности.

Решение B (шаблон)

Создайте класс шаблона. Есть много способов, например.: -

  • Adapter<T =RigidBody OR const RigidBody >
  • Adapter<bool=true is const OR false is non-const >

Недостаток: Во всех отношениях это неэлегантно. Это перебор. (?)
Я буду страдать от недостатка шаблона, например. все в заголовке.

Решение C1 (const_cast)

Я стараюсь избегать этого. Это зло.

class Adapter{
    public: RigidBody* rigid; 
    void setUnderlying(const RigidBody* r){
        rigid=const_cast< RigidBody*>(r);
    }
    ....
};

Решение C2 (+ manual assert)

Я могу добавить некоторое утверждение вручную.
Он просто подчеркивает, насколько это непрофессионально: -

    bool isConst;
    void setUnderlying(const RigidBody* r){
        ...
        isConst=true;
    }
    void setUnderlying(RigidBody* r){
        ...
        isConst=false;
    }
    void setPosition(float a){
        if(isConst){ /*throw some exception*/ }
        ....
    }

Решение D (убежать)

  • Lazy: измените с test( const RigidBody* rigid) на test(RigidBody* rigid).
  • Сумасшедший: измените RigidBody::setPosition() на const.

В любом случае моя программа больше не будет const -correct,
но достаточно одного класса Adapter.

Вопрос

Нужно ли мне делать одну из этих вещей везде, где я сталкиваюсь с шаблоном const/non-const?
Пожалуйста, предоставьте красивое решение. (полный код не требуется, но я не против)

Извините за длинный пост.

Изменить: В реальной жизни Adapter является параметром для многих функций.
Он проходит как игрушка.

Большинство таких функций не имеют знаний о RigidBody, поэтому не совсем удобно изменять из пакета, вызывающего someFunction(adapter) в someFunction(offset,rigidbody).

Ответ 1

Вы не должны придерживаться этой идеи. Это С++, а не Java.

Ваш код чрезвычайно ориентирован на Java. Я вижу это, как вы пишете код, используйте указатели и молча пропустите const, когда это необходимо.

На самом деле, большая часть плохого кода на С++, который я лично видел, в значительной степени написана как "C внутри классов" или "Java без GC". Оба из них - очень плохие способы написания кода на С++.

В вашем вопросе есть идиоматическое решение:

  • Снимите большинство шаблонов дизайна. они полезны для языков, где объект является ссылочным типом по умолчанию. С++ предпочитает большую часть времени проецировать объект как типы значений и предпочитает статический полиморфизм (шаблоны), а не полиморфизм времени выполнения (inherit + override).

  • Напишите два класса: один - Adapter, а один - ConstAdapter. Это то, что уже делает стандартная библиотека. по этой причине каждый контейнер имеет разные реализации iterator и const_iterator. вы можете либо хранить что-то по указателю, либо указателем const. Это ошибка, склонная к смешиванию двух. Если бы было хорошее решение для этой проблемы, у нас не было бы двух типов iterator для каждого контейнера.

Ответ 2

Плотно контролируемое const_cast выглядит как хорошее решение для меня:

// Only provides read-only access to the object
struct ConstAdapter {
    int offset = 2;

    // Constructible from const and non-const RigidBodies
    ConstAdapter(RigidBody const *rigid)
    : _rigid{rigid} { }

    // Read-only interface
    float getPosition() {
        return rigid()->getPosition() + offset;     //distort
    }

    // Hidden away for consistency with Adapter API
    // and to prevent swapping out an "actually non-const" RigidBody
    // for a "truly const" one (see  Adapter::rigid()`).
    RigidBody const *rigid() const { return _rigid; }

private:
    RigidBody const *_rigid;
};

// Inherits read-only functions, and provides write access as well
struct Adapter : ConstAdapter {

    // Only constructible from a non-const RigidBody!
    Adapter(RigidBody *rigid) : ConstAdapter{rigid} { }

    // Write interface
    void setPosition(float ppos){
        return rigid()->setPosition(ppos-offset); //distort
    }

    // Here the magic part: we know we can cast `const` away
    // from our base class' pointer, since we provided it ourselves
    // and we know it not actually `const`.
    RigidBody *rigid() const {
        return const_cast<RigidBody *>(ConstAdapter::rigid());
    }
};

Относительно:

В частности, расположение getPosition() будет находиться далеко от setPosition(), например, в разных файлах. Это вызывает проблему ремонтопригодности.

Это не проблема. С++, в отличие от Java, допускает несколько классов в одном файле, и вам на самом деле рекомендуется группировать такие тесно связанные классы вместе. Объявления функций будут только несколькими строками, и их определения могут быть сгруппированы вместе в соответствующем файле .cpp.

Ответ 3

Используйте ссылки вместо указателей, и пусть constness propogate соответствует Adapter. Тогда вы можете безопасно const_cast, так как

template <class Rigid>
class Adapter{
    Rigid & rigid;
    int offset;
public: 
    Adapter(Rigid & rigid, int offset = 2) : rigid(rigid), offset(offset) {}
    float getPosition() const { return rigid.getPosition() + offset; }
    void setPosition(float ppos) { rigid.setPosition(ppos - offset); }
};

Ответ 4

Моим первым инстинктом было бы получить доступ к RigidBody через Adaptor и a const RigidBody через a const Adaptor. Для этого мы используем factory для создания правильного типа Adaptor, а в реализации мы получаем доступ к базовому RigidBody через метод доступа, который (безопасно) выполняет только при разрешении.

Код:

Начните с частных членов:

class Adaptor
{
    RigidBody const& body;
    int offset;

    Adaptor(RigidBody body, int offset=2)
        : body(body),
          offset(offset)
    {}

    Adaptor(const Adaptor&) = delete;

Конструктор является закрытым, поэтому мы можем создавать экземпляры только через наш factory. И мы удаляем конструктор копирования, поэтому мы не можем создать non-const Adaptor из const const.

Затем у нас есть первая перегруженная пара - аксессор, используемый в реализации. Я отметил его protected, если вам нужен ряд аналогичных адаптеров. Если вы хотите, это может быть безопасно public.

protected:
    RigidBody& get_body() { return const_cast<RigidBody&>(body); }
    RigidBody const& get_body() const { return body; }

Мы знаем, что const_cast является безопасным, потому что мы получаем только неконстантный Adaptor из не-const RigidBody, как мы видим в другой перегруженной паре, которая является factory:

public:
    static Adaptor *adapt(RigidBody& body, int offset = 2) { return new Adaptor{ body, offset }; }
    static Adaptor const *adapt(RigidBody const& body, int offset = 2) { return new Adaptor{ body, offset }; }

Здесь каждый метод объявляется const, если ему не нужно изменять тело, как и следовало ожидать. Итак, если у вас есть const Adaptor (который вы будете, если вы построили его из const RigidBody, вы не можете вызвать какой-либо метод, который изменяет body. И реализация не может изменить body из любого из методы const.

    float getPosition() const
    {
        // this uses the const get_body()
        return get_body().getPosition() + offset;
    }

    void setPosition(float ppos)
    {
        // this uses the mutable get_body()
        get_body().setPosition(ppos-offset);
    }

Вы можете продемонстрировать безопасность, пытаясь изменить body в методе const:

   void illegal() const
   {
       get_body().setPosition(0); // error: passing ‘const RigidBody’ as ‘this’ argument discards qualifiers
       body.setPosition(0);       // error: passing ‘const RigidBody’ as ‘this’ argument discards qualifiers
   }

};

Демо:

При использовании RigidBody адаптер позволяет выполнять все операции; с константой RigidBody, адаптер допускает только операции const:

#include <memory>
int main()
{
    {
        RigidBody b;
        std::unique_ptr<Adaptor> a{Adaptor::adapt(b)};
        a->setPosition(15.);
        a->getPosition();
    }

    {
        RigidBody const b;
        std::unique_ptr<const Adaptor> a{Adaptor::adapt(b)};
        a->setPosition(15.);     // error: passing ‘const Adaptor’ as ‘this’ argument discards qualifiers
        a->getPosition();
    }
}