Argumen Kata Kunci Python-style dalam C ++ - praktik yang baik atau ide yang buruk?

8

Saat mencoba mencari urutan optimal untuk parameter opsional ke suatu fungsi baru-baru ini, saya sengaja menemukan posting blog ini dan menyertai repo GitHub , yang menyediakan header untuk kwargsfasilitas mirip- Pythonic di C ++. Meskipun akhirnya saya tidak menggunakannya, saya bertanya-tanya apakah ini bagus atau tidak dalam bahasa yang sangat diketik. Setelah bekerja di Python untuk sementara waktu, saya menemukan gagasan tentang kwargsfasilitas seperti-di proyek saya sangat menarik karena banyak objek / fungsinya memiliki sejumlah parameter opsional (sayangnya, tidak dapat dihindari,), menghasilkan daftar panjang konstruktor yang berbeda dengan satu atau dua parameter dan bisa dibuat lebih ringkas / KERING-ish.

Apa, jika ada, pengalaman orang lain dengan hal-hal seperti ini? Haruskah itu dihindari? Apakah ada pedoman untuk itu? Apa potensi masalah / jebakan?

Sebastian Lenartowicz
sumber
Anda mungkin menemukan N4172 yang menarik (lihat keberatannya) dan Bawa parameter bernama dalam C ++ modern .
manlio

Jawaban:

13

Saya tidak terlalu mengenal C ++ kwargs tetapi beberapa kerugian muncul di pikiran setelah membaca sepintas sumber mereka:

  1. Ini adalah perpustakaan pihak ketiga . Agak jelas, tetapi Anda masih perlu mencari cara untuk mengintegrasikannya ke dalam proyek Anda dan memperbarui sumber ketika repo asli diubah.
  2. Mereka membutuhkan secara global pra-deklarasi semua argumen . Contoh sederhana pada posting blog memiliki bagian ini yang beratnya mati:

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

    Tidak sesingkat aslinya pythonic.

  3. Potensi mengasapi biner . Fungsi Anda harus berupa templat variadik, sehingga setiap permutasi parameter akan menghasilkan kode biner yang baru. Compiler seringkali tidak dapat melihat mereka berbeda dalam hal sepele dan menggabungkan binari.
  4. Waktu kompilasi lebih lambat . Sekali lagi, fungsi Anda harus berupa templat dan pustaka itu sendiri berbasis templat. Tidak ada yang salah dengan template tetapi kompiler perlu waktu untuk menguraikan dan membuat instantiate.

C ++ menawarkan alternatif asli untuk mencapai fungsi parameter bernama:

  1. Pembungkus Struct . Tentukan parameter opsional Anda sebagai bidang struct.

    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. Objek proxy . Argumen disimpan dalam struct sementara yang dapat dimodifikasi dengan setter berantai.

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

    Catatan : pelat boiler dapat diabstraksi menjadi makro dengan mengorbankan keterbacaan:

    #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. Fungsi variadik . Ini jelas tipe tidak aman dan membutuhkan pengetahuan tentang promosi tipe untuk mendapatkan yang benar. Namun, ini tersedia dalam C murni jika C ++ bukan pilihan.

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

    Catatan : Di C ++ ini bisa dibuat aman dengan template variadic. Run-time overhead akan hilang dengan mengorbankan waktu kompilasi yang lebih lambat dan mengasapi biner.

  4. boost :: parameter . Masih perpustakaan pihak ketiga, meskipun lib lebih mapan daripada beberapa repo github jelas. Kelemahan: template-berat.

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

Pada catatan penutup, saya tidak akan menggunakan pustaka kwargs ini hanya karena ada sejumlah alternatif yang cukup baik di C ++ untuk mencapai hal yang sama. Saya pribadi akan memilih 1. atau 2. dari daftar (tidak lengkap) di atas.

Seekor burung hantu
sumber
Jawaban bagus! Karena penasaran, untuk pendekatan 2, mengapa variabel internal private? Membuatnya publicberarti mereka dapat memanggil fungsi atau mengatur variabel secara langsung.
svenev
@ sjm324, terima kasih. Karena objek struct foomembuang hanya untuk meniru sintaks fungsi Python asli; melewati nilai-nama dalam satu baris di situs panggilan. Mereka bisa saja publictetapi bukan itu intinya di sini.
An Owl
Itu masuk akal :)
svenevs
Masalah lain yang muncul dalam pikiran adalah bahwa kode jauh lebih sulit untuk dibaca dan dipahami oleh programmer C ++ berpengalaman daripada kode normal. Saya telah mengerjakan sebuah program di mana seseorang berpikir itu ide yang baik untuk melakukan sesuatu seperti #define PROCEDURE batal #define BEGIN {#define END} dll. Dll. Karena dia ingin membuat C terlihat seperti Pascal. Katakan lagi?
jwenting
Jawaban bagus. Tapi itu menimbulkan pertanyaan mengapa setelah bertahun-tahun C ++ masih tidak bisa melakukan ini. Khusus untuk bools. foo (senang: = benar, cepat: = salah) jauh lebih mudah diikuti daripada foo (benar, salah). (Menggunakan notasi Visual Basic di sini!).
Tuntable