関数の引数のフラグ

そのようなコードに出くわしたことはありますか?

process(true, false); 

この関数は、名前で判断して、何か(プロセス)を処理します。 しかし、パラメーターはどういう意味ですか? ここでどのパラメーターが真で​​、どれが偽ですか? これを呼び出しコードで判断することはできません。

ヒントを与える関数宣言を調べる必要があります。

 void process(bool withValidation, bool withNewEngine); 

明らかに、作者はbool型の2つのパラメーターをフラグ (トグル)として使用します。 関数の実装は次のようになります。

 void process(bool withValidation, bool withNewEngine) { if (withValidation) //  1-  validate(); // %  do_something_toggle_independent_1 if (withNewEngine) //  2-  do_something_new(); else do_something_old(); do_something_toggle_independent_2(); } 

各フラグには意味のある名前があるため、フラグの目的は明らかです。 問題は呼び出し元コードで発生します。 そして、ポイントは、どのフラグが使用されているかをすぐに理解できないことだけではありません。 これを知っていても、彼らの順序を簡単に混乱させることができます。 実際、私の最初の例は次のようになっているはずです。

 process(false, true); 

しかし、私は引数の順序を台無しにしました。

このバグに直面したプログラマーは、関数呼び出しにコメントを追加して、意図を明示的に示す可能性があります。

 process(/*withValidation=*/ false, /*withNewEngine=*/ true); 

そして、これは関数の名前付きパラメーターにあまりにも似ています-C ++にはない機能です。 もしそうなら、それは次のように見えるかもしれません:

 //  C++  : process(withValidation: false, withNewEngine: true); 

しかし、これがC ++の場合であっても、直接転送との互換性はほとんどありません。

 std::function<void(bool, bool)> callback = &process; callback(???, ???); //   ? 

さらに潜在的なバグがこれに関連している可能性があり、追跡がはるかに困難です。 プロセス関数が仮想クラスメソッドであると想像してください。 そして、他のクラスでは、フラグを間違った順序で配置しながら再定義します。

 struct Base { virtual void process(bool withValidation, bool withNewEngine); }; struct Derived : Base { void process(bool withNewEngine, bool withValidation) override; }; 

パラメーターは名前だけが異なり、タイプは同じであるため(両方ともブール値)、コンパイラーは問題に気付きません。

インターフェイスでの論理パラメーターの使用により発生するバグはそこで終わりません。 ほぼすべての組み込み型がブールに変換されるという事実により、次の例はエラーなしでコンパイルされますが、期待されることは行われません。

 std::vector<int> vec; process(vec.data(), vec.size()); 

より一般的な問題は、コンストラクターでboolを使用することです。 2つのコンストラクターを持つクラスがあるとします。

 class C { explicit C(bool withDefaults, bool withChecks); explicit C(int* array, size_t size); }; 

ある時点で、2番目のコンストラクターを削除することを決定し、コンパイラーが修正が必要なすべての場所をユーザーに示すことを期待している場合があります。 しかし、これは起こりません。 boolの暗黙的な変換により、最初のコンストラクターは、2番目のコンストラクターが以前に使用された場所で使用されます。

ただし、人々が通常boolを使用してフラグを表すのには理由があります。 これは、「そのまま」利用できる唯一の組み込み型であり、2つの可能な値のみを表すように設計されています。

乗り換え


これらの問題を解決するには、次の要件を満たすbool以外の型が必要です。

-フラグごとに一意のタイプが作成され、
-暗黙の変換は禁止されています。

C ++ 11では、両方の要件を満たす列挙クラスの概念が導入されています。 bool型を基本列挙型として使用することもできます。 したがって、列挙には2つの可能な値のみが含まれ、1つのブールのサイズを持つことが保証されます。 まず、フラグクラスを定義します。

 enum class WithValidation : bool { False, True }; enum class WithNewEngine : bool { False, True }; 

これで関数を宣言できます:

 void process(WithValidation withValidation, WithNewEngine withNewEngine); 

この宣言にはいくつかの冗長性がありますが、関数を使用する順序は必要に応じて変更されました。

 process(WithValidation::False, WithNewEngine::True); // ok 

また、フラグを間違った順序で配置すると、型の不一致によるコンパイルエラーが発生します。

 process(WithNewEngine::True, WithValidation::False); // ! 

各フラグには、直接転送(完全な転送)で正しく機能する一意のタイプがあり、仮想メソッドの関数宣言およびオーバーライドでパラメーターを間違った順序で配置することはできません。

しかし、リストをフラグとして使用することには代償が伴います。 フラグはブール値に多少似ていますが、列挙クラスはこの類似性を模倣しません。 boolとの間の暗黙的な変換は機能しませんが(これは良いことですが)、明示的な変換も機能せず、これは問題です。 プロセス関数の本体をもう一度見ると、コンパイルされないことがわかります。

 void process(WithValidation withValidation, WithNewEngine withNewEngine) { if (withValidation) // :     bool validate(); // ... } 

明示的な変換を使用する必要があります。

 if (bool(withValidation)) // ok validate(); 

そして、2つのフラグを持つ論理式が必要な場合は、さらに不適切に見えます。

 if (bool(withNewEngine) || bool(withValidation)) validate(); 

さらに、列挙クラスのインスタンスの場合、boolから直接初期化することはできません。

 bool read_bool(); class X { WithNewEngine _withNewEngine; public: X() : _withNewEngine(read_bool()) //  {} }; 

繰り返しますが、明示的な変換を行う必要があります。

 class X { WithNewEngine _withNewEngine; public: X() : _withNewEngine(WithNewEngine(read_bool())) // ok {} }; 

これはセキュリティの追加保証と見なすことができますが、明示的な変換が多すぎます。 列挙クラスには、「明示的」として宣言されたコンストラクターおよび変換演算子よりも「明示的」なものがあります。

agged_bool


boolクラスと列挙クラスを使用するときの問題により、tagged_boolという独自のツールを作成する必要がありました。 ここでその実装を見つけることができます。 彼女はとても小さいです。 これにより、フラグクラスは次のように宣言されます。

 using WithValidation = tagged_bool<class WithValidation_tag>; using WithNewEngine = tagged_bool<class WithNewEngine_tag>; 

「WithValidation_tag」などのタグクラスを事前に宣言する必要があります。 彼の定義は必要ありません。 これは、tagged_boolクラステンプレートの一意の特殊化を作成するために使用されます。 この特殊化は、boolに明示的に変換できます。また、実際には通常のように、アプリケーションの下位レベルに渡されるboolが別の名前を持つ別のフラグになるため、tagged_boolテンプレートのその他の特殊化にも同様に明示的に変換できます。 このようにして作成されたフラグは次のように使用できます。

 void process(WithValidation withValidation, WithNewEngine withNewEngine) { if (withNewEngine || withValidation) // ok validate(); // ... } process(WithValidation{true}, WithNewEngine{false}); // ok process(WithNewEngine{true}, WithValidation{false}); //  

以上です。 agged_boolはExplicitライブラリの一部であり、インターフェイスを設計する際の意図をより明確に表現するためのいくつかのツールが含まれています。

翻訳者から


Andrzejには、以前にタグに関する別の記事がありました-2013年7月5日付の「 直観的なインターフェース-パートI 」(パート2は決して明らかになりません、見ないでください)。 つまり、そこで次の問題が発生しました。

 std::vector<int> v1{5, 6}; // 2 : {5, 6} std::vector<int> v2(5, 6); // 5 : {6, 6, 6, 6, 6} 

コンストラクタの動作がブラケットの形状に依存する場合、これ自体が危険です。 さらに、これにより呼び出しコードが理解できなくなります。

 std::vector<int> v(5, 6); 

5と6とは何ですか? 5 6または6 5ですか? 忘れた場合は、ドキュメントを参照してください。

そして、指定された容量の空のベクターを作成する別のコンストラクターが必要です:std :: vector v(100)。 残念なことに、size_tを1つ取るコンストラクタは既に使用されています。既定で構築されたオブジェクトで満たされた、指定されたサイズのベクトルを作成します。

Andrzejは、この順序は直接送信の可能性を十分に活用していないと述べていますが、コメントでは、この問題はタグなしで解決されると説明しました。

Andrzejは、STLライブラリでのベクターの実装が完全に成功したわけではないという結論に達しました。 タグがデザイナーで使用されている場合は、はるかに簡単です。

 std::vector<int> v1(std::with_size, 10, std::with_value, 6); 

この記事に関連して、次のようになります。

 process(withValidation, true, withNewEngine, false); // ok process(withNewEngine, true, withValidation, false); //  

違いは、タグと値が1つのオブジェクトに結合されるようになったことです。 2016年7月29日付の「 競合するコンストラクター 」という記事で、Andrzej氏はついに、そのような組合のアイデアが好きではないと書いた。

 vector<int> w {with_size{4}, 2}; // {2, 2, 2, 2} vector<int> x {with_size{4}}; // {0, 0, 0, 0} vector<int> y {with_capacity{4}}; // {} 

現在、これらはタグではなく、本格的なオブジェクトです。 誰かがそれらをコンテナに入れることが起こるかもしれません:

 vector<with_size> w {with_size{4}, with_size{2}}; 

このコードの動作も、括弧の形状に依存します。 同じ問題に再び戻った場合、タグを導入してどんな喜びがありましたか? 少なくとも、単純なタグは、コンテナに保存したい人はほとんどいません。 結局のところ、それらには1つの意味しかありません。

しかし、boolでは、この脅威はそれほど怖くありません。 STLコンテナーのコンストラクターは、initializer_listsの一部を除き、boolを受け入れません。 どうやら、これがAndrzejが今回タグと値を組み合わせることにした理由です。

最後に、記事に関するいくつかのコメントの翻訳を提供します。

コメント


クシャタン
2017年2月17日午前11時36分
私はまず、これらすべてのフラグと検証用の出力コード、およびクラスを分離して引数として渡すための新旧エンジンを取り除くことを考えます。 プロセス関数はすでに多くのことを行っています。

アンドジェ・クルゼミェスキ
2017年2月17日午後12時3分
単純な場合、フラグを破棄することが実際に最良の選択である場合があります。 ただし、フラグを設定する決定がコールスタックで数レベル高くなると、そのようなリファクタリングは実行不可能または実用的ではない場合があります。

 int main() { Revert revert_to_old_functionality {read_from_config()}; Layer1::fun(revert_to_old_functionality) } void Layer1::fun(Revert revert) { // something else... Layer2::fun(revert); }; void Layer2::fun(Revert revert) { // something else... Layer3::fun(revert); }; void Layer3::fun(Revert revert) { // something else... if (revert) do_poor_validation(); else do_thorough_validation(); }; 

===スレッドの終わり===

ミケローウェン
2017年2月17日午後10時41分

 class C { explicit C(bool withDefaults, bool withChecks); explicit C(int* array, size_t size); }; 

「明示的」は、単一パラメーターコンストラクターで使用されます。

アンドジェ・クルゼミェスキ
2017年2月20日午前8時22分

(特にC ++ 11標準のリリースでは)1つの引数を持つコンストラクターだけでなく、ほぼすべてのコンストラクターを「明示的」として宣言する正当な理由があります。 場合によっては、デフォルトのコンストラクタでさえ「明示的」として宣言するのが最適です。 誰も気にしない、 この記事に目を向けることを勧めます。
===スレッドの終わり===

ARNAUD
2017年2月18日午後6時39分

「boolとの間の暗黙的な変換は機能しません(そしてそれは良いことですが)が、明示的な変換も機能せず、それは問題です。」

私はこれの何が悪いのか分かりません:

 if(withValidation == WithValidation::True) 

まず、この言語のよく知られている機能を使用します。コードはすべてのC ++専門家に読まれ、理解されます。 次に、boolへの自動変換およびその逆の自動変換に特別なテンプレートを使用しますか? これは私を納得させません。

その他。 しばらくすると、パラメータの1つがブール値でなくなり、値no_engine、engine_v1、engine_v2を取得できることを想像してください。列挙クラスを使用すると、tagged_boolとは異なり、自然な方法でこのような拡張を行うことができます。

アンドジェ・クルゼミェスキ
2017年2月20日午前8時36分

あなたは2つの質問を提起しました。

1.間の選択

 if (withValidation || withNewEngine) 

そして

 if (withValidation == WithValidation::True || withNewEngine == WithNewEngine::True) 

そして、名前空間を使用する場合:

 if (withValidation == SomeNamespace::WithValidation::True || withNewEngine == SomeNamespace::WithNewEngine::True) 


私にとって、これは望ましいレベルのセキュリティと使いやすさの妥協です。 私の個人的な選択はブールより安全なものですが、列挙クラスほど冗長ではありません。 どうやら、あなたの妥協は列挙クラスに近いところにあります。

2. 3番目の状態を追加する機能

将来、第3の状態が必要になると予想される場合、列挙クラスが実際に望ましい場合があります。 そして、そうではないかもしれません。 3番目の状態を追加すると、すべてのifが引き続き適切にコンパイルされますが、それらを編集して3番目の状態チェックを追加することもできます。

私の経験では、これらのフラグは一時的な解決策として使用されており、さらなる開発は3番目の状態を追加するのではなく、使用可能な2つの状態を取り除くことです。 たとえば、プログラムの一部を改善していますが、数か月間、何かを見落とした場合に備えて、ユーザーに古い実装に切り替える機会を与えたいと思います。改善によってすべてが台無しになります。 数か月後にすべてのユーザーが満足したら、古い実装のサポートを削除し、フラグを削除します。

===スレッドの終わり===

mftdev00
2017年3月13日午後1時5分

私は旗がまったく好きではありません。 彼らは唯一の責任の原則と矛盾します。 真の場合は何か、偽の場合は何か他のことを行います

アンドジェ・クルゼミェスキ
2017年3月13日午後1時10分

同意します。 可能な限り、フラグなしで行う必要があります。

===スレッドの終わり===

セブ
2017年3月21日午後6時9分

各タイプのコンストラクターを明示的に削除する代わりに可能ですか?

 constexpr explicit tagged_bool (bool v) : value {v} {} constexpr explicit tagged_bool (int) = delete; constexpr explicit tagged_bool (double) = delete; constexpr explicit tagged_bool (void*) = delete; 

...一度にすべてのタイプ(boolを除く)で削除しますか?

 constexpr explicit tagged_bool (bool v) : value {v} {} template <typename SomethingOtherThanBool> constexpr explicit tagged_bool(SomethingOtherThanBool) = delete; 

アンドジェ・クルゼミェスキ
2017年3月22日午前7時32分

インターフェイスを開発するときに、この可能性を考慮しませんでした。 これを追加すると便利かもしれません。 しかし、これを提案したので、マイナスの影響があるケースがあります:誰かがboolへの暗黙の変換で独自の(安全な)ブール型を使用できます。 この場合、この型をagged_boolで動作させる必要があるかもしれません。

Source: https://habr.com/ru/post/J349602/


All Articles