type erasure und std::exception_ptr



  • Ist euch schonmal aufgefallen dass man std::exception_ptr "schön" als Mittel zur Type-Erasure einsetzen kann -- völlig unabhängig von Exceptions?

    boost::any und ähnliche Konstrukte haben mMn. eine deutliche Schwäche, und zwar kann man keine Basisklassen-Zeiger/-Referenzen von einem any bekommen. Also wenn man z.B. eine polymorphe Basisklasse printable hat, von der dann mehrere Klassen erben, dann kommt man mit any nicht sehr weit.
    Man kann Objekte der abgeleiteten Klassen zwar schön in das any hineintun, aber man kann nicht mehr als printable darauf zugreifen. Man bräuchte den exakten Typ. So lange man immer nur Zugriff auf eine Basisklasse braucht könnte man sich natürlich behelfen indem man einen shared_ptr<Basisklasse> in das any hineintut. Möchte man aber auf 2 oder mehr Basisklassen zugreifen können, oder u.U. sogar ohne es vorher zu wissen checken ob das im any gespeicherte Objekt eine bestimmte Basisklasse hat, dann ... naja, dann wüsste ich zumindest nicht mehr wie man das machen sollte.

    Mit C++11 haben wir nun std::exception_ptr bekommen, und damit geht genau das auf einmal:

    http://ideone.com/YCMwyS

    Nun ist das ja eine ziemlich greisliche Zweckentfremdung von std::exception_ptr . Die Möglichkeit relativ unkompliziert über Basisklassen auf Objekte in Type-Erasure-Containern ala any zuzugreifen könnte mMn. aber öfters mal nützlich sein.

    Und daher frage ich mich: wieso ist kein weniger "brutaler" Weg vorgesehen auf Objekte in einem std::exception_ptr zuzugreifen?

    Mir ist schon klar dass keiner von Euch im Standard-Komitte sitzt (bzw. ich gehe davon aus), daher erwarte ich mir auch keine Antwort auf diese Frage. Aber vielleicht findet ja jmd. von Euch das Thema interessant, und hat ein paar Worte dazu zu sagen.

    Bzw. auch: würdet ihr diese Funktionalität begrüssen? Für sinnvoll halten? Hattet ihr vielleicht sogar schon Fälle wo ihr euch diese Funktionalität gewünscht hättet?



  • Deine inspect -Funktion ist falsch. (Kein return -Statement)

    Vorschlag:

    #include <iostream>
    #include <memory>
    
    class any_ptr
    {
        struct WrapperBase
        {
        	virtual ~WrapperBase(){}
        };
    
        template<typename T>
        struct Wrapper : WrapperBase, T
        {
    		template<typename ... Args>
        	Wrapper( Args&&... args ):
        		T( std::forward<Args>(args)... ) {}
        };
    
        std::unique_ptr<WrapperBase> mPtr;
    
    	any_ptr( std::unique_ptr<WrapperBase> ptr ):
    		mPtr{ std::move(ptr) } {}
    
    public:
    
    	template< typename T >
    	explicit operator T*()
    	{
    		return dynamic_cast<T*>(mPtr.get());
    	}
    
    	template< typename T >
    	explicit operator T const*() const
    	{
    		return dynamic_cast<T*>(mPtr.get());
    	}
    
    	template<typename T>
    	friend any_ptr make_any_ptr( T&& t )
    	{
    		return std::unique_ptr<WrapperBase>{ new Wrapper<T>{ std::forward<T>(t) } };
    	}
    
    	template< typename T,
    	          typename ... Args >
    	friend any_ptr make_any_ptr( Args&&... args )
    	{
    		return std::unique_ptr<WrapperBase>{ new Wrapper<T>{ std::forward<Args>(args)... } };
    	}
    };
    
    ////////////////////////////////////////////////////////////////////////////////
    
    template <class T>
    void inspect(any_ptr& p, void (&fun)(T))
    {
    	using actual_t = typename std::remove_reference<T>::type;
    
    	if( actual_t* t = static_cast<actual_t*>(p) )
    		fun(*t);
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    
    class printable
    {
    public:
    	virtual void print() const = 0;
    };
    
    ////////////////////////////////////////////////////////////////////////////////
    
    #include <string>
    using namespace std;
    
    class foo : public printable
    {
    public:
    	foo(string const& s) : m_s(s) {}
    
    	virtual void print() const
    	{
    		cout << "foo: " << m_s << "\n";
    	}
    
    private:
    	string m_s;
    };
    
    class bar : public printable
    {
    public:
    	bar(int i) : m_i(i) {}
    
    	virtual void print() const
    	{
    		cout << "bar: " << m_i << "\n";
    	}
    
    private:
    	int m_i;
    };
    
    ////////////////////////////////////////////////////////////////////////////////
    
    void print_printable(printable& p)
    {
    	p.print();
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    
    int main()
    {
    	auto f = make_any_ptr(foo("foo"));
    	auto b = make_any_ptr(bar(42));
    
    	inspect(f, print_printable);
    	inspect(b, print_printable);
    }
    

    Funktioniert nicht für skalare Typen, oder finale Klassen. Sonst aber für eine Vererbungshierarchie doch anwendbar?

    Ideone: http://ideone.com/yMrCqb



  • Sone schrieb:

    Deine inspect -Funktion ist falsch. (Kein return -Statement)

    Naja "kein" ist übertrieben, das return false war schon da, das return true hat halt gefehlt.
    Hab's korrigiert.

    Sone schrieb:

    Funktioniert nicht für skalare Typen, oder finale Klassen. Sonst aber für eine Vererbungshierarchie doch anwendbar?

    Funktioniert auch nicht für Zeiger.
    Aber gute Idee, an die Möglichkeit hab' ich nicht gedacht!



  • Und meine Lösung kommentierst du nicht? 😞



  • Ich hab doch schon geschrieben "gute Idee, an die Möglichkeit hab' ich nicht gedacht" 😕

    Mit etwas mehr Worten: ja, damit kann man wohl fast alles machen wo man sonst den exception_ptr Hack verwenden wollen würde, und das sogar ganz ohne C++11 (abgesehen von perfect forwarding für die make Funktion und ähnliche Kleinigkeiten).
    Die Sache mit den non-class Types kann man ja noch relativ einfach lösen (Spezialisierung), bleiben eigentlich nur noch finale Klassen. Und die Sache dass man keine rohen Zeiger so damit verpacken kann, dass man sie als Zeiger auf ne Basisklasse wieder rausholen kann.
    (Also nur den Zeiger, ohne das Objekt.)
    Was wohl beides super selten nötig sein wird.

    Also wie gesagt: gute Lösung.

    Ich könnte jetzt noch suchen ob ich irgend einen Formfehler/Flüchtigkeitsfehler/Unschönheit etc. finde, aber darum geht's ja nicht. Das Konzept sieht für mich OK aus.
    Das einzige was mir auf die Schnelle auffällt ist dass du einen dynamic_cast hinter einem static_cast versteckst, was ich nicht so gut finde. Aber wie gesagt, das ist Formsache, und darum geht's ja nicht.

    BTW: man könnte das ganze sogar noch weiter reduzieren, indem man gleich shared_ptr<anything> verwendet - wobei anything deinem any_ptr::WrapperBase entspricht. Dann könnte man dynamic_pointer_cast verwenden um an einen permanenten Zeiger mit Ownership auf eine Basisklasse zu kommen.



  • hustbaer schrieb:

    Ich hab doch schon geschrieben "gute Idee, an die Möglichkeit hab' ich nicht gedacht" 😕

    Das hast du editiert. 🙂
    Guck mal auf die Zeitstempel. Nein, ich wollte tatsächlich nur wissen, ob da nicht noch Fallen versteckt sind die du erkennen könntest.

    Tja, man kann Wrapper noch partiell Spezialisieren, sodass es dann auch für Skalare klappt, wie du schon erwähnt hast. Finale Klassen wären für diese Idee tatsächlich so nicht realisierbar.

    Und die Sache dass man keine rohen Zeiger so damit verpacken kann, dass man sie als Zeiger auf ne Basisklasse wieder rausholen kann.

    Hmm, eigentlich ein Einwand... das müsste doch machbar sein...

    Das einzige was mir auf die Schnelle auffällt ist dass du einen dynamic_cast hinter einem static_cast versteckst, was ich nicht so gut finde. Aber wie gesagt, das ist Formsache, und darum geht's ja nicht.

    Der User muss sich im Klaren sein, dass für den Konvertierungsoperator ein paar Takte draufgehen.



  • Sone schrieb:

    hustbaer schrieb:

    Ich hab doch schon geschrieben "gute Idee, an die Möglichkeit hab' ich nicht gedacht" 😕

    Das hast du editiert. 🙂
    Guck mal auf die Zeitstempel. Nein, ich wollte tatsächlich nur wissen, ob da nicht noch Fallen versteckt sind die du erkennen könntest.

    Ja, hab editiert. Die Zeit dazwischen war aber so kurz dass ich nicht damit gerechnet habe dass es jmd. dazwischen gelesen hat.

    EDIT: Wenn das Forum nicht spinnt, hab ich das doch nicht editiert. Denn der letzte EDIT betraf nur die return Sache, den Absatz hab ich später nochmal editiert. Und es steht da ja "1x editiert". Oder zählt das Forum EDITs nicht wenn man sie super kurz nach dem posten macht? /EDIT

    Sone schrieb:

    Und die Sache dass man keine rohen Zeiger so damit verpacken kann, dass man sie als Zeiger auf ne Basisklasse wieder rausholen kann.

    Hmm, eigentlich ein Einwand... das müsste doch machbar sein...

    Ich wüsste nicht wie. Falls es doch geht, immer her damit.
    Wenn es ginge, dann wäre damit nämlich auch dir Wrapper-Klasse unnötig.

    Sone schrieb:

    Das einzige was mir auf die Schnelle auffällt ist dass du einen dynamic_cast hinter einem static_cast versteckst, was ich nicht so gut finde. Aber wie gesagt, das ist Formsache, und darum geht's ja nicht.

    Der User muss sich im Klaren sein, dass für den Konvertierungsoperator ein paar Takte draufgehen.

    Ja, pfuh. Ich würde es halt eher über ne Member-Funktion machen. Oder über ne freie Funktion ala dynamic_pointer_cast .



  • Wenn man beim Wrapper Komposition anstatt Vererbung verwendet, geht das auch für finale Klassen.
    Und anstatt einem Crosscast von WrapperBase zu T, macht man halt einen downcast zu Wrapper und greift dort auf T zu.



  • @Nathan
    Das geht nicht, weil mit Komposition Wrapper<foo> nicht mit Wrapper<printable> verwandt ist (und auch nicht mit foo oder printable ).

    Mit Vererbung ist Wrapper<foo> aber mit foo verwandt, und dadurch auch mit printable . Und dadurch kann man casten. Und da WrapperBase ne virtuelle Funktion hat, kann man auch dynamic_cast verwenden. Das ist ja der Trick der die ganze Sache überhaupt möglich macht.



  • Stimmt, der springende Punkt war ja, Vererbungsstrukturen beizubehalten.
    Mein Fehler.



  • Hier die Version mit Skalaren, ich hoffe ich habe nicht zu viel Boilerplate produziert: http://ideone.com/4E9YZS

    Mache mich jetzt an die Zeiger-Geschichte.



  • Die Erweiterung auf Skalare ist trivial, das hätte ich dir schon geglaubt dass du das hinbekommst. 😉
    Interessant - aber vermutlich unmöglich - ist die Geschichte mit den Zeigern.



  • hustbaer schrieb:

    Die Erweiterung auf Skalare ist trivial, das hätte ich dir schon geglaubt dass du das hinbekommst. 😉

    Ich dachte, vielleicht will das ja tatsächlich mal jemand benutzen 🤡

    vermutlich unmöglich

    Es ist nicht unmöglich. Denn die Idee ist logisch. Solange die Idee richtig ist, kann und muss es auch richtigen Code geben. Irgendwo da draußen ist ein sauberer, völlig standardkonformer und hübscher Code, der nur darauf wartet von mir in der IDE eingetippt zu werden.

    Das hier ist eine unportable und hässliche Variante. Sie ist Implementationsspezifisch und funktioniert wahrscheinlich nicht, laut Standard ist da ebenfalls nichts garantiert (wahrscheinlich UB, könnte man aber sicher auch ohne UB hinbekommen): http://ideone.com/SShuV3

    Schon auf Ideone funktioniert die Variante mit den Skalaren nicht mehr. Obwohl Ideone sogar denselben Compiler verwendet wie ich. Und bei mir klappt es. Daher: Finger weg...
    Das funktioniert nur deswegen, weil die Memberfunktion before() bei einigen Compilern die Vererbung berücksichtigt.



  • Der Code den du da aktuell hast kann nichtmal ansatzweise funktionieren.
    Denk an Mehrfachvererbung und virtuelle Vererbung.
    Und natürlich braucht man auch definiertes Verhalten für den Fall dass der Zeiger nicht konvertierbar ist.



  • Was ich dazu weiss: http://www.cplusplus.com/forum/articles/18756/

    Die Möglichkeit relativ unkompliziert über Basisklassen auf Objekte in Type-Erasure-Containern

    Das ist wiederspruechlich: An den Typ/Basistyp von Objekten zu gelangen, deren Typ geloescht ist. Warum sollte ich dann im Vorfeld type erasure anwenden? Auch ist mir unklar, welches Problem geloest werden soll. Nein, "öfters mal nützlich sein" hatte ich noch nie, wobei das kein wirkliches Kriterium ist

    Type-Erasure-Containern

    Was das sind, weiss ich nicht. Container mit type erased objecten? Dann spielen nur type erased objecte 'ne Rolle und der Container ist unwichtig.

    Und warum bei Sone neuerdings alles friend sein muss , ist mir auch unklar.



  • knivil schrieb:

    Und warum bei Sone neuerdings alles friend sein muss , ist mir auch unklar.

    Wenn du den Code nicht verstehst, dann verschwende ich nicht meine Zeit damit in dir zu erklären. Soviel sei gesagt: Die Factory braucht Zugriff auf den privaten Konstruktor. Diesen public zu machen, um deinen Ansprüchen genüge zu tun, würde dem User erlauben any_ptr irgendwie zu konstruieren... völliger Blödsinn.



  • Wenn du den Code nicht verstehst, dann verschwende ich nicht meine Zeit damit in dir zu erklären.

    Und du laber mich nicht dumm von der Seite an, auch die friends in deinem Iteratorbeispiel konnten problemlos entfernt weden. Hoehenfluege?



  • knivil schrieb:

    Wenn du den Code nicht verstehst, dann verschwende ich nicht meine Zeit damit in dir zu erklären.

    Und du laber mich nicht dumm von der Seite an, auch die friends in deinem Iteratorbeispiel konnten problemlos entfernt weden. Hoehenfluege?

    Gut, gut, gut. Tut mir Leid. Erkläre mir bitte, wie man das friend entfernen kann, ohne die Syntax beim Aufruf zu verändern oder die Konstruktoren public zu machen oder sonst irgendwas. 🙂

    Ich sehe keinen Weg. Und friend ist nur ein Schlüsselwort. Solange der User eine Funktion make_any_ptr aufrufen kann, und any_ptr private Konstruktoren hat, ist doch alles im Lot.



  • Kurze Erklaerung: Es gibt gewisse Schluesselreize, die Gefahr bedeuten, beispielsweise ist "new". Gleiches gilt fuer "friend". Selbst brauche ich es sehr wenig. Bis jetzt 2 Mal insgesamt. Waehrend fuer "new" direkte Loesungsmoeglichkeiten bestehen, ist "friend" eher ein konzeptionelles Problem. Und bevor ich das angehe, wuerde ich gern wissen, warum man gerne den Typ eines type erased objects haben moechte, bzw. warum man dann in erster Linie ueberhaupt type erasure nutzt. Bei boost::variant erhaelt man das mit 'nem Visitor, ist wohl aber nicht das gesuchte.



  • @knivil
    Ein Type Erasure Container ist sowas wie boost::any .

    Und die Forderung an den Typ zu kommen ist überhaupt nicht widersprüchlich - das macht man bei Type-Erasure im Prinzip immer. Nur dass es meist hinter spezialisierten privaten Hilfs-Klassentemplates versteckt wird.

    Beispiel für wo so etwas nützlich sein könnte wäre eine Registry für Objekte beliebigen Typs. Denk an Dependency Injection Frameworks o.ä. Bzw. auch einfach als Erweiterung von boost::any o.ä.

    Was ich nicht verstehe, ist warum bei Fragen wie diesen oft so patzige Antworten kommen. OK, du hast keine Verwendung dafür. Fein. Noted. Kann man aber auch freundlicher sagen.

    Das selbe gilt für friend . friend ist ein recht nützlicher Bestandteil von C++, der dabei hilft die Kapselung zu verstärken. Natürlich kann man auch so arbeiten als ob es friend nicht gäbe. Dann wird man friend auch nicht brauchen. Oh Wunder. Was aber nicht heisst dass man dabei unbedingt den besten Code produziert.


Anmelden zum Antworten