C ++: шаблон наилучшей практики использования члена с полиморфными данными - PullRequest
3 голосов
/ 12 ноября 2010

Каков наилучший метод использования члена полиморфных данных в C ++? Самый простой из известных мне - просто использовать простой указатель.

Например, если тип элемента данных не является полиморфным:

class Fruit {
  public:
    int GetWeight() { return 1; }
};

class Food {
  public:
    Food(Fruit f=Fruit()) : m_fruit(f) {}
    void SetFruit(Fruit f) { m_fruit = f; }
    Fruit GetFruit() { return m_fruit; }
    int Test() { return m_fruit.GetWeight(); }
  private:
    Fruit m_fruit;
};
// Use the Food and Fruit classes
Food food1; //global scope
void SomeFunction() {
  Fruit f;
  food1.SetFruit(f);
}
int main() {
  Fruit fruit1, fruit2;
  Food food2(fruit1);
  food2.SetFruit(fruit2);
  food2.SetFruit(Fruit());
  SomeFunction();
  food1.Test();
  food2.Test();
  return 0;
}

Легко использовать классы разными способами с минимальной вероятностью ошибок.

Теперь, если мы сделаем Fruit базовым классом (используйте полиморфный), это самый простой / лучший из всех, что я могу придумать:

class Fruit {
  public:
    virtual int GetWeight() { return 0; }
};

class Apple : public Fruit {
  public:
    virtual int GetWeight() { return 2; }
};

class Banana : public Fruit {
  public:
    virtual int GetWeight() { return 3; }
};

class Food {
  public:
    Food(Fruit& f=defaultFruit) : m_fruit(&f) {}
    void SetFruit(Fruit& f) { m_fruit = &f; }
    Fruit& GetFruit() { return *m_fruit; }
    int Test() { return m_fruit->GetWeight(); }
  private:
    Fruit* m_fruit;
    static const Fruit m_defaultFruit = Fruit();
};
// Use the Food and Fruit classes
Food food1; //global scope
void SomeFunction() {
  Apple a;
  food1.SetFruit(a);
}

int main() {
  Apple a;
  Banana b;
  Food food2(a);
  food2.SetFruit(b);
  food2.SetFruit(Apple()); //Invalid ?!
  SomeFunction(); //Weakness: the food1.m_fruit points to invalid object now.
  food1.Test();   //Can the compiler detect this error? Or, can we do something to assert m_fruit in Food::Test()?
  food2.Test();
  return 0;
}

Есть ли лучший способ сделать это? Я стараюсь сделать это простым, не используя никаких новых / удаленных утверждений. (Примечание: должно иметь значение по умолчанию)

UPDATE: Давайте рассмотрим более практичный пример:

class Personality {
public:
  void Action();
};
class Extrovert : public Personality {
  ..implementation..
};
class Introvert : public Personality {
  ..implementation..
};

class Person {
  ...
  void DoSomeAction() { personality->Action(); }
  ...
  Personality* m_personality;
};

Цель состоит в том, чтобы иметь людей с разными личностями и облегчить добавление большей Личности с помощью подклассов. Каков наилучший способ реализовать это? Поскольку по логике нам нужен только один объект для каждого типа Личности, Личность не имеет особого смысла копировать или брать на себя ответственность за объект Личности. Может ли существовать какая-либо разница между подходом / дизайном, который не требует от нас решения проблемы копирования / владения?

Ответы [ 3 ]

3 голосов
/ 12 ноября 2010

В этом случае вам лучше использовать отдельные конструкторы по умолчанию и копировать. Кроме того, вы не должны брать адрес чего-либо, переданного по ссылке; это может быть объект в стеке, который скоро исчезнет. В случае вашей функции:

void SomeFunction() {
  Apple a;
  food1.SetFruit(a);
}

a (и, следовательно, &a) также становится недействительным, когда функция возвращается, поэтому food1 будет содержать недопустимый указатель.

Поскольку вы храните указатель, вы должны ожидать, что Food получит право собственности на переданный Fruit.

