Designfrage Rückgabeparameter



  • Hallo,

    ich habe eine Funktion, die eine Datenstruktur aus einer Datei ausliest. Dabei schließt der Anwendungsfall ein, dass die Datei durch einen anderen Prozess gelockt ist und nicht geöffnet werden kann, das ist ein gültiges Szenario.
    Wie sollte die Signatur für diese Funktion aussehen (C++11)?

    Option 1)
    unsigned int read_struct( const string& file_name, StructType& data )
    Naiver Ansatz, die Funktion füllt die Datenstruktur und gibt einen Status zurück. Fühlt sich mit den Möglichkeiten von C++ aber iwie komisch an. Außerdem muss man data vorher anlegen, finde ich auch nicht schön.

    Option 2)
    std::tuple<unsigned int, StructType> read_struct( const std::string& file_name )
    Element 0 hält den Fehlercode, Element 1 die Daten. Wenn die Datei nicht gelesen werden konnte muss StructType trotzdem konstruiert werden.

    Option 3)
    boost::variant<unsigned int, StructType> read_struct( const std::string& file_name )
    Die Auswertung des Rückgabewerts ist iwie sperrig. Ist im Moment trotzdem mein Favorit.

    Option 4)
    ?

    Gruß,
    Doc



  • boost::optional<StructType> read_struct( const std::string& file_name ) falls du nur "ging nicht" benötigst.



  • @manni66
    Ne, leider nicht. Wenn´s nicht geht muss der Benutzer erfahren, warum´s nicht ging.



  • Dann würde ich Option 2 mit boost::optional nehmen:
    std::tuple<unsigned int, boost::optional<StructType>> read_struct( const std::string& file_name )

    Statt tuple könnte man auch eine struct definieren.



  • Ich persönlich tendiere zu 1.
    Das Problem mit den Daten kann man auch einfach mit einem shared_ptr lösen...



  • @manni66
    Hast du ne Begründung? Oder ist das Bauchgefühl?



  • Mal doof gefragt: Gute, alte Exceptions sind keine Option?

    Die könnte man z.B. bequem nur dort behandeln, wo man den Benutzer informieren will, z.B. in einem "Datei laden"-Dialog - ohne den Status quer durch den ganzen Callstack reichen zu müssen.

    Auch eine Hybrid-Variante aus optional und Exceptions wäre möglich - siehe u.a. Andrei Alexandrescus expected<T>.

    Kann natürlich sein, dass das Lowlevel-Code werden soll, der auch in -fno-exceptions-Umgebungen zur Anwendung kommen soll. Da kann man natürlich mit optional und Status-Codes arbeiten.



    1. es gibt immer einen Errorcode den man testen kann (wie in Option 1)
    2. ich brauche keinen künstlichen "Leer"-Wert für StructType
    3. das Ergebnis kommt insgesamt als Return, es muss nicht vorher etwas besorgt werden

    Mit C++ 17 würde ich es so benutzen:

    #include <optional>
    #include <tuple>
    #include <iostream>
    
    std::tuple<int, std::optional<int>> doit() {
        return { 0, std::optional<int>{19} };
    }
    
    int main()
    {
        if( auto [ err, val ] = doit(); err ) {
    	std::cerr << "Fehler: " << err << "\n";
        }
        else {
    	std::cout << *val << "\n";
        }
    }
    




  • Weil noch ein Rückgabeparameter dazugekommen ist habe ich mich für eine Mischung aus 2) und 4) entschieden:

    struct ResultXYZ
    {
       StatusType::Type_t   Status;
       unsigned int         ErrorCode;
       boost::optional<XYZ> Data;
    
       explicit ResultXYZ( unsigned int error_code ); // Status kann aus ErrorCode abgeleitet werden
       explicit ResultXYZ( XYZ data );                // wird nach Data gemoved, Status und ErrorCode können abgeleitet werden
    };
    

    Danke für eure Vorschläge.



  • @Finnegan
    Ich könnte schon mischen, dann wird´s aber noch unübersichtlicher. Es gäbe dann drei Fälle:

    1. Datei kann gelesen werden -> alles ok
    2. Datei kann nicht gelesen werden, weil durch anderen Prozess gelockt -> Kein Fehler, keine Exception, Fehlercode?
    3. Sonstiger Fehler, der nicht vorgesehen ist -> Exception werfen

    Obwohl... jetzt wo ich so schreibe...
    Ich könnte das vllt über boost::optional erschlagen. Im Fall 1) StructType zurückgeben, im Fall 2) boost::none und ansonsten eine Exception werfen. Ich werd´ da noch mal in Ruhe drüber nachdenken.



  • @DocShoe sagte in Designfrage Rückgabeparameter:

    @Finnegan
    Ich könnte schon mischen, dann wird´s aber noch unübersichtlicher. Es gäbe dann drei Fälle:

    1. Datei kann gelesen werden -> alles ok
    2. Datei kann nicht gelesen werden, weil durch anderen Prozess gelockt -> Kein Fehler, keine Exception, Fehlercode?
    3. Sonstiger Fehler, der nicht vorgesehen ist -> Exception werfen

    Genau so mache ich das meistens. Also wenn es genau einen "erlaubten" Fall gibt wo nix zurückkommt, dann nenne ich die Funktion "tryXxx" oder "tryGetXxx" und im erlaubten Fehlerfall kommt halt nix zurück. Verträgt sich auch gut mit diversen anderen möglichen Problemen die sowieso schon per Exception kommuniziert würden, z.B. out of memory -> {{bad_alloc}}.

    Geht aber natürlich nur wenn es nur "den" erlaubten Fehlergrund gibt, bzw. wenn die genauen Details einfach nicht wichtig sind.



  • @DocShoe sagte in Designfrage Rückgabeparameter:

    @Finnegan
    Ich könnte schon mischen, dann wird´s aber noch unübersichtlicher. Es gäbe dann drei Fälle:

    1. Datei kann gelesen werden -> alles ok
    2. Datei kann nicht gelesen werden, weil durch anderen Prozess gelockt -> Kein Fehler, keine Exception, Fehlercode?
    3. Sonstiger Fehler, der nicht vorgesehen ist -> Exception werfen

    Obwohl... jetzt wo ich so schreibe...
    Ich könnte das vllt über boost::optional erschlagen. Im Fall 1) StructType zurückgeben, im Fall 2) boost::none und ansonsten eine Exception werfen. Ich werd´ da noch mal in Ruhe drüber nachdenken.

    Das klingt alles so, als könnte expected die Lösung sein, die du suchst. expected ist eine Art Kombination aus optional und variant. Es hält entweder den erwarteten Rückgabewert (in deinem Fall StructType) oder eine noch nicht geworfene (!) Exception, die Informationen darüber beinhaltet, weshalb kein erwarteter Rückgabewert vorliegt.

    expected bietet die Möglichkeit, den Fehler entweder lokal zu handhaben, wie bei einem klassischen Fehlercode (Dein Punkt 2):

    expected<StructType, ResultXYZ> read_struct(const string& file_name);
    ...
    auto result = read_struct(file_name);
    if (!result && result.error().ErrorCode == DATEI_IST_GELOCKT)
    {
        std::cerr << "Datei ist gelockt :(" << std::endl;
        ...
    }
    

    ... oder sich nicht um den Fehler zu scheren und einfach mit dem erwarteten Rückgabewert zu arbeiten (Punkt 3):

    auto result = read_struct(file_name);
    ...
    mach_was_mit_struct(result.value());
    

    In diesem Fall wirft dann expected die hinterlegte Exception vom Typ ResultXYZ, falls kein Wert vorliegt. ResultXYZ habe ich hier jetzt einfach mal genommen, weil es dem Fehlerfall/Exception-Typ, den du wahrscheinlich für excpected haben wollen würdest am nächsten kommt. Ohne Data-Member natürlich und evtl. noch von std::exceptionabgeleitet.

    Leider ist expected noch nicht Teil der Standardbibliothek. Du müsstest also auf die von theta gepostete Implementierung zurückgreifen, oder es selbst schreiben. Beim stöbern bin ich auch noch über boost.outcome gestolpert gestolpert. Das scheint etwas sehr ähnliches, wenn nicht sogar das gleiche zu sein.



  • Sind expected und/oder outcome überhaupt schon fertig ge-bike-shedded?
    Ich hab da zumindest vor kurzem noch elendslange Diskussionen in der Boost Mailingliste zu dem Thema gesehen. Das blöde bei der Sache ist halt, dass es einfach klingt, aber leider überhaupt nicht einfach ist. Zumindest nicht wenn es flexibel und akzeptabel einfach zu verwenden sein soll und performant implementierbar.

    Von daher würde ich eher bei

    optional<Result> tryDoTheThing(params);
    // und/oder
    optional<Result> tryDoTheThing(params, ErrorInfo& out_errorInfo);
    

    bleiben.

    Bzw. alternativ auch

    bool tryDoTheThing(params, Result& out_result);
    // und/oder
    ErrorCode tryDoTheThing(params, Result& out_result);
    

    Ich persönlich werde auch jeden Fall bei expected und outcome nicht early-adoptor spielen.



  • @hustbaer sagte in Designfrage Rückgabeparameter:

    Sind expected und/oder outcome überhaupt schon fertig ge-bike-shedded?

    Da ist was dran. Dass das noch gar nicht so ausgereift ist, war mir gar nicht bekannt, auf dem Papier sieht es jedenfalls toll aus. Danke für den Hinweis, das ist wohl erstmal nur für Projekte geeignet, bei denen man sich Experimente erlauben kann.

    Dennoch die m.E. "schönste" Lösung - wenn sie denn standardisiert wäre 😉



  • @Finnegan Naja die Frage war schon ernst gemeint, ich weiss nicht ob die schon "final" sind bzw. in einem "verlässlich" brauchbarem Zustand. Kann sein, kann sein nicht. Ich weiss nur dass auf der Boost Mailing Liste vor wenigen Monaten noch lebhaft über das Design diskutiert wurde. @Columbo weiss da vermutlich mehr 🙂



  • @hustbaer sagte in Designfrage Rückgabeparameter:

    @Finnegan Naja die Frage war schon ernst gemeint, ich weiss nicht ob die schon "final" sind bzw. in einem "verlässlich" brauchbarem Zustand. Kann sein, kann sein nicht. Ich weiss nur dass auf der Boost Mailing Liste vor wenigen Monaten noch lebhaft über das Design diskutiert wurde. @Columbo weiss da vermutlich mehr 🙂

    Nun, zumindest laut der Outcome-Website ist die Bibliothek wohl gerade offiziell in Boost aufgenommen worden:

    This library joined the Boost C++ libraries in the 1.70 release (Spring 2019).

    1.70 ist das kommende Release. Mehr Hintergründe habe ich nicht, aber es lässt vermuten, dass wohl ein Großteil der in den Mailinglisten angeprochenen Probleme behoben wurde. "Battle tested" kann man das aber wohl noch nicht nennen, deutlich ausgereifter als etwas "mal eben selbst gestricktes" dürfte es aber dennoch sein.

    Auch gibt es ein paar Unterschiede zum vorgeschlagenen std::expected.



  • Naja wenn das ganze in den Standard wandern soll ... ich glaub ich warte dann einfach bis es im Standard ist 🙂
    Ansonsten warte ich auch immer gerne bis Dinge 2-3 Jahre von den Early Adoptern verwendet wurden. Dann sollten die meisten Kinderkrankheiten behoben sein, bzw. wenn sich herausstellt dass es vom Grundkonzept her in der Praxis eigentlich gar nicht so gut funktioniert wie erhofft, dann lass ich es lieber ganz.



  • Wow, schicke Diskussion!
    Werde mich in outcome und expected mal einlesen, obwohl ich das wohl nie einsetzen können werde (Ich fürchte, wir bleiben beim Embarcadero RAD Studio 10.1 und dem clang 3.2. Der unterstützt nur C++11 und eine Portierung der boost Bibliotheken ist zu aufwändig. expected wird vom clang 3.2 auch nicht unterstützt, also sind beide raus).


Log in to reply