Args de palabras clave al estilo Python en C ++: ¿buenas prácticas o malas ideas?

8

Al tratar de averiguar el orden óptimo para los parámetros opcionales de una función recientemente, me topé con esta publicación de blog y acompañando el repositorio de GitHub , que proporciona un encabezado para una kwargsinstalación similar a Pythonic en C ++. Aunque no terminé usándolo, me pregunto si esto es bueno o no en un lenguaje fuertemente tipado. Después de haber trabajado en Python durante un tiempo, me parece kwargsmuy atractiva la noción de una instalación similar en mi proyecto porque muchos de sus objetos / funciones tienen una serie de parámetros opcionales (que desafortunadamente no se pueden evitar), produciendo largas listas de constructores que difieren en uno o dos parámetros y podrían hacerse mucho más concisos / DRY-ish.

¿Cuál es, en todo caso, la experiencia de otros con cosas como esta? ¿Debería ser evitado? ¿Hay pautas para ello? ¿Cuáles son los posibles problemas / dificultades?

Sebastian Lenartowicz
fuente
Puede encontrar interesantes N4172 (eche un vistazo a las objeciones) y traer parámetros con nombre en C ++ moderno .
manlio

Respuestas:

13

No estoy muy familiarizado con los kwargs de C ++, pero me vienen a la mente algunas desventajas después de desnatar su fuente:

  1. Es una biblioteca de terceros . Es obvio, pero aún necesita encontrar una forma de integrarlo en su proyecto y actualizar la fuente cuando se cambia el repositorio original.
  2. Requieren una declaración previa global de todos los argumentos . El ejemplo simple en la publicación del blog tiene esta sección que es peso muerto:

    #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...);
    
      ...

    No tan conciso como el original pitónico.

  3. Potencial hinchazón binaria . Su función debe ser una plantilla variada, por lo que cada permutación de parámetros generará el código binario nuevamente. El compilador a menudo no puede ver que difieren en trivialidades y fusionan los binarios.
  4. Tiempos de compilación más lentos . Una vez más, su función debe ser una plantilla y la biblioteca en sí está basada en plantillas. Las plantillas no tienen nada de malo, pero los compiladores necesitan tiempo para analizarlas e instanciarlas.

C ++ ofrece alternativas nativas para lograr la funcionalidad de los parámetros con nombre:

  1. Envolturas de estructura . Defina sus parámetros opcionales como campos de una estructura.

    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. Objetos proxy . Los argumentos se almacenan en una estructura temporal que se puede modificar con setters encadenados.

    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;
    }

    Nota : la plantilla puede resumirse en una macro a expensas de la legibilidad:

    #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. Funciones variadas . Obviamente, esto no es seguro para el tipo y requiere el conocimiento de las promociones de tipo para hacerlo bien. Sin embargo, está disponible en C puro si C ++ no es una opción.

    #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;
    }

    Nota : En C ++ esto se puede hacer de tipo seguro con plantillas variadic. La sobrecarga del tiempo de ejecución desaparecerá a expensas de tiempos de compilación más lentos y una hinchazón binaria.

  4. boost :: parámetro . Todavía es una biblioteca de terceros, aunque una biblioteca más establecida que algún repositorio oscuro de Github. Inconvenientes: plantilla pesada.

    #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");
    }

En una nota de cierre, no usaría esta biblioteca de kwargs simplemente porque hay varias alternativas suficientemente buenas en C ++ para lograr lo mismo. Yo personalmente optaría por 1. o 2. de la lista (no exhaustiva) anterior.

Una lechuza
fuente
¡Gran respuesta! Por curiosidad, para el enfoque 2, ¿por qué son las variables internas private? Hacerlos publicsignifica que pueden llamar a la función o establecer la variable directamente.
svenevs
@ sjm324, gracias. Porque struct fooes un objeto desechable solo para imitar la sintaxis original de la función Python; pasar valores de nombre en una línea en el sitio de la llamada. Podrían serlo, publicpero ese no era el punto aquí.
Un búho
Eso tiene sentido :)
svenevs
Otro problema que viene a la mente es que el código es mucho más difícil de leer y comprender para los programadores experimentados de C ++ que el código normal. He trabajado en un programa donde alguien pensó que era una buena idea hacer algo como #define PROCEDURE void #define BEGIN {#define END} etc. etc. porque quería que C se pareciera a Pascal. ¿Decir de nuevo?
Jwenting
Buena respuesta. Pero plantea la pregunta de por qué, después de todos estos años, C ++ todavía no puede hacer esto. Particularmente para bools. foo (feliz: = verdadero, rápido: = falso) es mucho más fácil de seguir que foo (verdadero, falso). (¡Usando la notación de Visual Basic aquí!).
Tuntable