Vector auf unique_ptr und Vererbung



  • Hallo zusammen,

    ich frage mich, ob es möglich ist, einer Funktion, die eine Referenz auf einen vector<unique_ptr<A>> erwartet, auch eine Referenz auf einen vector<unique_ptr<B>> zu übergeben, wenn B von A erbt.

    Also, das folgende Beispiel compiliert nicht. Die Fehlermeldung ist soweit auch verständlich. Es wird ein Vector auf Smartpointer von A erwartet, nicht von B. Meine Hoffnung war, dass das geht, weil B ja ein spezielleres A ist, also von A abgeleitet ist. Gibts dafür vielleicht eine einfach Lösung, und ich sehe nur den Wald vor lauter Bäumen nicht?

    #include <vector>
    #include <memory>
    #include <iostream>
    
    using namespace std;
    
    class A
    {
    public:
        virtual ~A() = default;
        virtual void printMsg() { cout << "I'm A" << endl; }
    };
    
    class B : public A
    {
    public:
        void printMsg() override { cout << "I'm B" << endl; }
    };
    
    void doSomething(vector<unique_ptr<A>> &objs)
    {
        for (auto &elem : objs)
        {
            elem->printMsg();
        }
    }
    
    int main()
    {
        vector<unique_ptr<A>> manyAs;
        manyAs.push_back(make_unique<A>());
        doSomething(manyAs);
    
        vector<unique_ptr<B>> manyBs;
        manyBs.push_back(make_unique<B>());
        doSomething(manyBs); // <-- compiliert nicht
    }
    

    Die Fehlermeldung:

    example.cpp: In Funktion »int main()«:
    example.cpp:44:17: Fehler: ungültige Initialisierung einer Referenz des Typs »std::vector<std::unique_ptr<A> >&« von Ausdruck des Typs »std::vector<std::unique_ptr<B> >«
       44 |     doSomething(manyBs);
          |                 ^~~~~~
    example.cpp:20:41: Anmerkung: bei Übergabe des Arguments 1 von »void doSomething(std::vector<std::unique_ptr<A> >&)«
       20 | void doSomething(vector<unique_ptr<A>> &objs)
          |                  ~~~~~~~~~~~~~~~~~~~~~~~^~~~
    


  • @beedaddy Nein, durch das Einpacken in unique_ptr sind das zwei verschiedene Typen. Du kannst aber ein B in einen unique_ptr<A> reinpacken.

    #include <iostream>
    #include <memory>
    #include <vector>
    
    struct A{
        virtual ~A() = default;
        virtual void print(){
            std::cout << __PRETTY_FUNCTION__ << "\n";
        }
    };
    
    struct B : public A{
        virtual void print(){
            std::cout << __PRETTY_FUNCTION__ << "\n";
        }
    };
    
    using ptr_t = std::unique_ptr<A>;
    
    void doSomething(const std::vector<ptr_t> &objs)
    {
        for (const auto &elem : objs)
        {
            elem->print();
        }
    }
    
    int main()
    {
    
        std::vector<ptr_t> v;
        v.emplace_back(ptr_t(new A));
        v.emplace_back(ptr_t(new B));
        doSomething(v);
    }
    

  • Mod

    Eine bessere Lösung liegt in Verallgemeinerung deiner Funktion doSomething: Lass die keinen konkreten Typ als Parameter haben, sondern templatisiere auch diese Funktion. Entweder indem du dem Container objs einen Templatetyp gibst, oder indem du gar keinen Container objs nimmst, sondern zwei Iteratoren (die auch wieder einen Templatetyp haben) auf Beginn und Ende. Also quasi so wie das sämtliche Funktionen in der STL (aus gutem Grund) machen. Wenn du fancy sein willst, machst du es auch wie die STL es seit neuerem macht, und bietest beides an.


  • Mod

    @Tyrdal sagte in Vector auf unique_ptr und Vererbung:

    @beedaddy Nein, durch das Einpacken in unique_ptr sind das zwei verschiedene Typen.

    Das sind übrigens auch alleine durch den Vector schon zwei verschiedene Typen. vector<A*> und vector<B*> sind auch nicht kompatibel. Vectoren - und allgemein alle Templates - sind nicht kovariant, sondern invariant. Das ist einfach durch die Sprache C++ so definiert. Das ist beispielsweise ein ganz großer Unterschied zu Java.

    Wollte ich nur klar stellen, weil Frage und Antwort so klangen, als sei das ein Problem von unique_ptr im speziellen.

    Oft merkt man das mit der Invarianz bloß nicht, weil beispielsweise der unique_ptr auch einen Template-Copy-Konstruktor hat (also ähnlich wie mein Tipp oben), der dann versucht die internen Pointer zuzuweisen, was bei kompatiblen Klassen dann auch klappt (die Pointer selbst sind nämlich kovariant). Daher klappt es auch, einem unique_ptr<A> einen unique_ptr<B> zuzuweisen. Aber das ist halt reingecoded von den Machern von unique_ptr, weil sie wussten, dass sie Kovarianz haben möchten.



  • @SeppJ Ja stimmt, es liegt letztlich an den Templatemechanismen.



  • Vielen Dank für die Erläuterungen, @Tyrdal und @SeppJ. Durch templatisieren der Funktion doSomething konnte ich das Problem (also: meine Fehlannahme) umgehen. Über Concepts kann ich ja den konkreten Typ ggf. einschränken…


  • Mod

    @beedaddy sagte in Vector auf unique_ptr und Vererbung:

    Über Concepts kann ich ja den konkreten Typ ggf. einschränken…

    Kann man, aber ich würde immer genau überlegen, warum man das machen will und das nur mit gutem Grund machen ("Dann weiß ich, es sind nur A und B erlaubt!" ist selten ein guter Grund). Ich weiß, das hier ist nur ein Beispiel, aber wenn die Funktion wirklich nur print auf allen Elementen eines Container aufrufen soll, gibt es ja eigentlich kaum Gründe, wieso das speziell nur für A und B gehen sollte, außer man braucht wirklich ein ganz spezielles Verhalten von print, das nur A und B garantieren können.



  • @SeppJ sagte in Vector auf unique_ptr und Vererbung:

    Kann man, aber ich würde immer genau überlegen, warum man das machen will und das nur mit gutem Grund machen ("Dann weiß ich, es sind nur A und B erlaubt!" ist selten ein guter Grund).

    Ja, ich bin da auch noch etwas am experimentieren.

    Tatsächlich handelt es sich um Spezialisierungen einer Klasse Entity, die also in eine Datenbank gespeichert werden sollen und eben – derzeit 😉 – in Form eines vectors auf Smartpointer vorliegen. Es gibt dann auch noch andere vectoren mit anderen von Entity abgeleiteten Typen. Diese sollen alle diese Funktion verwenden können. Daher dachte ich erst, dass das über die Basisklasse machbar ist. Aber nun weiß ich ja, dass das wegen Invarianz nicht geht. ☺

    Im Moment schränke ich also über Concepts ein, dass der Typ von Entity abgeleitet sein muss. Einfach, um sicherzustellen, dass bestimmte Methoden vorhanden sind.


  • Mod

    @beedaddy sagte in Vector auf unique_ptr und Vererbung:

    Tatsächlich handelt es sich um Spezialisierungen einer Klasse Entity, die also in eine Datenbank gespeichert werden sollen und eben – derzeit 😉 – in Form eines vectors auf Smartpointer vorliegen. Es gibt dann auch noch andere vectoren mit anderen von Entity abgeleiteten Typen. Diese sollen alle diese Funktion verwenden können. Daher dachte ich erst, dass das über die Basisklasse machbar ist. Aber nun weiß ich ja, dass das wegen Invarianz nicht geht. ☺

    Stimmt, die Pointer brauchst du dann gar nicht mehr.

    Im Moment schränke ich also über Concepts ein, dass der Typ von Entity abgeleitet sein muss. Einfach, um sicherzustellen, dass bestimmte Methoden vorhanden sind.

    Das klingt für diesen Anwendungsfall auch korrekt, eben weil du für deine Datenbank sicherlich ganz genaue Anforderungen haben willst, die nicht jede dahergelaufene Klasse erfüllen wird, bloß weil sie eine gleichnamige Methode implementiert.


Anmelden zum Antworten