Я пытаюсь создать класс Collider, который будет обрабатывать столкновения между разными классами одной базы. Одно ограничение заключается в том, что я хочу, чтобы эти классы ничего не знали друг о друге, теперь они зависят только от классов Base и Collider. Вот мой код:
collidees.h
#pragma once
#include "collider.h"
#define ALLOW_COLLIDER_VISIT
virtual void visit(Collider& c) override { c.collide(*this); };
virtual void visit(Collider& c, Base& other) override { c.collide(*this, other); };
struct Base {
virtual ~Base() = default;
virtual void visit(Collider& c) = 0;
virtual void visit(Collider& c, Base& other) = 0;
};
struct A : Base {
ALLOW_COLLIDER_VISIT;
};
struct B : Base {
ALLOW_COLLIDER_VISIT;
};
struct C : Base {
ALLOW_COLLIDER_VISIT;
};
collider.h
#pragma once
#include <memory>
#include <iostream>
#define ADD_REVERSE_COLLISION(A, B)
template<>
inline void Collider::performCollision(B& b, A& a)
{
performCollision(a, b);
}
#define CALL_COLLIDER_FOR(CLASS)
void collide(CLASS& a) { collider.performCollision<T, CLASS>(static_cast<T&>(base), a); }
struct Base;
struct A;
struct B;
struct C;
struct Collider;
struct HelperBase {
HelperBase(Base& base_, Collider& collider_) : base { base_ }, collider { collider_ } {}
virtual ~HelperBase() = default;
virtual void collide(A& base) = 0;
virtual void collide(B& base) = 0;
virtual void collide(C& base) = 0;
Base& base;
Collider& collider;
};
template<typename T>
struct Helper;
struct Collider {
template<typename T>
void collide(T& t) {
if (helper) {
helper->collide
}
}
template<typename T>
void collide(T& t, Base& other) {
helper = std::make_unique<Helper<T>>(t, *this);
callOtherToVisit(other);
}
void callOtherToVisit(Base& other);
template<typename T1, typename T2>
void performCollision(T1& first, T2& second) {
std::cout << "No collision handlern";
}
std::unique_ptr<HelperBase> helper = nullptr;
};
template<typename T>
struct Helper : HelperBase {
Helper(T& base, Collider& c) : HelperBase { base, c } {}
CALL_COLLIDER_FOR(A);
CALL_COLLIDER_FOR(B);
CALL_COLLIDER_FOR(C);
};
template<>
inline void Collider::performCollision(A& a, B& b) {
std::cout << "Colliding a and bn";
}
ADD_REVERSE_COLLISION(A, B);
template<>
inline void Collider::performCollision(B& b1, B& b2) {
std::cout << "Colliding b1 and b2n";
}
template<>
inline void Collider::performCollision(A& a, C& c) {
std::cout << "Colliding a and cn";
}
ADD_REVERSE_COLLISION(A, C);
template<>
inline void Collider::performCollision(B& b, C& c) {
std::cout << "Colliding b and cn";
}
ADD_REVERSE_COLLISION(B, C);
collider.cpp
#include "collider.h"
#include "collidees.h"
void Collider::callOtherToVisit(Base& other)
{
other.visit(*this);
}
main.cpp — код драйвера
#include "collidees.h"
#include "collider.h"
int main()
{
Collider collider;
auto a = std::make_unique<A>();
auto b = std::make_unique<B>();
auto c = std::make_unique<C>();
a->visit(collider, *b);
a->visit(collider, *c);
c->visit(collider, *b);
c->visit(collider, *a);
b->visit(collider, *a);
return 0;
}
Я предполагаю, что у этого кода есть некоторые недостатки:
- Это довольно сложно; добавление нового столкновения влечет за собой добавление кода в нескольких местах;
- Вместо истинной диспетчеризации есть одно static_cast (хотя, полагаю, это безопасно);
- Можно позвонить
void visit(Collider& c)
себя, что бессмысленно; - Учебный класс
Helper
хранит ссылку на объект, который можно легко удалить, что приводит к сохранению висячей ссылки.
(Не стесняйтесь добавлять другие недостатки в комментариях)
Буду рад любым предложениям и улучшениям (или даже вашим версиям этого кода).
ОБНОВИТЬ: Ответы на комментарии:
- Я считаю обнаружение столкновений отдельным шагом. По крайней мере, в моем случае достаточно добавить данные формы (или даже просто прямоугольника) в базовый класс и найти пересечения двух объектов, поэтому производные по-прежнему ничего не знают друг о друге.
- Ключевые слова virtual и override в объявлении метода — это просто невнимательность.
- Постоянная правильность — я снова сосредоточился на основной проблеме и упустил ее, хотя, да,
performCollision
может изменять аргументы, поэтому они не могут быть константными.
В любом случае, спасибо за ответы. Думаю, я попытаюсь исправить свой код в соответствии с предложениями пользователя 673679, а затем попытаюсь включить std::variant
и std::visit
как упомянули оба комментатора. Я подожду еще немного, а потом получу ответ.
2 ответа
const
правильность:
Есть много из const
отсутствует.
Ни одна из функций-членов не изменяет данные-члены (в классах фигур или помощниках), поэтому все они должны быть отмечены
const
. напримерvirtual void visit(Collider& c) const = 0;
Ни один из аргументов функции, переданных по ссылке (классам форм или помощникам), не изменяется, поэтому они должны передаваться
const&
вместо. напримерvirtual void collide(A const& base) = 0;
(Примечание: я предполагаю, что если вы намеревались реализовать ответ на столкновение (т.е. изменить объекты) внутри performCollision
вызовы, тогда вам придется оставить вещи неконстантными. Вместо этого я предлагаю вернуть достаточно информации (например, в виде std::optional<CollisionData>
), чтобы позже разрешить конфликт как отдельный шаг. Это более гибкий подход (например, он позволяет обрабатывать конфликты в группах для оптимизации или точности)).
ненужное выделение:
Нам не нужно хранить Helper
в unique_ptr
. Вместо того, чтобы хранить ссылку на Base
класс в HelperBase
, мы можем сохранить ссылку на класс шаблона в шаблоне Helper
класс, например:
template<class T>
struct Helper: HelperBase
{
Helper(T const& t): t
T const& t;
...
};
С Helper
теперь сохраняет тип первого аргумента, который нам больше не нужен static_cast
.
Затем мы можем создать Helper
соответствующего типа прямо в классе формы visit
функции, например:
struct A : Base
{
void visit(Shape const& other) const override
{
auto helper = Helper<A>(*this);
other.visit(helper);
}
void visit(HelperBase const& helper) const override
{
helper.collide(*this);
}
};
Это позволяет избежать выделения кучи и означает, что на самом деле нам не нужен Collider
учебный класс. 🙂
Мы могли бы передать тип класса формы объекту ALLOW_COLLIDER_VISIT
макрос, или добавьте make_helper<T>(*this)
функция, так что нам не пришлось.
ненужная специализация шаблона:
Нам не нужно использовать специализацию шаблона для функций столкновения:
template<typename T1, typename T2>
void performCollision(T1& first, T2& second) ...
Мы можем использовать простую перегрузку:
void performCollision(A const& , A const& ) { std::cout << "(A, A)n"; }
void performCollision(B const& , B const& ) { std::cout << "(B, B)n"; }
void performCollision(C const& , C const& ) { std::cout << "(C, C)n"; }
void performCollision(A const& , B const& ) { std::cout << "(A, B)n"; }
... etc.
пример:
Чтобы проиллюстрировать вышеизложенное:
#include <iostream>
struct A;
struct B;
struct C;
void collide(A const& , A const& ) { std::cout << "(A, A)n"; }
void collide(B const& , B const& ) { std::cout << "(B, B)n"; }
void collide(C const& , C const& ) { std::cout << "(C, C)n"; }
void collide(A const& , B const& ) { std::cout << "(A, B)n"; }
void collide(B const& , A const& ) { std::cout << "(B, A)n"; }
void collide(A const& , C const& ) { std::cout << "(A, C)n"; }
void collide(C const& , A const& ) { std::cout << "(C, A)n"; }
void collide(C const& , B const& ) { std::cout << "(C, B)n"; }
void collide(B const& , C const& ) { std::cout << "(B, C)n"; }
struct CollisionDispatcherBase
{
virtual void collide(A const& other) const = 0;
virtual void collide(B const& other) const = 0;
virtual void collide(C const& other) const = 0;
};
template<class T>
struct CollisionDispatcher : CollisionDispatcherBase
{
CollisionDispatcher(T const& t): t
T const& t;
void collide(A const& other) const override { ::collide(t, other); };
void collide(B const& other) const override { ::collide(t, other); };
void collide(C const& other) const override { ::collide(t, other); };
};
struct Shape
{
virtual ~Shape() {}
virtual void visit(Shape const& other) const = 0;
virtual void visit(CollisionDispatcherBase const& c) const = 0;
};
struct A : Shape
{
void visit(Shape const& other) const override { auto c = CollisionDispatcher<A>{ *this }; other.visit(c); }
void visit(CollisionDispatcherBase const& c) const override { c.collide(*this); }
};
struct B : Shape
{
void visit(Shape const& other) const override { auto c = CollisionDispatcher<B>{ *this }; other.visit(c); }
void visit(CollisionDispatcherBase const& c) const override { c.collide(*this); }
};
struct C : Shape
{
void visit(Shape const& other) const override { auto c = CollisionDispatcher<C>{ *this }; other.visit(c); }
void visit(CollisionDispatcherBase const& c) const override { c.collide(*this); }
};
#include <vector>
#include <memory>
int main()
{
auto colliders = std::vector<std::unique_ptr<Shape>>();
colliders.push_back(std::make_unique<A>());
colliders.push_back(std::make_unique<B>());
colliders.push_back(std::make_unique<C>());
for (auto i = std::size_t{ 0 }; i != colliders.size() - 1; ++i)
for (auto j = i + 1; j != colliders.size(); ++j)
colliders[i]->visit(*colliders[j]);
}
вместо этого использовать std :: variant?
Если используемая вами версия C ++ поддерживает std::variant
, мы можем отказаться от наследования и использовать std::visit
сделать для нас двойную отправку:
#include <iostream>
#include <variant>
#include <vector>
struct A { };
struct B { };
struct C { };
struct collide_visitor
{
void operator()(A const& , A const& ) const { std::cout << "(A, A)n"; }
void operator()(B const& , B const& ) const { std::cout << "(B, B)n"; }
void operator()(C const& , C const& ) const { std::cout << "(C, C)n"; }
void operator()(A const& , B const& ) const { std::cout << "(A, B)n"; }
void operator()(B const& , A const& ) const { std::cout << "(B, A)n"; }
void operator()(A const& , C const& ) const { std::cout << "(A, C)n"; }
void operator()(C const& , A const& ) const { std::cout << "(C, A)n"; }
void operator()(C const& , B const& ) const { std::cout << "(C, B)n"; }
void operator()(B const& , C const& ) const { std::cout << "(B, C)n"; }
};
int main()
{
using collider = std::variant<A, B, C>;
auto colliders = std::vector<collider>();
colliders.push_back(A{});
colliders.push_back(B{});
colliders.push_back(C{});
for (auto i = std::size_t{ 0 }; i != colliders.size() - 1; ++i)
for (auto j = i + 1; j != colliders.size(); ++j)
std::visit(collide_visitor{}, colliders[i], colliders[j]);
}
Всего один совет по кодированию:
virtual void visit(Collider& c) override
Использовать ровно один из virtual
или override
. То есть вы не повторяете virtual
по переопределениям.