Модел на най-добра практика на C++ за използване на полиморфен член от данни

Кой е най-добрият практически модел за използване на полиморфен член от данни в 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;
}

Има ли по-добър начин да направите това? Опитвам се да го запазя просто, без да използвам оператор за ново/изтриване. (Забележка: трябва да може да има стойност по подразбиране)

АКТУАЛИЗАЦИЯ: Нека разгледаме по-практичен пример:

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

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

Целта е да има хора с различни характери и да се улесни добавянето на повече Личност чрез подкласиране. Какъв е най-добрият начин за прилагане на това? Тъй като логично се нуждаем само от един обект за всеки тип Личност, няма много смисъл Човек да копира или да поеме собственост върху обекта Личност. Може ли да има някакъв различен подход/дизайн, който не изисква от нас да се занимаваме с проблема с копирането/собствеността?


person leiiv    schedule 12.11.2010    source източник


Отговори (3)


В този случай е по-добре с отделни конструктори по подразбиране и копиране. Също така, не трябва да вземате адреса на нещо, предадено чрез препратка; може да е обект в стека, който скоро ще изчезне. В случай на вашата функция:

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.

person Mike DeSimone    schedule 12.11.2010
comment
+1 Хубав, ясен текст. Можете да използвате снизходителността на съвариантния тип връщане, за да имате функцията за клониране на извлечения обект да връща указатели към производните типове... просто запазва малко повече от познанията за типа на компилатора, документира по-добре какво се случва и понякога избягва преобразуване. - person Tony Delroy; 12.11.2010
comment
Слабостта на предаването на собствеността върху предадения Fruit е, че не можем да присвояваме един и същ обект много пъти. Например, имаме Apple* a = new Apple(some parameters); и искаме да направим Food f1(a);, Food f2(a); и т.н. И така, това означава ли, че стратегията за създаване на копие е по-добра? - person leiiv; 12.11.2010
comment
@Tony: Бихте ли ми дали пример за това? Може би публикуван като нов отговор? - person leiiv; 12.11.2010
comment
@leiiv: не си струва друг отговор, само малка промяна в типовете връщане, както в virtual Banana* clone() const { return new Banana(*this); }. Повторното присвояване много пъти, да - копирането решава този проблем и е по-безопасно. Но копирането на големи обекти може да бъде бавно, така че интелигентните указатели, използващи референтни броячи (вижте boost shared_ptr), могат да автоматизират процеса, но имайте предвид, че всички модификации на състоянието на плода ще се виждат и през двата указателя на Food. - person Tony Delroy; 12.11.2010
comment
@Тони: Благодаря, просто осъзнах какво имаш предвид. Слабостта на защитното копиране е производителността. Можем да използваме стратегия за копиране при запис, но това прави нещата още по-сложни. Така че изглежда, че няма свещен граал за този проблем. - person leiiv; 12.11.2010

Няма чист и прост начин да го направите без оператор new, защото различните обекти може да заемат различно количество място в паметта. Така че храната не може да съдържа самия плод, а само указател към плода, който от своя страна трябва да бъде разпределен в купчината. Ето някои алтернативи.

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

За да направите това ръчно, можете да накарате Food да приеме указател към Fruit и да поеме собствеността върху него, като извика delete в неговия деструктор ~Food() (или може да използва интелигентен указател като shared_ptr, scoped_ptr или auto_ptr, за да го изтрие автоматично). Трябва ясно да документирате, че предадената стойност ТРЯБВА да бъде разпределена с new. (Стойността по подразбиране ще представлява специално раздразнение; ще трябва или да я проверите, или да добавите флаг, или да разпределите копие на тази стойност също с operator new.)

Може да искате да можете да вземете всеки плод като аргумент и да го копирате в купчината. Това изисква повече работа, защото не знаете, когато правите копието, кой плод всъщност е. Един често срещан начин за решаването му включва добавяне на виртуален метод clone() към Fruit, който извлечените Fruits трябва да се прилагат по подходящ начин.

person DS.    schedule 12.11.2010
comment
Разбрах, че просто описах с думи решението на Mike DeSimone (публикувано по същото време). Това просто показва, че това наистина е обичайният начин за решаване на това. - person DS.; 12.11.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;
};

Полиморфизмът изисква да правите всичко с указатели (или препратки като определени параметри, но това не се отнася за вашия случай).

person chrisaycock    schedule 12.11.2010
comment
SetFruit(Fruit& f), f е просто препратка към предадения обект, така че мисля, че &f ще даде адреса на всеки предаден обект. Сигурен ли си за това? - person leiiv; 12.11.2010
comment
@leiiv Чувствайте се свободни да използвате &f и &a, ако сте скептични. - person chrisaycock; 12.11.2010
comment
Току-що го пробвах с помощта на прост тест и ми даде същия адрес. - person leiiv; 12.11.2010
comment
Предаването на обект по референция не прави локално копие, както го прави предаването по стойност. На ниво машинен код предаването на обект по препратка и предаването на указател по стойност са едно и също нещо и поради тази причина Objective-C не използва препратки, а само указатели. Основните семантични разлики между указатели и препратки са, че указателят може да бъде NULL (т.е. 0 може да бъде имплицитно прехвърлен към всеки тип указател), а препратката не може (тъй като може да бъде създадена само от съществуващ обект), а променливите на указателя могат да се променят докато препратките не могат. - person Mike DeSimone; 12.11.2010
comment
И, да, наясно съм, че можете да създадете нулева препратка чрез изрично преобразуване на типове (напр. Fruit& nullFruit = *(Fruit*)0);), но можете да подкопаете много други правила с изрични препратки на типове и да нарушите програмата си (напр. Food food; Fruit* fruitInFood = *(Fruit**)&food;, което може да изглежда правилно, но няма гаранция за работещ). - person Mike DeSimone; 12.11.2010