Класс Collider, использующий какой-то шаблон посетителя

Я пытаюсь создать класс 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;
}

Я предполагаю, что у этого кода есть некоторые недостатки:

  1. Это довольно сложно; добавление нового столкновения влечет за собой добавление кода в нескольких местах;
  2. Вместо истинной диспетчеризации есть одно static_cast (хотя, полагаю, это безопасно);
  3. Можно позвонить void visit(Collider& c) себя, что бессмысленно;
  4. Учебный класс Helper хранит ссылку на объект, который можно легко удалить, что приводит к сохранению висячей ссылки.

(Не стесняйтесь добавлять другие недостатки в комментариях)

Буду рад любым предложениям и улучшениям (или даже вашим версиям этого кода).

ОБНОВИТЬ: Ответы на комментарии:

  • Я считаю обнаружение столкновений отдельным шагом. По крайней мере, в моем случае достаточно добавить данные формы (или даже просто прямоугольника) в базовый класс и найти пересечения двух объектов, поэтому производные по-прежнему ничего не знают друг о друге.
  • Ключевые слова virtual и override в объявлении метода — это просто невнимательность.
  • Постоянная правильность — я снова сосредоточился на основной проблеме и упустил ее, хотя, да, performCollision может изменять аргументы, поэтому они не могут быть константными.

В любом случае, спасибо за ответы. Думаю, я попытаюсь исправить свой код в соответствии с предложениями пользователя 673679, а затем попытаюсь включить std::variant и std::visit как упомянули оба комментатора. Я подожду еще немного, а потом получу ответ.

2 ответа
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 по переопределениям.

      Добавить комментарий

      Ваш адрес email не будет опубликован. Обязательные поля помечены *