Assert wenn keine überladene Funktion einer virtuellen Funktion exisitiert



  • @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