Szablon czy abstrakcyjna klasa bazowa?

Jeśli chcę, aby klasa była adaptowalna i umożliwiała wybieranie różnych algorytmów z zewnątrz -- jaka jest najlepsza implementacja w C++?

Widzę głównie dwie możliwości:

  • Użyj abstrakcyjnej klasy bazowej i przekaż konkretny obiekt w
  • Użyj szablonu

Oto mały przykład, zaimplementowany w różnych wersjach:

Wersja 1: abstrakcyjna klasa bazowa

class Brake {
public: virtual void stopCar() = 0;  
};

class BrakeWithABS : public Brake {
public: void stopCar() { ... }
};

class Car {
  Brake* _brake;
public:
  Car(Brake* brake) : _brake(brake) { brake->stopCar(); }
};

Wersja 2a: Szablon

template<class Brake>
class Car {
  Brake brake;
public:
  Car(){ brake.stopCar(); }
};

Wersja 2b: szablon i dziedziczenie prywatne

template<class Brake>
class Car : private Brake {
  using Brake::stopCar;
public:
  Car(){ stopCar(); }
};

Wychodząc z Javy, jestem oczywiście skłonny zawsze używać wersji 1, ale wersje szablonów wydają się być preferowane często, np. w kodzie STL? Jeśli to prawda, to tylko ze względu na wydajność pamięci itp (brak dziedziczenia, brak wywołań funkcji wirtualnych)?

Zdaję sobie sprawę, że nie ma dużej różnicy między wersją 2A A 2b, zobacz C++ FAQ.

Czy możesz skomentować te możliwości?

Author: richq, 2009-03-02

9 answers

To zależy od twoich celów. Możesz użyć wersji 1, Jeśli

  • zamiar wymiany hamulców samochodu (w czasie pracy)
  • zamierzamy przekazać samochód do funkcji nie szablonowych

Ogólnie wolałbym wersję 1 używając polimorfizmu runtime, ponieważ jest on nadal elastyczny i pozwala na to, aby samochód nadal miał ten sam typ: {[2] } jest innym typem niż Car<Nissan>. Jeśli Twoje cele to świetna wydajność podczas częstego korzystania z hamulców, polecam podejście template. Nawiasem mówiąc, nazywa się to projekt oparty na zasadach. Zapewniasz politykę hamulcową. Przykład ponieważ powiedziałeś, że programujesz w Javie, prawdopodobnie nie jesteś jeszcze zbyt doświadczony w C++. Jeden sposób:

template<typename Accelerator, typename Brakes>
class Car {
    Accelerator accelerator;
    Brakes brakes;

public:
    void brake() {
        brakes.brake();
    }
}

Jeśli masz wiele zasad, możesz je grupować w ich własną strukturę i przekazać ją, na przykład jako SpeedConfiguration collecting Accelerator, Brakes i jeszcze trochę. W moich projektach staram się zachować dużą ilość szablonów kodu, dzięki czemu mogą być skompilowane raz do własnych plików obiektowych, bez konieczności umieszczania kodu w nagłówkach, ale nadal pozwalające na polimorfizm (za pomocą funkcji wirtualnych). Na przykład, możesz chcieć zachować wspólne dane i funkcje, które kod inny niż szablon będzie prawdopodobnie wywoływał wiele razy w klasie bazowej:

class VehicleBase {
protected:
    std::string model;
    std::string manufacturer;
    // ...

public:
    ~VehicleBase() { }
    virtual bool checkHealth() = 0;
};


template<typename Accelerator, typename Breaks>
class Car : public VehicleBase {
    Accelerator accelerator;
    Breaks breaks;
    // ...

    virtual bool checkHealth() { ... }
};

Nawiasem mówiąc, jest to również podejście, którego używają strumienie C++: std::ios_base zawiera flagi i rzeczy, które nie zależą od typu znaku lub cech, takich jak openmode, flagi formatowania i takie tam, podczas gdy std::basic_ios jest to szablon klasy, który go dziedziczy. Zmniejsza to również nadmuchiwanie kodu poprzez współdzielenie kodu, który jest wspólny dla wszystkich instancji szablonu klasy.

Prywatny Spadek? Należy unikać dziedziczenia prywatnego. Jest to bardzo rzadko przydatne, a zabezpieczenie jest lepszym pomysłem w większości przypadków. Często zdarza się, że jest odwrotnie, gdy rozmiar jest naprawdę kluczowy( na przykład klasa łańcuchowa oparta na zasadach): Optymalizacja pustej klasy bazowej może mieć zastosowanie w przypadku wyprowadzania z pustej polityki klasa (tylko zawierająca funkcje).

Przeczytaj wykorzystanie i nadużycia dziedziczenia przez Herb Sutter.

 33
Author: Johannes Schaub - litb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-02 16:01:45

Zasada kciuka brzmi:

1) jeśli wybór konkretnego typu jest dokonywany w czasie kompilacji, preferuj szablon. Będzie to bezpieczniejsze (błędy czasu kompilacji vs błędy czasu uruchamiania) i prawdopodobnie lepiej zoptymalizowane. 2) jeśli wybór zostanie dokonany w czasie wykonywania (tj. w wyniku działania użytkownika), nie ma wyboru-Użyj funkcji dziedziczenia i wirtualnych.

 32
Author: Nemanja Trifunovic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-02 17:24:30

Inne opcje:

  1. Użyj Visitor Pattern (pozwól, aby Zewnętrzny kod działał na twojej klasie).
  2. Uzewnętrznij część swojej klasy, na przykład poprzez Iteratory, że ogólny kod oparty na iteratorze może na nich działać. Działa to najlepiej, jeśli obiekt jest kontenerem innych obiektów.
  3. Zobacz także Wzór strategii (w środku znajdują się przykłady c++)
 6
