
こんにちは同僚。
SFINAEについてお話したいと思います。SFINAEは、非常に便利な(残念ながら*)C ++言語メカニズムですが、準備ができていない人には非常に頭が痛いようです。 実際、その使用の原則は非常にシンプルで明確であり、いくつかの明確な規定の形で定式化されています。 この記事は、C ++のテンプレートの基本的な知識があり、少なくともC ++ 11に精通している読者を対象としています。
* 残念ながらなぜですか? SFINAEの使用は、広く使用されている言語のイディオムに成長した興味深い美しい手法ですが、型の操作を明示的に記述するツールを用意する方がはるかに良いでしょう。
まず、念のため、C ++でのメタプログラミングについて簡単に説明します。 メタプログラミングは、プログラムのコンパイル中に実行される操作です。 通常の関数で値を取得できるように、メタ関数を使用してコンパイル時の型と定数を取得します。 メタプログラミングの最も一般的な使用法の1つは、唯一のものではありませんが、型のプロパティを認識することです。 もちろん、これはすべて、テンプレートの開発に使用されます。複雑なユーザークラスを扱うのが非自明なコンストラクターなのか、通常の
int
なのか、ある型が別の型から継承されるのか、それとも変換できるのかを確認するのに役立つ別に入力します。 SFINAEを適用するメカニズムを古典的な例で見ていきます:メンバー関数が指定されたタイプの引数と戻り値を持つクラスに存在するかどうかをチェックします。 テストメタ関数を作成するすべての段階を詳細に、そして詳細に調べ、それがどこから来たのかを追跡してみます。
SFINAEは、 置換失敗を表す略語はエラーではなく 、次を意味します。関数のオーバーロードを定義する場合、誤ったテンプレートのインスタンス化はコンパイルエラーを引き起こしませんが、最適なオーバーロードの候補リストからは破棄されます。 人間的に言えば、これはこれを意味します:
- 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
ようなものです。
結論として、このメカニズムを使用する可能性は、型のプロパティをチェックするよりもはるかに広いという事実に焦点を当てたいと思います。 目的の目的に直接使用して、関数とメソッドの最適化されたオーバーロードを作成できます。たとえば、ランダムアクセスイテレータを使用すると、シーケンシャルイテレータよりも自由度が高くなります。 必要に応じて、