Шаблон адаптера: поддержка базовых данных, которые могут быть 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();
}
}