Assert wenn keine überladene Funktion einer virtuellen Funktion exisitiert



  • Hallo zusammen,

    ich habe eine Basisklasse, die eine virtuelle Funktion mit einem Standardverhalten implementiert. Ich habe aber ein paar Klassen, die von der Basisklasse ableiten die die Funktion überladen müssen.

    Da das an ein paar Stellen offenbar vergssen wurde, wollte ich jetzt im Konstruktor mit einem static_assert überprüfen ob es eben eine Überladung gibt.

    Meine Idee war sowas:

    static_assert(std::is_same_v<decltype(&Base::Function), decltype(&Derived::Function)>);
    

    Das funktioniert aber leider nicht. Hat jemand von euch eine Idee, wie man das schön lösen kann?



  • Hallo,

    ich würde es dann wahrscheinlich pure-virtual machen.
    Mit deinem static_assert verstehe ich nicht...muss dann jeder, der von dieser Klasse ableitet, daran denken dieses assert in den Konstruktor zu schreiben!?
    Das wäre dann ja dasselbe in grün.



  • @Jockelx Pure virtual war auch mein erster Ansatz. Führt nur dazu, dass die Funktion dann auch für alle Klassen implementiert werden muss, denen die Defaultimplementierung reicht. Daher habe ich das erstmal wieder verworfen.

    Meine Hauptidee war, dass ich das jetzt schnell überall in den Ctor hauen kann und dann direkt sehe ob die Klassen die Funktion implementieren. Hab mir eingebildet, dass das einfacher sei, als manuell in den entsprechenden Klassen nach den Funktionen zu suchen.



  • Du kannst deinen pure virtual funktionen trotzdem eine implementierung geben die erbende klassen als default verwenden können.

    struct A {
      virtual void foo() = 0;
    };
    
    void A::foo() {
      std::cout << "hello\n";
    }
    
    struct B : A {
      void foo() override { A::foo(); }
    };
    


  • @Schlangenmensch sagte in Assert wenn keine überladene Funktion einer virtuellen Funktion exisitiert:

    Führt nur dazu, dass die Funktion dann auch für alle Klassen implementiert werden muss, denen die Defaultimplementierung reicht

    Ja, das stimmt zwar, aber die "Implementierung" wäre dann ja auch einfach nur ein Aufruf der Basis-Funktion. Das scheint mir jetzt auch nicht aufwendiger als jeden Konstruktor anzupassen.
    Und du stellst sicher, dass sich zukünftig Leute Gedanken machen müssen, wenn Sie deine Basisklasse nutzen. Das würde bei dem static_assert-Ansatz ja auch nicht funktionieren.



  • @5cript @Jockelx Ihr habt mich überzeugt, danke!



  • Aber nochmal zu deiner Ursprungsidee:

    
    struct Base
    {
        virtual void f() {}
        virtual ~Base() {}
        
    };
    
    struct Concrete1 : Base
    {
        Concrete1()
        {
            //static_assert(!std::is_same_v<decltype(&Concrete1::f), decltype(&Base::f)>);
            if constexpr (!std::is_same_v<decltype(&Concrete1::f), decltype(&Base::f)>)
            {
                std::cout << "Yes";
            }
            else
            {
                std::cout << "No";
            }
        }
        virtual void f() override {}
    };
    
    struct Concrete2 : Base
    {
        Concrete2()
        {
            //static_assert(!std::is_same_v<decltype(&Concrete2::f), decltype(&Base::f)>);
            if constexpr (!std::is_same_v<decltype(&Concrete2::f), decltype(&Base::f)>)
            {
                std::cout << "Yes";
            }
            else
            {
                std::cout << "No";
            }
        }
    };
    
    int main(int , char const**)
    {
        Concrete1 c1;
        Concrete2 c2;
        return 0;
    }
    

    macht in einem kleinen Test genau das, was es soll: "YesNo" (bzw. das assert geht auch, wenn ich es einkommentiere).
    Was funktioniert denn da bei dir nicht?



  • @Jockelx Ich geh mir mal nen Loch buddeln... Dein Beispiel funktioniert wunderbar. Tatsächlich bekomme ich auch im realen Code den Fehler nicht mehr reproduziert.

    Es gibt damit tatsächlich ein anderes "Problem". Bei mir ist die Funktion in Base protected und damit geht es nicht: "'Base::f': cannot access protected member declared in class 'Base'".

    Man, mich ärgert gerade vor allem, dass ich ursprünglich wohl was anderes falsch gemacht habe und die einfache Regel des "minimalen Beispiels" missachtet habe.



  • @Schlangenmensch @Jockelx Das Problem, dass mann es in jeder abgeleiteten Klasse implementieren muss, könnte man ab C++23 wahrscheinlich mit einem expliziten this-Parameter vermeiden. Dieser hat den Typ der abgeleiten Klasse, auch in Member-Funktionen der Basisklasse. Leider ist die Compiler-Unterstützung dafür noch sehr lückenhaft, das wird soweit ich mich erinnere derzeit nur von MSVC unterstützt.

    So könnte das dann vielleicht ungefähr aussehen (ungetestet):

    template <typename Self>
    bool implements_f(this Self&&)
    {
        return std::is_same_v<decltype(&Self::f), decltype(&Base::f)>;
    }
    

    Eine Alternative wäre CRTP, also dass die abgeleitete Klasse der basisklasse ihren eigenen Typ via template-Parameter kommuniziert:

    struct Derived : Base<Derived>
    {
    }
    

    ... aber fraglich, ob sich das wirklich lohnt. Das ist nicht die elegante Lösung, die dir wahrscheinlich vorschwebt.



  • @Schlangenmensch @Jockelx Danke für die Upvotes, aber die waren wohl verfrüht. Ich habe gerade nochmal versucht, das tatsächlich zu implementieren und dabei festgestellt, dass implements_f() nur dann die abgeleiteten Klassen für this deduziert, wenn die Funktion auch aus der abgeleiteten Klasse aufgerufen wird. Im Base-Konstruktor wird auch für Derived1 und Derived2 lediglich Base als Typ deduziert. Das ist also doch keine so schöne Lösung. Auch wenn es den Vorteil hat, dass man nur stumpf eine Funktion zum prüfen aufrufen muss, ohne den Typen der aktuellen Klasse explizit angeben zu müssen. Das hier funktioniert jedenfalls ("Microsoft (R) C/C++ Optimizing Compiler Version 19.36.32532 for x64" mit "-std:c++latest"):

    #include <type_traits>
    #include <cassert>
    #include <iostream>
    
    struct Base
    {
        virtual void f() {}
        virtual ~Base() {}
    
        template <typename Self>
        void check(this Self&& self)
        {
            std::cout 
                << std::boolalpha
                << typeid(Self).name()
                << ":"
                << !std::is_same_v<
                    decltype(&std::remove_reference_t<Self>::f),
                    decltype(&Base::f)
                >
                << std::endl;
        }
    };
    
    struct Derived1 : Base
    {
        Derived1()
        {
            check();
        }
    
        virtual void f() override {}
    };
    
    struct Derived2 : Base
    {
        Derived2()
        {
            check();
        }    
    };
    
    int main()
    {
        Derived1 d1;
        Derived2 d2;
    }
    

    Ausgabe:

    struct Derived1:true 
    struct Derived2:false
    

    Ich frage mich, ob man da noch weitertricksen kann, mir fällt aber gerade nichts ein.



  • @Schlangenmensch
    Meinst du sowas in der Art:

    struct Base {
        virtual void f(int) = 0;
    };
    
    struct Derived1 : Base {
        Derived1();
        void f(int) override;
        void f(int, int);
    };
    
    struct Derived2 : Base {
        Derived2();
        void f(int) override;
    };
    
    Derived1::Derived1() {
        using dummy = decltype(f(1, 1)); // OK, overload f(int, int) vorhanden
    }
    
    Derived2::Derived2() {
        using dummy = decltype(f(1, 1)); // Fehler overload f(int, int) nicht vorhanden
    }
    


  • Bzw. so wenn es um potentiell fehlende Overloads geht deren Fehlen zum Aufruf der Funktion der Basisklasse führen würden:

    struct Base {
        virtual void f(int) = 0;
    };
    
    struct Derived1 : Base {
        Derived1();
        void f(int) override;
        void f(double);
    };
    
    struct Derived2 : Base {
        Derived2();
        void f(int) override;
    };
    
    Derived1::Derived1() {
        static_cast<void (Derived1::*)(double)>(&Derived1::f); // OK, overload f(double) vorhanden
    }
    
    Derived2::Derived2() {
        static_cast<void (Derived2::*)(double)>(&Derived2::f); // Fehler, overload f(double) nicht vorhanden
    }
    


  • @hustbaer Es wäre cool, wenn man es irgendwie schaffen könnte, die Überprüfung ausschliesslich in Base unterzubringen. Es ist glaube ich nicht viel gewonnen, wenn man eine Sache, die man eventuell vergessen kann, gegen eine andere austauscht, die man genau so gut vergessen könnte 🤔 ... aber da bin ich leider auch grad am Ende meiner Ideen.

    Vielleicht ist das ja auch eher ne Aufgabe für nen Linter, dem man irgendwie auch ein paar user-definierte Regeln beibringen kann. Da bin ich aber nicht so bewandert.



  • @Finnegan @Schlangenmensch
    Mir fällt gerade etwas auf: ich vermute @Schlangenmensch hat "überladen" (overload) geschrieben obwohl er "überschrieben" (override) meint.

    Und da sollte eigentlich der Code in der Frage funktionieren, bis auf dass halt ein ! fehlt:

    #include <type_traits>
    
    struct Base {
        virtual void fun(int){}
    };
    
    struct D1 : Base {
        D1();
        void fun(int) override {}
    };
    
    struct D2 : Base {
        D2();
    };
    
    D1::D1() {
        // OK, D1 überschreibt fun
        static_assert(!std::is_same_v<decltype(&Base::fun), decltype(&D1::fun)>);
    }
    
    D2::D2() {
        // Fehler, D2 überschreibt fun nicht
        static_assert(!std::is_same_v<decltype(&Base::fun), decltype(&D2::fun)>);
    }
    

    Bzw. wenn man es als Makro haben möchte:

    #include <type_traits>
    
    struct Base {
        virtual void fun(int){}
    };
    
    struct D1 : Base {
        D1();
        void fun(int) override {}
    };
    
    struct D2 : Base {
        D2();
    };
    
    #define MUST_HAVE_FUN_OVERRIDE() \
        { \
            using DX = std::remove_pointer_t<decltype(this)>; \
            static_assert(!std::is_same_v<decltype(&Base::fun), decltype(&DX::fun)>); \
        }
    
    D1::D1() {
        MUST_HAVE_FUN_OVERRIDE(); // OK
    }
    
    D2::D2() {
        MUST_HAVE_FUN_OVERRIDE(); // Fehler
    }
    


  • Also ich habe mal so eine Fragestellung etwa so gelöst (Stichwort Einschubmethoden):

    Basisklasse hat eine ggf. nicht virtuale Methode z.b. mit Namen void arbeiteNachStandard(…) und eine pur virtuale Methode virtual bool arbeiteAlternativ(…).
    Um das Alternativverhalten ausführen zu können, hat man zu Anfang in void arbeiteNachStandard(…) die Einschubmethoden void arbeiteNachStandard(…) aufgerufen. Wenn diese mit false verlassen wird, wird das Standardverhalten abgearbeitet, ansonsten übersprungen.
    Das bedeutet, dass alle direkten Unterklassen die Methode bool arbeiteAlternativ(…) implementieren müssen und dort entschieden werden muss, was passieren soll.
    Entweder mit oder ohne einem Ablauf retour mit true (Standardverhalten wird nicht abgearbeitet) oder false (Standardverhalten wird abgearbeitet).



  • @hustbaer ja, natürlich meine ich override. Und wie @Jockelx angemerkt hat, funktioniert die Idee. Ich weiß nicht was mich da geritten hat, als ich die Frage gestellt habe.

    Die anderen Lösungsvorschläge werde ich mir am Montag morgen anschauen, dann habe ich die nötige Ruhe dafür.



  • Nochmal vielen Dank für die Antworten.

    Ich habe bereits Freitag die Basisfunktion pure virtual gemacht, dann muss die Funktion zwar immer implementiert werden, dafür kann die nicht vergessen werden und die Chancen sich darüber einen Bug einzufangen verringern sich zumindest.

    @Finnegan Die Möglichkeit mit dem expliziten this ist interessant, da muss ich mal ein bisschen mit rumspielen. Ingesamt muss ich mich noch mit den C++23 Features beschäftigen. Über CRTP hatte ich auch schon nachgedacht, aber mehr um die Laufzeitpolymorphie aufzulösen. Aber das Rad war mir aktuell zu groß.

    @hustbaer Hm, das ist tatsächlich mal eine Stelle, an der ein Macro sinnvoll ist. Und das trifft genau das, was ich ursprünglich vor hatte.

    @Helmut-Jakoby Danke für die Anregung. Ich sehe da gerade aber noch nicht den Vorteil gegenüber einer pure virual Funktion mit Default Implementierung.



  • Guten Morgen @Schlangenmensch ,
    Du hast natürlich recht. Ich hatte vergessen, dass man eine pur virtuale Methode implementieren kann und dann wahlweise in der Unterklasse aufruft oder auch nicht.


Anmelden zum Antworten