Keyword-Argumente im Python-Stil in C ++ - gute Praxis oder schlechte Idee?

8

Während ich kürzlich versuchte, die optimale Reihenfolge für optionale Parameter für eine Funktion herauszufinden, bin ich auf diesen Blog-Beitrag und das dazugehörige GitHub-Repo gestoßen , das einen Header für eine Pythonic- kwargsähnliche Funktion in C ++ enthält. Obwohl ich es nicht benutzt habe, frage ich mich, ob dies in einer stark typisierten Sprache gut ist oder nicht. Nachdem ich eine Weile in Python gearbeitet habe, finde ich die Vorstellung einer kwargsähnlichen Einrichtung in meinem Projekt sehr ansprechend, da viele seiner Objekte / Funktionen eine Reihe optionaler Parameter haben (die leider nicht vermieden werden können), was zu langen Listen von Konstruktoren führt, die unterscheiden sich durch ein oder zwei Parameter und könnten viel prägnanter / trockener gemacht werden.

Was ist, wenn überhaupt, die Erfahrung anderer mit solchen Dingen? Sollte es vermieden werden? Gibt es Richtlinien dafür? Was sind die möglichen Probleme / Fallstricke?

Sebastian Lenartowicz
quelle
Möglicherweise finden Sie interessante N4172 ( werfen Sie einen Blick auf die Einwände) und Bring benannte Parameter in modernem C ++ .
Manlio

Antworten:

13

