Argumentos de palavras-chave no estilo Python em C ++ - boas práticas ou más idéias?

8

Enquanto tentava descobrir a ordem ideal para parâmetros opcionais para uma função recentemente, deparei com esta postagem no blog e acompanhando o repositório GitHub , que fornece um cabeçalho para uma kwargsinstalação semelhante a Pythonic em C ++. Embora eu não tenha terminado de usá-lo, me pergunto se isso é bom ou não em uma linguagem fortemente tipada. Tendo trabalhado em Python por um tempo, acho a idéia de uma kwargsinstalação semelhante no meu projeto muito atraente, porque muitos de seus objetos / funções têm vários parâmetros opcionais (que não podem ser evitados, infelizmente), produzindo longas listas de construtores que diferem em um ou dois parâmetros e podem ser muito mais sucintos / DRY-ish.

Qual é a experiência dos outros com coisas assim? Deve ser evitado? Existem diretrizes para isso? Quais são os possíveis problemas / armadilhas?

Sebastian Lenartowicz
fonte
Você pode achar interessante o N4172 (dê uma olhada nas objeções) e Traga parâmetros nomeados no C ++ moderno .
manlio 31/08/16

Respostas:

13

Não conheço muito bem os kwargs de C ++, mas algumas desvantagens vêm à mente depois de analisar sua fonte:

  1. É uma biblioteca de terceiros . É óbvio, mas você ainda precisa descobrir uma maneira de integrá-lo ao seu projeto e atualizar a fonte quando o repositório original for alterado.
  2. Eles exigem pré-declaração global de todos os argumentos . O exemplo simples na postagem do blog tem esta seção que é um peso morto:

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

    Não é tão conciso quanto o original pitonico.

  3. Potencial inchaço binário . Sua função precisa ser um modelo variável, para que cada permutação de parâmetros gere o código binário novamente. O compilador geralmente não consegue ver que diferem em trivialidades e mescla os binários.
  4. Tempos de compilação mais lentos . Novamente, sua função precisa ser um modelo e a própria biblioteca é baseada em modelos. Não há nada errado com os modelos, mas os compiladores precisam de tempo para analisá-los e instancia-los.

O C ++ oferece alternativas nativas para alcançar a funcionalidade dos parâmetros nomeados:

  1. Wrappers de estrutura . Defina seus parâmetros opcionais como campos de uma estrutura.

    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 de proxy . Os argumentos são armazenados em uma estrutura temporária que pode ser modificada com setters encadeados.

    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 : o boilerplate pode ser abstraído para uma macro em detrimento da legibilidade:

    #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. Funções variáveis . Obviamente, isso não é seguro para o tipo e requer conhecimento de promoções de tipo para ser acertado. Está, no entanto, disponível em C puro se C ++ não for uma opção.

    #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 : No C ++, isso pode ser feito com segurança de tipo com modelos variados. A sobrecarga do tempo de execução desaparecerá às custas de tempos de compilação mais lentos e inchaço binário.

  4. boost :: parameter . Ainda é uma biblioteca de terceiros, embora a lib seja mais estabelecida que alguns repositórios obscuros do github. Desvantagens: pesado de modelo.

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

Em uma nota final, eu não usaria essa biblioteca kwargs simplesmente porque há várias alternativas boas o suficiente em C ++ para obter o mesmo. Eu pessoalmente optaria por 1. ou 2. da lista (não exaustiva) acima.

Uma coruja
fonte
Ótima resposta! Por curiosidade, para a abordagem 2, por que as variáveis ​​internas private? Torná-los publicsignifica que eles podem chamar a função ou definir a variável diretamente.
Svenevs
@ sjm324, obrigado. Porque struct fooé um objeto descartável apenas para imitar a sintaxe original da função Python; passando valores de nome em uma linha no site da chamada. Eles poderiam ser, publicmas esse não era o ponto aqui.
An Owl
Isso faz sentido :) #
301 svenevs
outro problema que vem à mente é que o código é muito mais difícil de ler e compreender para programadores C ++ experientes do que o código normal. Eu trabalhei em um programa em que alguém achou uma boa idéia fazer algo como #define PROCEDURE void #define BEGIN {#define END} etc. etc. porque ele queria fazer C parecer Pascal. Repita?
Jwenting
Boa resposta. Mas ele levanta a questão de por que, depois de todos esses anos, o C ++ ainda não pode fazer isso. Especialmente para bools. foo (feliz: = verdadeiro, rápido: = falso) é muito mais fácil de seguir do que foo (verdadeiro, falso). (Usando a notação do Visual Basic aqui!).
Tuntable 26/03/19