Конвертировать между дочерними объектами при использовании полиморфизма? - PullRequest
2 голосов
/ 04 июня 2019

У меня есть проблема, которая, вероятно, связана с моим дизайном, но я хотел бы получить предложения о том, как ее улучшить. По сути, у меня есть родительский класс, который имеет несколько дочерних классов, и мне нужно иметь возможность конвертировать между дочерними классами. Однако, объект, который будет преобразован в моем текущем проекте, использует полиморфизм и указатель на родительский класс. Я делаю это, потому что, в конце концов, какой дочерний класс используется, определяется пользовательским вводом. Я нашел три способа решения этой проблемы:

  1. Реализация отдельного класса "конвертеров", который может брать каждый дочерний класс и преобразовывать его в другой дочерний класс.
  2. Объявите виртуальные функции, которые преобразуют каждый дочерний класс в другие дочерние классы. (Это создаст круговые зависимости, которые я не считаю хорошей идеей ...)
  3. Включите элемент данных enum в объекты, которые говорят, к какому типу они относятся, чтобы я мог использовать оператор switch () при преобразовании.

Есть ли другие способы, которыми я должен думать об этом? Вот код, который, я думаю, показывает, что я хочу сделать.

class Rotation
{
   public:
      void  Set();
      Vector Get();
      virtual void Rotate(float amount);
      virtual void SetFromQuaternion(Vector t_quaternion);
   private:
      Vector m_rotation;
}

class EulerAngles : Rotation
{
   public:
     void Rotate(float t_amount);
     void SetFromQuaternion(Vector t_quaternion);
}

class Quaternion: Rotation
{ 
  public:
     void Rotate(float t_amount);
     void SetFromQuaternion(Vector t_quaternion);//Just copies data
}
class RigidBody
{
  public:
     RigidBody(Rotation *t_rotation);
     Rotation GetRotation();
     void SetRotationFromQuaternion(Vector t_rotation) {m_rotation->SetRotationFromQuaternion(t_rotation);}
  private:
     std::unique_ptr<Rotation> *m_rotation;
}
int main()
{
   //Argument is based on user input file, but euler angles are used as an example
   Rigidbody body_1 = RigidBody(new EulerAngles());
   // I want to rotate using quaternions to avoid singularities, but then convert back to euler angles. So here's where I would convert. How should I do this?
   Quaternion temp_quaternion = (Quaternion)body_1.GetRotation();
   temp_quaternion.Rotate(amount);
   body_1.SetRotationFromQuaternion(temp_quaternion.Get());

   return;
}

Обратите внимание, что мой настоящий код более сложный. Мой вопрос больше касается общего дизайна "лучших практик". Заранее спасибо!

Ответы [ 2 ]

0 голосов
/ 04 июня 2019

Ваша основная проблема в том, что вы пытаетесь решить что-то, используя ООП, где ОО абсолютно не требуется!Цель объектной ориентации:

  • разделение интересов
  • описание иерархий классов

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

  • Почему ваш RigidBody хранит информацию о ротации?Это должно быть просто тело, а ротация должна преобразовывать лежащие в его основе данные.
  • Почему все ваши классы имеют функцию SomethingFromQuaternion?Если вы не можете установить его с помощью EulerAngles, вам не следует добавлять его к имени.Кроме того, вращение связано с Quaternion, оно не должно появляться как функция-член.

Правильный путь

Правильный путь крешить вашу проблему - реализовать библиотеку типов вращения и функций для работы с этими типами.

//We need a forward declaration of Quaternion in order to get the conversion
//operator working.
struct Quaternion;

//All of the classes (probably) can be struct as they do not save any private
//data members.
struct EulerAngles
{
    //members ...
    //constructors...
    //we declare a user defined conversion function
    explicit operator Quaternion() const;
};
struct Quaternion
{
    //members ...
    //constructors...
    //we declare a user defined conversion function
    explicit operator EulerAngles() const;
};
struct RigidBody
{
    //members ...
    //constructors...
    //we need a friend declaration to make our life easier
    friend auto inplace_rotate(RigidBody&,const Quaternion&) -> void;
};
//We actually need to define the conversion after the class declaration,
//or the compiler complains...
Quaternion::operator EulerAngles() const
{
    //add actual conversion here...
    return EulerAngles{};
}
EulerAngles::operator Quaternion() const
{
    //add actual conversion here...
    return Quaternion{};
}
//the actual rotation function
auto inplace_rotate(RigidBody& body, const Quaternion& rotation) -> void
{
    //implement...
    //notice the friend declaration in the RigidBody class, which gives us direct access.
    //(which we need in the case of private variables)
}
auto rotate(RigidBody body, const Quaternion& rotation) -> RigidBody
{
    //using the body without a reference, gives us the copy we need.
    //we are left with our rotation.
    inplace_rotate(body, rotation);
    return body;
}

