So definieren Sie verschiedene Typen für dieselbe Klasse in C ++

84

Ich hätte gerne mehrere Typen, die dieselbe Implementierung verwenden, aber in C ++ immer noch vom unterschiedlichen Typ sind.

Um meine Frage anhand eines einfachen Beispiels zu veranschaulichen, hätte ich gerne eine Klasse für Äpfel, Orangen und Bananen, die alle die gleichen Operationen und die gleiche Implementierung haben. Ich möchte, dass sie unterschiedliche Typen haben, weil ich Fehler aufgrund der Typensicherheit vermeiden möchte.

class Apple {
     int p;
public:
     Apple (int p) : p(p) {}
     int price () const {return p;}
}

class Banana {
     int p;
public:
     Banana (int p) : p(p) {}
     int price () const {return p;}
}

class Orange ...

Um Code nicht zu duplizieren, könnte ich anscheinend eine Basisklasse Fruit verwenden und davon erben:

class Fruit {
     int p;
public:
     Fruit (int p) : p(p) {}
     int price () const {return p;}
}

class Apple: public Fruit {};
class Banana: public Fruit {};
class Orange: public Fruit {};

Aber dann werden die Konstruktoren nicht geerbt und ich muss sie neu schreiben.

Gibt es einen Mechanismus (Typedefs, Vorlagen, Vererbung ...), mit dem ich problemlos dieselbe Klasse mit unterschiedlichen Typen haben kann?

anumi
quelle
Können Sie genauer erklären, warum Sie das brauchen würden? Ich kann mir keine gute Idee einfallen lassen. Wenn die Klassen die Implementierung gemeinsam nutzen, bedeutet dies nicht, dass sie auch die Funktionalität gemeinsam nutzen?
Jnovacho
4
Ja, aber da sie unterschiedliche Typen haben, können beim Kompilieren einige Programmierfehler erkannt werden (z. B. Zusammenführen von Äpfeln und Orangen).
Anumi

Antworten:

119

Eine übliche Technik besteht darin, eine Klassenvorlage zu haben, bei der das Vorlagenargument einfach als eindeutiges Token („Tag“) dient, um es zu einem eindeutigen Typ zu machen:

template <typename Tag>
class Fruit {
    int p;
public:
    Fruit(int p) : p(p) { }
    int price() const { return p; }
};

using Apple = Fruit<struct AppleTag>;
using Banana = Fruit<struct BananaTag>;

Beachten Sie, dass die Tag-Klassen nicht einmal definiert werden müssen. Es reicht aus , einen eindeutigen Typnamen zu deklarieren . Dies funktioniert , weil der Tag ISN tatsächlich verwendet überall in der Vorlage. Und Sie können den Typnamen in der Liste der Vorlagenargumente deklarieren (Tipp an @Xeo).

Die usingSyntax lautet C ++ 11. Wenn Sie mit C ++ 03 nicht weiterkommen, schreiben Sie stattdessen Folgendes:

typedef Fruit<struct AppleTag> Apple;

Wenn die allgemeine Funktionalität viel Code beansprucht, führt dies leider ziemlich viel doppelten Code in die endgültige ausführbare Datei ein. Dies kann verhindert werden, indem eine gemeinsame Basisklasse die Funktionalität implementiert und dann eine Spezialisierung (die Sie tatsächlich instanziieren) hat, die sich daraus ableitet.

Leider müssen Sie dazu alle nicht vererbbaren Mitglieder (Konstruktoren, Zuweisungen…) erneut implementieren, was selbst einen geringen Overhead verursacht. Dies ist also nur für große Klassen sinnvoll. Hier wird es auf das obige Beispiel angewendet:

// Actual `Fruit` class remains unchanged, except for template declaration
template <typename Tag, typename = Tag>
class Fruit { /* unchanged */ };

template <typename T>
class Fruit<T, T> : public Fruit<T, void> {
public:
    // Should work but doesn’t on my compiler:
    //using Fruit<T, void>::Fruit;
    Fruit(int p) : Fruit<T, void>(p) { }
};

using Apple = Fruit<struct AppleTag>;
using Banana = Fruit<struct BananaTag>;
Konrad Rudolph
quelle
+1, ich würde damit weitermachen, wenn Sie keine zusätzlichen Eigenschaften für die einzelnen Früchte definieren möchten ...
Nim
20
Sie können sie tatsächlich einfach in der Vorlagenargumentliste deklarieren, was ich ziemlich praktisch finde : Fruit<struct SomeTag>.
Xeo
1
@KonradRudolph es ist eine Schande , ich kann nicht +1 das Bearbeiten selbst ..... ich diese bearbeiten sah Kommentar .
ewig matt
1
@eternalmatt LOL - Ich hätte nie gedacht, dass jemand das sehen würde. Aber gut, du musst lustig sein, auch wenn niemand hinschaut. ;-)
Konrad Rudolph
2
Ein Nachteil davon ist die mehrfache Emission der Vorlageninstanziierung für die verschiedenen Typen. Werden diese Duplikate von weit verbreiteten Linkern entfernt?
Boycy
19

Verwenden Sie Vorlagen und ein Merkmal pro Frucht, zum Beispiel:

struct AppleTraits
{
  // define apple specific traits (say, static methods, types etc)
  static int colour = 0; 
};

struct OrangeTraits
{
  // define orange specific traits (say, static methods, types etc)
  static int colour = 1; 
};

// etc

Dann haben Sie eine einzelne FruitKlasse, die auf diesem Merkmal eingegeben wird, z.

template <typename FruitTrait>
struct Fruit
{
  // All fruit methods...
  // Here return the colour from the traits class..
  int colour() const
  { return FruitTrait::colour; }
};

// Now use a few typedefs
typedef Fruit<AppleTraits> Apple;
typedef Fruit<OrangeTraits> Orange;

Kann etwas übertrieben sein! ;)

Nim
quelle