Ich bin mit C ++ - Warwar nicht sehr vertraut, aber nach dem Überfliegen der Quelle fallen mir einige Nachteile ein:

  1. Es ist eine Bibliothek von Drittanbietern . Ein bisschen offensichtlich, aber dennoch müssen Sie einen Weg finden, um es in Ihr Projekt zu integrieren und die Quelle zu aktualisieren, wenn das ursprüngliche Repo geändert wird.
  2. Sie erfordern die globale Vordeklaration aller Argumente . Das einfache Beispiel im Blog-Beitrag enthält diesen Abschnitt mit dem Eigengewicht:

    #include "kwargs.h"
    
    // these are tags which will uniquely identify the arguments in a parameter
    // pack
    enum Keys {
      c_tag,
      d_tag
    };
    
    // global symbols used as keys in list of kwargs
    kw::Key<c_tag> c_key;
    kw::Key<d_tag> d_key;
    
    // a function taking kwargs parameter pack
    template <typename... Args>
    void foo(int a, int b, Args... kwargs) {
      // first, we construct the parameter pack from the parameter pack
      kw::ParamPack<Args...> params(kwargs...);
    
      ...

    Nicht so prägnant wie das pythonische Original.

  3. Mögliches binäres Aufblähen . Ihre Funktion muss eine variable Vorlage sein, damit bei jeder Permutation von Parametern der Binärcode neu generiert wird. Der Compiler kann häufig nicht erkennen, dass sie sich in Kleinigkeiten unterscheiden, und die Binärdateien zusammenführen.
  4. Langsamere Kompilierungszeiten . Auch hier muss Ihre Funktion eine Vorlage sein und die Bibliothek selbst ist vorlagenbasiert. An Vorlagen ist nichts auszusetzen, aber Compiler benötigen Zeit, um sie zu analysieren und zu instanziieren.

C ++ bietet native Alternativen, um die Funktionalität benannter Parameter zu erreichen:

  1. Strukturverpackungen . Definieren Sie Ihre optionalen Parameter als Felder einer Struktur.

    struct foo_args {
        const char* title = "";
        int year = 1900;
        float percent = 0.0;
    };
    
    void foo(int a, int b, const foo_args& args = foo_args())
    {
        printf("title: %s\nyear: %d\npercent: %.2f\n",
            args.title, args.year, args.percent);
    }
    
    int main()
    {
        foo_args args;
        args.title = "foo title";
        args.percent = 99.99;
        foo(1, 2, args);
    
        /* Note: in pure C brace initalizers could be used instead
           but then you loose custom defaults -- non-initialized
           fields are always zero.
    
           foo_args args = { .title = "foo title", .percent = 99.99 };
        */
        return 0;
    }
  2. Proxy-Objekte . Argumente werden in einer temporären Struktur gespeichert, die mit verketteten Setzern geändert werden kann.

    struct foo {
        // Mandatory arguments
        foo(int a, int b) : _a(a), _b(b) {}
    
        // Optional arguments
        // ('this' is returned for chaining)
        foo& title(const char* title) { _title = title; return *this; }
        foo& year(int year) { _year = year; return *this; }
        foo& percent(float percent) { _percent = percent; return *this; }
    
        // Do the actual call in the destructor.
        // (can be replaced with an explicit call() member function
        // if you're uneasy about doing the work in a destructor) 
        ~foo()
        {
            printf("title: %s\nyear: %d\npercent: %.2f\n", _title, _year, _percent);
        }
    
    private:
        int _a, _b;
        const char* _title = "";
        int _year = 1900;
        float _percent = 0.0;
    };
    
    
    int main()
    {
        // Under the hood:
        //  1. creates a proxy object
        //  2. modifies it with chained setters
        //  3. calls its destructor at the end of the statement
        foo(1, 2).title("foo title").percent(99.99);
    
        return 0;
    }

    Hinweis : Die Boilerplate kann auf Kosten der Lesbarkeit in ein Makro abstrahiert werden:

    #define foo_optional_arg(type, name, default_value)  \
        public: foo& name(type name) { _##name = name; return *this; } \
        private: type _##name = default_value
    
    struct foo {
        foo_optional_arg(const char*, title, "");
        foo_optional_arg(int, year, 1900);
        foo_optional_arg(float, percent, 0.0);
    
        ...
  3. Variadische Funktionen . Dies ist offensichtlich typunsicher und erfordert Kenntnisse über Typwerbung, um richtig zu sein. Es ist jedoch in reinem C verfügbar, wenn C ++ keine Option ist.

    #include <stdarg.h>
    
    // Pre-defined argument tags
    enum foo_arg { foo_title, foo_year, foo_percent, foo_end };
    
    void foo_impl(int a, int b, ...)
    {
        const char* title = "";
        int year = 1900;
        float percent = 0.0;
    
        va_list args;
        va_start(args, b);
        for (foo_arg arg = (foo_arg)va_arg(args, int); arg != foo_end;
            arg = (foo_arg)va_arg(args, int))
        {
            switch(arg)
            {
            case foo_title:  title = va_arg(args, const char*); break;
            case foo_year:  year = va_arg(args, int); break;
            case foo_percent:  percent = va_arg(args, double); break;
            }
        }
        va_end(args);
    
        printf("title: %s\nyear: %d\npercent: %.2f\n", title, year, percent);
    }
    
    // A helper macro not to forget the 'end' tag.
    #define foo(a, b, ...) foo_impl((a), (b), ##__VA_ARGS__, foo_end)
    
    int main()
    {
        foo(1, 2, foo_title, "foo title", foo_percent, 99.99);
    
        return 0;
    }

    Hinweis : In C ++ kann dies mit verschiedenen Vorlagen typsicher gemacht werden. Der Laufzeitaufwand geht zu Lasten langsamerer Kompilierungszeiten und binärer Aufblähung.

  4. boost :: parameter . Immer noch eine Bibliothek von Drittanbietern, wenn auch etablierter als irgendein obskures Github-Repo. Nachteile: vorlagenlastig.

    #include <boost/parameter/name.hpp>
    #include <boost/parameter/preprocessor.hpp>
    #include <string>
    
    BOOST_PARAMETER_NAME(foo)
    BOOST_PARAMETER_NAME(bar)
    BOOST_PARAMETER_NAME(baz)
    BOOST_PARAMETER_NAME(bonk)
    
    BOOST_PARAMETER_FUNCTION(
        (int),  // the return type of the function, the parentheses are required.
        function_with_named_parameters, // the name of the function.
        tag,  // part of the deep magic. If you use BOOST_PARAMETER_NAME you need to put "tag" here.
        (required // names and types of all required parameters, parentheses are required.
            (foo, (int)) 
            (bar, (float))
        )
        (optional // names, types, and default values of all optional parameters.
            (baz, (bool) , false)
            (bonk, (std::string), "default value")
        ) 
    )
    {
        if (baz && (bar > 1.0)) return foo;
        return bonk.size();
    }
    
    int main()
    {
        function_with_named_parameters(1, 10.0);
        function_with_named_parameters(7, _bar = 3.14);
        function_with_named_parameters( _bar = 0.0, _foo = 42);
        function_with_named_parameters( _bar = 2.5, _bonk= "Hello", _foo = 9);
        function_with_named_parameters(9, 2.5, true, "Hello");
    }

Abschließend möchte ich sagen, dass ich diese kwargs-Bibliothek nicht einfach verwenden würde, weil es in C ++ eine Reihe von Alternativen gibt, die gut genug sind, um dasselbe zu erreichen. Ich persönlich würde mich für 1. oder 2. aus der obigen (nicht erschöpfenden) Liste entscheiden.

Eine Eule
quelle
Gute Antwort! Warum sind aus Neugier für Ansatz 2 die internen Variablen private? Wenn Sie sie publicerstellen, können Sie entweder die Funktion aufrufen oder die Variable direkt festlegen.
Svenevs
@ sjm324, danke. Weil struct fooes sich um ein Wegwerfobjekt handelt, das nur die ursprüngliche Python-Funktionssyntax nachahmt. Übergabe von Namenswerten in einer Zeile an der Anrufstelle. Sie könnten es sein, publicaber das war hier einfach nicht der Punkt.
Eine Eule
Das macht Sinn :)
svenevs
Ein weiteres Problem ist, dass der Code für erfahrene C ++ - Programmierer viel schwieriger zu lesen und zu verstehen ist als normaler Code. Ich habe an einem Programm gearbeitet, in dem jemand es für eine gute Idee gehalten hatte, etwas wie #define PROCEDURE void #define BEGIN {#define END} usw. usw. zu tun, weil er C wie Pascal aussehen lassen wollte. Nochmal sagen?
Jwenting
Gute Antwort. Es stellt sich jedoch die Frage, warum C ++ dies nach all den Jahren immer noch nicht kann. Besonders für Bools. foo (glücklich: = wahr, schnell: = falsch) ist viel einfacher zu folgen als foo (wahr, falsch). (Verwenden Sie hier die Visual Basic-Notation!).
Tuntable