Các đối số từ khóa kiểu Python trong C ++ - thực hành tốt hay ý tưởng tồi?


8

Trong khi cố gắng tìm ra thứ tự tối ưu cho các tham số tùy chọn cho một chức năng gần đây, tôi tình cờ thấy bài đăng trên blog nàykèm theo repo GitHub , cung cấp tiêu đề cho một kwargscơ sở giống Pythonic trong C ++. Mặc dù tôi đã không sử dụng nó, nhưng tôi tự hỏi liệu đây có phải là một ngôn ngữ tốt hay không. Đã làm việc với Python được một thời gian, tôi thấy khái niệm về một kwargscơ sở giống như trong dự án của tôi rất hấp dẫn bởi vì nhiều đối tượng / hàm của nó có một số tham số tùy chọn (không thể tránh được, thật không may), đưa ra danh sách dài các hàm tạo khác nhau bởi một hoặc hai tham số và có thể được thực hiện ngắn gọn hơn / DRY-ish.

Điều gì, nếu có, là kinh nghiệm của người khác với những thứ như thế này? Có nên tránh? Có hướng dẫn cho nó? Các vấn đề / cạm bẫy tiềm năng là gì?


Bạn có thể tìm thấy N4172 thú vị (hãy xem các phản đối) và Mang các tham số được đặt tên trong C ++ hiện đại .
manlio

Câu trả lời:


13

Tôi không quen thuộc lắm với các kwargs C ++ nhưng một vài nhược điểm xuất hiện sau khi lướt qua nguồn của họ:

  1. Đó là một thư viện của bên thứ ba . Rõ ràng, nhưng bạn vẫn cần tìm cách tích hợp nó vào dự án của mình và cập nhật nguồn khi repo ban đầu được thay đổi.
  2. Họ yêu cầu khai báo trước trên toàn cầu tất cả các đối số . Ví dụ đơn giản trên bài đăng blog có phần này là trọng lượng chết:

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

    Không ngắn gọn như bản gốc pythonic.

  3. Sự phình to tiềm năng . Hàm của bạn cần phải là một khuôn mẫu biến đổi, vì vậy mỗi hoán vị của các tham số sẽ tạo ra mã nhị phân một lần nữa. Trình biên dịch thường không thể thấy chúng khác nhau về tầm thường và hợp nhất các nhị phân.
  4. Thời gian biên dịch chậm hơn . Một lần nữa, chức năng của bạn cần phải là một mẫu và chính thư viện là dựa trên mẫu. Không có gì sai với các mẫu nhưng trình biên dịch cần thời gian để phân tích và khởi tạo chúng.

C ++ cung cấp các lựa chọn thay thế riêng để đạt được chức năng của các tham số được đặt tên:

  1. Cấu trúc bao bọc . Xác định các tham số tùy chọn của bạn dưới dạng các trường của một cấu trúc.

    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. Đối tượng proxy . Các đối số được lưu trữ trong một cấu trúc tạm thời có thể được sửa đổi với setters chuỗi.

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

    Lưu ý : bản tóm tắt có thể được trừu tượng hóa thành một macro với chi phí dễ đọc:

    #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. Chức năng biến thể . Điều này rõ ràng là không an toàn và đòi hỏi kiến ​​thức về các chương trình khuyến mãi để có được quyền. Tuy nhiên, nó có sẵn trong C thuần nếu C ++ không phải là một tùy chọ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;
    }
    

    Lưu ý : Trong C ++, điều này có thể được thực hiện an toàn kiểu với các mẫu matrixdic. Chi phí hoạt động trong thời gian chạy sẽ không còn chi phí cho thời gian biên dịch chậm và phình nhị phân.

  4. boost :: tham số . Vẫn là một thư viện của bên thứ ba, mặc dù lib được thiết lập nhiều hơn một số repo github tối nghĩa. Nhược điểm: nặng mẫu.

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

Trong một lưu ý cuối cùng, tôi sẽ không sử dụng thư viện kwargs này đơn giản vì có một số lựa chọn thay thế đủ tốt trong C ++ để đạt được điều tương tự. Cá nhân tôi sẽ chọn 1. hoặc 2. từ danh sách (không đầy đủ) ở trên.


Câu trả lời chính xác! Vì tò mò, đối với cách tiếp cận 2, tại sao các biến nội bộ private? Làm cho chúng publiccó nghĩa là chúng có thể gọi hàm hoặc đặt biến trực tiếp.
svenevs

@ sjm324, cảm ơn. Bởi vì struct foolà một đối tượng vứt đi chỉ để bắt chước cú pháp hàm Python ban đầu; chuyển các giá trị tên trong một dòng tại trang web cuộc gọi. Họ có thể publicnhưng đó không phải là vấn đề ở đây.
Một con cú

Điều đó có ý nghĩa :)
svenevs

Một vấn đề khác xuất hiện là mã khó đọc và hiểu đối với các lập trình viên C ++ có kinh nghiệm hơn rất nhiều so với mã thông thường. Tôi đã từng làm việc trong một chương trình mà ai đó đã nghĩ rằng nên làm một cái gì đó như #define PROCEDURE void #define BEGIN {#define END}, v.v. vì anh ta muốn làm cho C trông giống Pascal. Nhắc lại?
jwenting

Câu trả lời tốt đẹp. Nhưng nó đặt ra câu hỏi là tại sao sau ngần ấy năm, C ++ vẫn không thể làm được điều này. Riêng đối với bool. foo (happy: = true, fast: = false) dễ theo dõi hơn nhiều so với foo (đúng, sai). (Sử dụng ký hiệu Visual Basic tại đây!).
Tuntable 26/03/19
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.