Author: Assaf Lavie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-02 14:58:12

Szablony są sposobem na to, aby Klasa używała zmiennej, której Typ tak naprawdę nie obchodzi. Dziedziczenie jest sposobem na zdefiniowanie, co klasa jest oparta na jej atrybutach. Jest to pytanie "jest-a" kontra "ma-a".

 5
Author: John Ellinwood,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-02 14:57:47

Większość z twoich pytań została już udzielona, ale chciałem rozwinąć ten fragment:

Pochodzę z Javy, jestem naturalnie skłonni zawsze korzystać z wersji 1, ale wersje szablonów wydają się być preferowane często, np. w kodzie STL? Jeśli to prawda, czy to tylko z powodu wydajność pamięci itp. (brak dziedziczenia, żadnych wirtualnych wywołań funkcji)?

To część tego. Ale innym czynnikiem jest dodatkowe bezpieczeństwo typu. Kiedy traktujesz BrakeWithABS jako Brake, przegrywasz wpisz informacje. Nie wiesz już, że obiekt jest w rzeczywistości BrakeWithABS. Jeśli jest to parametr szablonu, masz dokładnie dostępny typ, który w niektórych przypadkach może umożliwić kompilatorowi lepsze sprawdzanie typów. Lub może być przydatne w zapewnieniu, że zostanie wywołane prawidłowe przeciążenie funkcji. (jeśli stopCar() przekaże obiekt hamulca do drugiej funkcji, która może mieć osobne przeciążenie dla BrakeWithABS, nie będzie wywołana, jeśli użyłeś dziedziczenia, a twoje BrakeWithABS zostało rzucone do Brake.

Innym czynnikiem jest to, że pozwala na większą elastyczność. Dlaczego wszystkie implementacje hamulców muszą dziedziczyć z tej samej klasy bazowej? Czy klasa bazowa ma coś do zaoferowania? Jeśli napiszę klasę, która ujawnia oczekiwane funkcje członka, czy nie jest to wystarczająco dobre, aby działać jako hamulec? Często jawne użycie interfejsów lub abstrakcyjnych klas bazowych ogranicza kod bardziej niż jest to konieczne.

(Uwaga, Nie mówię, że szablony powinny być zawsze preferowane rozwiązanie. Istnieją inne obawy, które mogą mieć na to wpływ, od szybkości kompilacji po "to, co programiści z mojego zespołu są zaznajomieni" lub po prostu "to, co preferuję". I czasami, potrzebujesz polimorfizmu uruchomieniowego, w którym to przypadku rozwiązanie szablonu po prostu nie jest możliwe)

 4
Author: jalf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-02 16:00:15

Ta odpowiedź jest mniej lub bardziej poprawna. Jeśli chcesz coś parametryzować w czasie kompilacji-powinieneś preferować szablony. Jeśli chcesz, aby coś parametryzowano w czasie wykonywania, powinieneś preferować nadpisywanie funkcji wirtualnych.

jednak używanie szablonów nie wyklucza wykonania obu (uelastycznienie wersji szablonu):

struct Brake {
    virtual void stopCar() = 0;
};

struct BrakeChooser {
    BrakeChooser(Brake *brake) : brake(brake) {}
    void stopCar() { brake->stopCar(); }

    Brake *brake;
};

template<class Brake>
struct Car
{
    Car(Brake brake = Brake()) : brake(brake) {}
    void slamTheBrakePedal() { brake.stopCar(); }

    Brake brake;
};


// instantiation
Car<BrakeChooser> car(BrakeChooser(new AntiLockBrakes()));

Biorąc to pod uwagę, prawdopodobnie nie użyłbym do tego szablonów... Ale to naprawdę osobisty gust.

 3
Author: Greg Rogers,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:25:42

Abstrakcyjna klasa bazowa ma narzut wirtualnych wywołań, ale ma tę zaletę, że wszystkie klasy pochodne są tak naprawdę klasami bazowymi. Nie tak, gdy używasz szablonów-Car i Car nie są ze sobą powiązane i będziesz musiał albo dynamic_cast i sprawdzić czy nie ma null, albo mieć szablony dla całego kodu, który dotyczy samochodu.

 2
Author: sharptooth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-03 14:41:11

Użyj interfejsu, jeśli chcesz jednocześnie obsługiwać różne klasy Break i ich hierarchię.

Car( new Brake() )
Car( new BrakeABC() )
Car( new CoolBrake() )

I nie znasz tej informacji w czasie kompilacji.

Jeśli wiesz, którą przerwę zamierzasz użyć 2b jest właściwym wyborem dla ciebie, aby określić różne klasy samochodów. Hamulec w tym przypadku będzie twój samochód "Strategia" i można ustawić domyślny.

Nie używałbym 2a. zamiast tego możesz dodać statyczne metody do łamania i wywoływania ich bez instancji.

 0
Author: Mykola Golubyev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-03-02 14:59:03

Osobiście zawsze wolałabym używać interfejsów zamiast szablonów z kilku powodów:

  1. błędy kompilacji i linkowania szablonów są czasami tajemnicze
  2. trudno debugować kod oparty na szablonach (przynajmniej w Visual studio IDE)
  3. Szablony mogą powiększyć Twoje pliki binarne.
  4. Szablony wymagają umieszczenia całego kodu w pliku nagłówkowym, co sprawia, że klasa szablonu jest nieco trudniejsza do zrozumienia.
  5. Szablony są trudne do utrzymania przez początkujący Programiści.

Używam szablonów tylko wtedy, gdy wirtualne tabele tworzą jakiś napowietrzny.

Oczywiście, to tylko moje zdanie.

 0
Author: user88637,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-10 10:42:38