int main()
{
   auto body = RigidBody{};
   //You need to get this from the user input.
   auto euler_rotation = EulerAngles{};
   //Using the user defined conversion operator.
   auto quaternion = static_cast<Quaternion>(euler_rotation);
   //Do the rotation.
   inplace_rotate(body, quaternion);

   return 0;
}

Этот подход, по-видимому, не отличается от того, что вы написали, но он резко сократил количество взаимосвязанностимежду классами.Каждый класс теперь можно просматривать самостоятельно и без учета всех остальных.Кроме того, это позволяет нам определять преобразования, используя способ c ++, с определенными пользователем конвертерами.(https://en.cppreference.com/w/cpp/language/cast_operator) Вытаскивая функцию вращения и делая ее автономной, мы можем легко расширить ее в одном месте (вам не нужно обновлять классы тела, чтобы вносить в нее изменения).

InКроме того, вы могли бы даже определить простые вспомогательные перегрузки для вашей EulerAngles:

auto inplace_rotate(RigidBody& body, const EulerAngles& rotation)
{
    return inplace_rotate(body, static_cast<Quaternion>(rotation));
    //technically returning here is unnecessary because the return type is void,
    //but this allows you to consistenly change the inplace_function at any time
    //without needing to update the convenience funtion
}
auto rotate(RigidBody body, const EulerAngles& rotation)
{
    return inplace_rotate(body, static_cast<Quaternion>(rotation));
}

Ваша настоящая проблема?

Я думаю, что путаница возникает из-за вашей попытки получитьпользовательский ввод работает как для Quaternions, так и для EulerAngles. Существует несколько подходов к решению этой проблемы:

  1. Напишите пользовательскую функцию, которая возвращает уже повернутые тела.
  2. Использование std::variant (c ++ 17) или альтернатива boost.
  3. Возврат к наследованию классов.

Я бы порекомендовал использовать 1 или 2, однако, если это невозможно,Вы можете сделать следующее (3):

#include <string>
#include <memory>

//An empty base class for rotation values.
struct Rotation {};

struct EulerAngles : Rotation
{
    //...
};
struct Quaternion : Rotation
{
    //...
};

auto parse_user_input(const std::string& input)
{
    if (input == "euler")
        return std::make_unique<Rotation>(EulerAngles{});
    else if (input == "quaternion")
        return std::make_unique<Rotation>(Quaternion{});
    else
        throw;
}
0 голосов
/ 04 июня 2019

Если вы используете полиморфизм, единственное, что вы можете сделать, это вернуть исходный тип с dynamic_cast. Пример:

Rotation * r = new Quaternion;
Quaternion * q = dynamic_cast<Quaternion*>(r);

Теперь, если вы хотите преобразовать один дочерний класс в другой, я думаю, вы уже знаете, что dynamic_cast потерпит неудачу, потому что внутренний тип не EulerAngles, а Quaternion.

Чтобы сделать это, насколько я знаю, у вас нет выбора, вы должны конвертировать его самостоятельно. Другими словами, напишите функцию преобразователя.

Идея добавить элемент данных для указания типа объекта довольно хороша, но этого недостаточно. В любом случае, если у вас есть Rotation*, который на самом деле является Quaternion*, вам все равно понадобится конвертер, чтобы получить EulerAngles* из него.

Исходя из вашей идеи, вы можете написать:

// Here, I assumed that you will make the Rotation class pure virtual (I know that you didn't, but I think you should, and this only is an example)
enum class ROTATION_T {QUATERNION, EULERANGLES};

Rotation * convert(const Rotation * r)
{
   // We assume here than the class Rotation has the attribute rotation_type saying the type.
   if(r->rotation_type == ROTATION_T::QUATERNION)
   {
      // convert the Quaternion and return a EulerAngles*
   }
   else
   {
      // convert the EulerAngles and return a Quaternion*
   }
}

Я никогда не буду притворяться, что это единственное решение, но это способ сделать это.

Я надеюсь, что это может / поможет вам.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...