class Food {
  public:
    Food() : m_fruit(new Fruit()) {}
    Food(Fruit* f) : m_fruit(f) {}
    void SetFruit(Fruit* f) { if(m_fruit != f) { delete m_fruit; m_fruit = f; } }
    Fruit& GetFruit() { return *m_fruit; }
    const Fruit& GetFruit() const { return *m_fruit; }
    int Test() { return m_fruit->GetWeight(); }
  private:
    Fruit* m_fruit;
};

Другой вариант - для Food сделать копию переданного Fruit. Это сложно. В старые времена мы определяли Fruit::clone() метод, который подклассы переопределяли бы:

class Fruit {
  public:
    virtual int GetWeight() { return 0; }
    virtual Fruit* clone() const { return new Fruit(*this); }
};

class Apple : public Fruit {
  public:
    virtual int GetWeight() { return 2; }
    virtual Fruit* clone() const { return new Apple(*this); }
};

class Banana : public Fruit {
  public:
    virtual int GetWeight() { return 3; }
    virtual Fruit* clone() const { return new Banana(*this); }
};

Я не думаю, что вы можете сделать виртуальный конструктор так, как вам нужно, чтобы сделать его красивее.

Тогда класс Food изменится на:

class Food {
  public:
    Food() : m_fruit(new Fruit()) {}
    Food(const Fruit& f) : m_fruit(f.clone()) {}
    void SetFruit(const Fruit& f) { delete m_fruit; m_fruit = f.clone(); }
    Fruit& GetFruit() { return *m_fruit; }
    const Fruit& GetFruit() const { return *m_fruit; }
    int Test() { return m_fruit->GetWeight(); }
  private:
    Fruit* m_fruit;
};

При этой настройке ваш SomeFunction будет работать, поскольку food1 больше не зависит от существования a.

1 голос
/ 12 ноября 2010

Нет чистого и простого способа сделать это без оператора new, потому что разные объекты могут занимать разное количество места в памяти. Таким образом, Food не может содержать сам Fruit, а только указатель на Fruit, который, в свою очередь, должен быть размещен в куче. Итак, вот несколько альтернатив.

Взгляните на библиотеку boost :: any . Он существует именно для того, чтобы решить вашу проблему. Недостатком является то, что он добавляет много зависимостей Boost, и, хотя он может сделать ваш код простым, его будет труднее понять тем, кто не знаком с библиотекой. Внизу, конечно, он все еще делает новый / удалить.

Чтобы сделать это вручную, вы можете заставить Food принимать указатель на Fruit и вступать во владение им, вызывая delete в его деструкторе ~Food() (или он может использовать умный указатель, такой как shared_ptr, scoped_ptr или auto_ptr, для удаляется автоматически). Вы должны четко документировать, что переданное значение ДОЛЖНО быть присвоено с новым. (Значение по умолчанию будет вызывать особое раздражение; вам придется либо проверить его, либо добавить флаг, либо выделить копию этого значения также с помощью operator new.)

Возможно, вы захотите использовать любой Фрукт в качестве аргумента и скопировать его в кучу. Это требует больше работы, потому что вы не знаете, когда делаете копию, какой это на самом деле Fruit. Один из распространенных способов ее решения заключается в добавлении виртуального метода clone () к Fruit, который производные Fruits должны реализовать соответствующим образом.

0 голосов
/ 12 ноября 2010

Ваш SetFruit() назначает f адрес m_fruit, а не адрес того, что было передано. Поскольку f является просто аргументом функции, он существует в стеке только до тех пор, пока SetFruit() не вернет , После этого f уходит и его адрес больше не действителен. Таким образом, m_fruit указывает на то, чего не существует.

Так что вместо этого сделайте что-то вроде этого:

class Food {
  public:
    Food(Fruit* f = &defaultFruit) : m_fruit(f) {}
    void SetFruit(Fruit* f) { m_fruit = f; }
    Fruit* GetFruit() { return m_fruit; }
    int Test() { return m_fruit->GetWeight(); }
  private:
    Fruit* m_fruit;
    static const Fruit m_defaultFruit;
};

Полиморфизм требует, чтобы вы делали все с помощью указателей (или ссылались как на определенные параметры, но это не относится к вашему случаю).

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