SFINAEは簡単です

TLDR型に特定の名前とシグネチャを持つメソッドがあるかどうかを判断する方法、および型のその他のプロパティを、その気を失うことなく認識する方法。

画像






こんにちは同僚。

SFINAEについてお話したいと思います。SFINAEは、非常に便利な(残念ながら*)C ++言語メカニズムですが、準備ができていない人には非常に頭が痛いようです。 実際、その使用の原則は非常にシンプルで明確であり、いくつかの明確な規定の形で定式化されています。 この記事は、C ++のテンプレートの基本的な知識があり、少なくともC ++ 11に精通している読者を対象としています。

* 残念ながらなぜですか? SFINAEの使用は、広く使用されている言語のイディオムに成長した興味深い美しい手法ですが、型の操作を明示的に記述するツールを用意する方がはるかに良いでしょう。



まず、念のため、C ++でのメタプログラミングについて簡単に説明します。 メタプログラミングは、プログラムのコンパイル中に実行される操作です。 通常の関数で値を取得できるように、メタ関数を使用してコンパイル時の型と定数を取得します。 メタプログラミングの最も一般的な使用法の1つは、唯一のものではありませんが、型のプロパティを認識することです。 もちろん、これはすべて、テンプレートの開発に使用されます。複雑なユーザークラスを扱うのが非自明なコンストラクターなのか、通常のint



なのか、ある型が別の型から継承されるのか、それとも変換できるのかを確認するのに役立つ別に入力します。 SFINAEを適用するメカニズムを古典的な例で見ていきます:メンバー関数が指定されたタイプの引数と戻り値を持つクラスに存在するかどうかをチェックします。 テストメタ関数を作成するすべての段階を詳細に、そして詳細に調べ、それがどこから来たのかを追跡してみます。



SFINAEは、 置換失敗を表す略語はエラーではなく 、次を意味します。関数のオーバーロードを定義する場合、誤ったテンプレートのインスタンス化はコンパイルエラーを引き起こしませんが、最適なオーバーロードの候補リストからは破棄されます。 人間的に言えば、これはこれを意味します:



ある関数のオーバーロードを操作し、必要に応じて人為的なエラーを作成することでそれらの一部を非表示にすることができます。 この場合、非表示の候補はテンプレートである必要があり、エラーの事実はこのテンプレートのパラメーターに依存する必要があります。 コンパイラーは、テンプレートの定義そのものをかろうじて見ただけで、置換なしで他のタイプのエラーを識別できますが、SFINAEは成功しませんが、代わりに痛々しいoldりを受けます。



簡単な例を考えてみましょう。

 int difference(int val1, int val2) { return val1 - val2; } template<typename T> typename T::difference_type difference(const T& val1, const T& val2) { return val1 - val2; }
      
      





difference



関数は、整数引数に対して正常に機能します。 しかし、カスタムデータ型では、微妙なことが始まります。 減算の結果は、常にオペランドと同じ型であるとは限りません。 したがって、2つの日付の差は時間間隔であり、それ自体は日付ではありません。 カスタム型MyDate



typedef MyInterval difference_type;



定義が含まれている場合、 typedef MyInterval difference_type;



および減算MyInterval operator - (const MyDate& rhs) const;



、テンプレートのオーバーロードが適用されます。 呼び出しのdifference(date1, date2)



は、テンプレートのオーバーロードとint



を受け入れるバージョンの両方を「見る」ことができますが、テンプレートのオーバーロードはより適切であると見なされます。

MyString



が存在しないMyString



タイプは、置換されるとエラーを引き起こします。関数は存在しないタイプを返します。 タイプMyString



引数を使用したdifference



呼び出しは、関数のint



バージョンのみを「見る」ことができます。 この単一バージョンは、数値演算子への変換がMyString



で定義されている場合にのみ適切MyString



val1 - val2



の構築には、2進数のマイナス演算子が必要であり、構文エラーを生成することもあります。 テンプレート関数のdifference



は、3つの条件を同時に同時に満たすために引数の型をチェックすることがわかります: difference_type



の存在、減算演算子の存在、および減算の結果を型difference_type



にキャストする可能性(変換はreturn



によって暗示されます)。 ただし、最初の条件に違反する型にはこのオーバーロードは表示されませんが、2番目または3番目の条件に違反するとコンパイルエラーが発生します。



void foo(int)



メソッドが何らかのタイプであるかどうかを示すメタ関数を作成する方法を考えてみましょう。 特にバージョンC ++ 11から始まるSTLの管理では、主にtype_traits



およびtype_traits



ヘッダーにある多くの便利なメタ関数が既に特定されていますが、何らかの理由で私たちが大胆に決めたことはありません。 メタ関数は通常、データのないテンプレート構造のように見え、その内部で操作の結果が定義されtypedef



typedef



名前付きtype



または静的定数value



指定されたタイプ。 STLはそのような契約を順守しており、独創的である理由はないため、確立されたモデルを順守します。



将来のメタ機能の「スケルトン」をすぐに書くことができます。
 template<typename T> struct has_foo{};
      
      





