Assert wenn keine überladene Funktion einer virtuellen Funktion exisitiert
-
@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.
-
-
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ürthis
deduziert, wenn die Funktion auch aus der abgeleiteten Klasse aufgerufen wird. ImBase
-Konstruktor wird auch fürDerived1
undDerived2
lediglichBase
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 Methodevirtual bool arbeiteAlternativ(…)
.
Um das Alternativverhalten ausführen zu können, hat man zu Anfang invoid arbeiteNachStandard(…)
die Einschubmethodenvoid arbeiteNachStandard(…)
aufgerufen. Wenn diese mit false verlassen wird, wird das Standardverhalten abgearbeitet, ansonsten übersprungen.
Das bedeutet, dass alle direkten Unterklassen die Methodebool 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.