C ++でのPythonスタイルのキーワード引数-良い習慣か悪い考えか?


8

最近、関数へのオプションのパラメーターの最適な順序を見つけようとしているときに、このブログの投稿と、C ++のPythonのような機能のヘッダーを提供するGitHubリポジトリを偶然見つけましたkwargs。結局使ったわけではないのですが、強く型付けされた言語でこれがいいのか疑問に思っています。しばらくPythonで作業していkwargsて、プロジェクト内の-のような機能の概念が非常に魅力的であることがわかります。そのオブジェクト/関数の多くには多数のオプションパラメーター(残念ながら回避できない)があり、コンストラクターの長いリストが生成されるためです。 1つまたは2つのパラメータが異なり、はるかに簡潔/ DRY風にすることができます。

このようなことを他の人が経験したとしたら、どうですか。避けるべきですか?ガイドラインはありますか?潜在的な問題/落とし穴は何ですか?


回答:


13

私はC ++ kwargsについてはあまり詳しくありませんが、ソースをざっと読んだ後、いくつかの欠点が頭に浮かびます。

  1. サードパーティのライブラリです。明らかですが、元のリポジトリが変更されたときにプロジェクトに統合してソースを更新する方法を理解する必要があります。
  2. すべての引数をグローバルに事前宣言する必要があります。ブログ投稿の簡単な例には、次のセクションがあります。

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

    Pythonicのオリジナルほど簡潔ではありません。

  3. 潜在的なバイナリ膨張。関数は可変テンプレートである必要があるため、パラメーターの順列ごとにバイナリコードが新たに生成されます。多くの場合、コンパイラーは、それらの違いが小さいことを確認してバイナリーをマージすることができません。
  4. 遅いコンパイル時間。この場合も、関数はテンプレートである必要があり、ライブラリ自体はテンプレートベースです。テンプレートには何も問題はありませんが、コンパイラーはテンプレートを解析してインスタンス化するのに時間が必要です。

C ++は、名前付きパラメーターの機能を実現するためのネイティブの代替手段を提供します。

  1. 構造体ラッパー。オプションのパラメーターを構造体のフィールドとして定義します。

    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. プロキシオブジェクト。引数は、チェーンされたセッターで変更できる一時的な構造体に格納されます。

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

    :読みやすさを犠牲にして、ボイラープレートをマクロに抽象化できます。

    #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. 可変関数。これは明らかにタイプセーフではなく、正しくタイププロモーションを行うための知識が必要です。ただし、C ++がオプションでない場合は、純粋なCで使用できます。

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

    :C ++では、可変個のテンプレートを使用してタイプセーフにすることができます。実行時間のオーバーヘッドは、コンパイル時間の低下とバイナリの膨張を犠牲にしてなくなります。

  4. boost :: parameter。まだサードパーティのライブラリですが、一部のあいまいなgithubリポジトリよりもlibが確立されています。欠点:template-heavy。

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

最後に、C ++には同じことを実現するのに十分な数の代替案があるため、このkwargsライブラリを使用しません。私は個人的に、上記の(完全ではない)リストから1.または2.を選択します。


正解です。好奇心から、アプローチ2で、内部変数はなぜprivateですか?それらpublicを作成することは、関数を呼び出すか、変数を直接設定できることを意味します。
2017

@ sjm324、ありがとう。これstruct fooは、元のPython関数の構文を模倣するための使い捨てオブジェクトだからです。呼び出しサイトで名前と値を1行で渡します。彼らはそうかもしれないpublicが、それはここでのポイントではありませんでした。
フクロウ

それは理にかなっています:)
svenevs

頭に浮かぶもう1つの問題は、経験豊富なC ++プログラマにとって、コードが通常のコードよりも読みにくく、理解しにくいことです。#define PROCEDURE void #define BEGIN {#define END}などを実行することをお勧めします。CをPascalのようにしたかったからです。もう一度お願いします?
2018

いい答えだ。しかし、なぜこれらすべての年月の後でC ++がまだこれを行うことができないのかという疑問を投げかけます。特にboolの場合。foo(happy:= true、fast:= false)は、foo(true、false)よりもはるかに簡単に追跡できます。(ここではVisual Basic表記を使用しています!)
調整可能
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.