Undefiniertes Verhalten bei "Closures" (structs mit operator()), wenn sie stateful sind???



  • Servus,

    ich beiß' hier gerade in die Tischkante, weil ich ein Problem mit Funktoren habe, was offenbar auf undefiniertes Verhalten zurückzuführen ist.

    Ich versuche mal ein Minimalbeispiel:
    Es gibt eine Klasse, die zwei Pointer auf Structs speichert, die jeweils den Klammeroperator überladen. Zwei Pointer, weil es eine Vorwärts- und eine Rückwärtskonvertierung gibt:

    class Foo
    {
    public:
        struct Bar
        {
            virtual ~ConverterFunctor() {}
            virtual int operator()(int) const = 0;
        };
        Foo(Bar* f1, Bar* f2):
            mf1(f1),
            mf2(f2)
        {}
        int calc1(int val) const { return (*mf1)(val); }
        int calc2(int val) const { return (*mf2)(val); }
    private:
        Bar* mf1;
        Bar* mf2;
    };
    

    Dann gibts verschiedene konkrete Klassen, die alle andere Funktoren definieren, bspws:

    class ConcreteFoo : public Foo 
    {
        struct Forward : public Foo::Bar {
            int operator()(int val) const { /* tatsächliche Berechnung */ }
        };
    
        struct Backward : public Foo::Bar {
            int operator()(int) const { /* umkehrung der Berechnung */ }
        };
    
    public:
        ConcreteFoo(): Foo(new Forward, new Backward) {}
    };
    

    Aufgerufen wird das ganze von einer Klasse, die einen Pointer auf Foo speichert:

    class User {
        User(Foo* foo): m_foo(foo) {}
        int doSomething() { return m_foo->calc1(m_some_value); }
    private:
        std::auto_ptr<Foo> m_foo;
        int m_some_value;
    };
    

    Gratulation, wer bis hierhin durchgehalten hat.
    Jetzt wirds nämlich spannend: Das funktioniert alles so lange, wie die Funktoren "stateless" sind. Sobald ich aber einen Functor definiere, der eine Membervariable hat, die zur Berechnung hinzugezogen wird (natürlich nur lesend, die operator()-Funktionen sind ja const!), kommt nur noch Blödsinn raus.
    In einer printf-Orgie hab ich jetzt festgestellt, das sämtliche Instantiierungen eines Funktors mit Membervariable ALLE den GLEICHEN wert in dieser Membervariable haben, egal wie ich sie instantiiert habe.

    Beispiel:

    class SomeConcreteFoo : public Foo 
    {
        struct Stateful : public Foo::Bar {
            Stateful(int i):member(i) {Log(member); /* -> hier kommt der richtige Wert! */}
            int operator()(int val) const { Log(member); /* -> hier kommt immer 1! WTF? */ }
        private:
            int member;
        };
    
    public:
        SomeConcreteFoo(): Foo(new Stateful(42), new Stateful(23)) {}
    };
    

    Wo habe ich hier Mist gebaut? Wird da irgendwo ein Objekt gesliced? Passiert da irgendwo eine Kopie, die ich übersehen habe?

    Philipp


  • Mod

    Ich kann's leider nicht nachvollziehen. Hast du deinen Fehler bei deinem Beispiel eventuell rausgekürzt? Wenn ich dein Beispiel nämlich zu etwas compilierbarem ergänze:

    #include<iostream>
    using namespace std;
    
    class Foo
    {
    public:
        struct Bar
        {
            virtual ~Bar() {}
            virtual int operator()(int) const = 0;
        };
        Foo(Bar* f1, Bar* f2):
            mf1(f1),
            mf2(f2)
        {}
        int calc1(int val) const { return (*mf1)(val); }
        int calc2(int val) const { return (*mf2)(val); }
    private:
        Bar* mf1;
        Bar* mf2;
    };	
    
    class SomeConcreteFoo : public Foo
    {
        struct Stateful : public Foo::Bar {
          Stateful(int i):member(i) {cout<<"Konstruktor: "<<member<<endl; /* -> hier kommt der richtige Wert! */}
          int operator()(int val) const { cout<<"Operator(): "<< member<<endl; return member;/* -> hier kommt immer 1! WTF? */ }
        private:
            int member;
        };
    public:
        SomeConcreteFoo(): Foo(new Stateful(42), new Stateful(23)) {}
    };	
    
    class User {
    public:
        User(Foo* foo): m_foo(foo) {}
        int doSomething() { return m_foo->calc1(m_some_value); }
    private:
        Foo* m_foo;
        int m_some_value;
    }; 
    
    int main()
    {
      User test(new SomeConcreteFoo());
      int a = test.doSomething();
      cout<<"Test: "<< a<< endl;
    }
    

    Bekomme ich:

    Konstruktor: 23
    Konstruktor: 42
    Operator(): 42
    Test: 42
    

    Ist das nicht genau das was du möchtest?

    edit: Compilier doch mal dein Programm mit extra vielen Compilerwarnungen ("-Wall -Wextra" bei vielen Compilern). Vielleicht findest du ja was.



  • Ja, sieht so aus, dass ich den Fehler rausekürzt habe.
    Was ich rausgekürzt habe, sind die Templates. Da ich aber im Augenblick eh nur die int-instantiierung verwende, sollte das irrelevant sein.

    PS:
    Mach ich schon: g++ -Werror -Wall -Wextra -pedantic mehr geht eigentlich nicht mehr, außer -Weffc++.


  • Mod

    PhilippM schrieb:

    Ja, sieht so aus, dass ich den Fehler rausekürzt habe.
    Was ich rausgekürzt habe, sind die Templates. Da ich aber im Augenblick eh nur die int-instantiierung verwende, sollte das irrelevant sein.

    PS:
    Mach ich schon: g++ -Werror -Wall -Wextra -pedantic mehr geht eigentlich nicht mehr, außer -Weffc++.

    Hmm, dann hilft's wohl nichts, außer ein neues Minimalbeispiel zu erstellen, welches den Fehler noch zeigt. Alles andere wäre Wahrsagerei. Vielleicht findet sich der Fehler dabei ja auch schon von alleine.



  • Hmpf, wenn ich mir den Wert des this-Zeigers ausgeben lasse, sieht man, dass immer nur das zuerst instantierte Objekt benutzt wird. Alle späteren Objekten werden zwar instantiiert, aber ihr this-Zeiger taucht nie wieder auf.
    "I sense an all-night debugging sesion coming". 😞



  • Fehler gefunden.

    Vollkommen unrelated mit dem oben geposteten Code, sorry. Manchmal ist der Fehler eben nicht da, wo er sich zeigt.
    Würde jetzt zu weit führen, zu erklären, was passiert ist, nur so viel:

    Gefunden habe ich den Fehler gerade durch Cookie-Debugging: Rechner zugeklappt, in die Teeküche gegangen, Tee gekocht, Keks gegessen, nachgedacht, "Aha!"-Erlebnis gehabt. Rechner aufgeklappt, probiert, funktioniert.
    🙂

    Phil


Anmelden zum Antworten