メソッドの可用性を決定し、結果がブール型でなければならないことがすぐにわかります。
 template<typename T> struct has_foo{ static constexpr bool value = true; //  ,   ,     "". };
      
      





そして今、必要な型プロパティを定義するオーバーロードを行う方法と、そこからブール定数を取得する方法を理解する必要があります。 利点は、ボディにオーバーロードを与える必要がないことです。すべての作業は型の操作によりコンパイルモードで行われるため、宣言だけで十分です。

明らかに、メタファンクションをあらゆるタイプに適用できるようにします。 結局のところ、どのタイプについても、その中に必要なメソッドがあるかどうかを言うことができます。 つまり、どのパラメーターを代入しても、 has_foo



はコンパイルエラーを引き起こすべきではありません。 しかし、タイプTに必要なメソッドがないことが突然判明すると、エラーが発生します。 1つのテスト関数の2つのオーバーロードが必要であることがわかりました。 そのうちの1つである「検出器」は、目的のメソッドを含む型に対してのみ構文的に正しいものでなければなりません。 もう1つの「バッキング」は雑食性である必要があります。つまり、置換されたタイプに十分適している必要があります。 同時に、「検出器」は、「基板」よりも「適合性」において否定できない利点があります。 優先順位が最も低く、同時に過負荷を判断する上で最も雑多なのは、省略記号(省略記号、引数の可変数を示す)です。
 template<typename T> struct has_foo{ void detect(...); //  ""  . static constexpr bool value = true; // -,   ! };
      
      





ここで、「検出器」を宣言する必要があります。 それはテンプレートでなければなりません:テンプレート構造の中に既にあるという事実だけでは十分ではありません! テンプレート内にテンプレートが必要です[数秒間、映画のインセプションのヒーローから承認された視線を楽しみます]。 これは「裏付け」には当てはまりません。これは、破棄しないためです。 ただし、「ディテクタ」では、マジックワードdecltype



を使用します。これは式のタイプを決定し、式自体は計算されず、コードに変換されません。 式として、同じメソッドの呼び出しを、目的の型の引数に置き換えます。 次に、 decltype



答えはメソッドの戻り値の型になります。 そして、その名前のメソッドがない場合、または他のタイプの引数を取る場合、非常に制御されたエラーが発生します。 「ディテクター」がfoo



と同じものを返すようにします。
 template<typename T> struct has_foo{ void detect(...); template<typename U> decltype(U().foo(42)) detect(const U&); static constexpr bool value = true; //   ! };
      
      







const T&



に参照を渡し、 detect



に渡すと、 U



T



と同じ型であることがわかりますT



返された値の型の対応を確認するために、後で検出器を改良するか、途中で他の何かを考え出します。

しかし、待って! 新しく構築された匿名オブジェクトでメソッドを呼び出しますが、デフォルトで構築されます。 しかし、デフォルトのコンストラクタを持たない型をhas_foo



どうなりますか? もちろん、コンパイルエラーです。 目的の型の値を返す関数を宣言する方が適切です。 とにかく呼び出されず、目的の効果が達成されます。 STLもこれを処理しました。 utility



ヘッダーにはdeclval



関数があります。
 template<typename T> struct has_foo{ void detect(...); template<typename U> decltype(std::declval<U>().foo(42)) detect(const U&); static constexpr bool value = true; //  ! };
      
      







「検出器」から「基質」を区別することを学ぶことだけが残っています。 ここでは、同じdecltype



が役立ちます。 「基質」の戻り値の型は常にvoid



で、「検出器」の戻り値の型はメソッドによって返されvoid



。つまり、メソッドが要件を満たしている場合、同じvoid



です。 うまくいきません。 「素材」のタイプをint



変更します。 次に、チェックは簡単です。オブジェクトT



detect



呼び出しがvoid



型の場合、「検出器」は機能し、メソッドは要件に完全に準拠しています。 タイプが異なる場合、「サブストレート」が機能しているか、メソッドが存在し、同じ引数を受け入れますが、何か間違ったものを返します。 STLの注意度を確認し、 is_same



等価性をチェックする型のメタ関数を見つけます。
 template<typename T> struct has_foo{ private: //     . static int detect(...); //     . template<typename U> static decltype(std::declval<U>().foo(42)) detect(const U&); public: static constexpr bool value = std::is_same<void, decltype(detect(std::declval<T>()))>::value; //  , . };
      
      





やあ、私たちは望みを達成しました。 ご覧のとおり、すべてが本当に簡単です。 以前の標準の厳しい条件でこのトリックを行うことができたプログラマーに敬意をdeclval



ますdeclval



などの有用なものがないため、はるかに冗長で独創的です。



SFINAEは非常に広く使用されているため、思いやりのあるSTLにも特別なenable_if



メタ関数が含まれていました。 そのパラメーターはブール定数とタイプ(デフォルトではvoid



)です。 true



渡された場合、タイプtype



メタファンクションに存在します。2番目のパラメーターによって渡されたtype



false



渡された場合、そこにはtype



が存在しないため、非常に制御されたエラーが作成されます。 上記のenable_if



なリストに記載されている考慮事項に照らして、 enable_if



はテンプレートの場合にのみ関数オーバーロードを「削除」でき、「マークされていない」オーバーロードのリストが完全に空のままにならないことを忘れないでください。 テンプレートクラスの特殊化でenable_if



を使用できますが、この場合はSFINAEではなく、 static_assert



ようなものです。



結論として、このメカニズムを使用する可能性は、型のプロパティをチェックするよりもはるかに広いという事実に焦点を当てたいと思います。 目的の目的に直接使用して、関数とメソッドの最適化されたオーバーロードを作成できます。たとえば、ランダムアクセスイテレータを使用すると、シーケンシャルイテレータよりも自由度が高くなります。 必要に応じて、 特に姓がAlexandrescuの場合は 、さらに奇妙なデザインを思いつくことができます 。 この記事で説明した基本原則に基づいて、独自に使用される型の機能にオンザフライで適応できる強力で柔軟で信頼性の高いコードを作成できます。



All Articles