38


38

C SFINAEを非C プログラマに説明する

C ++のSFINAEとは何ですか?

C ++に精通していないプログラマーが理解できる言葉で説明していただけますか? また、SFINAEはPythonのような言語のどの概念に対応していますか?

5 Answer


97


警告:これは_本当に_長い説明ですが、うまくいけばSFINAEが何をするかだけでなく、いつそれを使用するかもしれないかについてのいくらかのアイデアを与えてくれることを願っています。

さて、これを説明するには、テンプレートを少しバックアップして説明する必要があります。 ご存知のように、Pythonは一般にダックタイピングと呼ばれるものを使用します。たとえば、関数を呼び出すときに、Xがその関数で使用されるすべての操作を提供する限り、オブジェクトXをその関数に渡すことができます。

C ++では、通常の(非テンプレート)関数では、パラメーターのタイプを指定する必要があります。 次のような関数を定義した場合:

int plus1(int x) { return x + 1; }

その関数を「int」に適用することだけができます。 long`や float`のような他の型にも_could_同様に適用されるように x`を使用するという事実は違いはありません-とにかく int`にのみ適用されます。

Pythonのアヒルのタイピングに近づけるには、代わりにテンプレートを作成できます。

template
T plus1(T x) { return x + 1; }

これで、 plus1`はPythonのようになりました。特に、 x + 1`が定義されている任意のタイプのオブジェクト `x`に対しても同様に呼び出すことができます。

ここで、たとえば、いくつかのオブジェクトをストリームに書き出すことを考えてみましょう。 残念ながら、これらのオブジェクトの一部は「stream << object」を使用してストリームに書き込まれますが、他のオブジェクトは代わりに「object.write(stream);」を使用します。 ユーザーがどちらを指定しなくても、どちらかを処理できるようにしたいと考えています。 これで、テンプレートの特殊化により、特殊なテンプレートを記述できるようになりました。そのため、 `object.write(stream)`構文を使用するタイプが_one_だった場合、次のようなことができます。

template
std::ostream &write_object(T object, std::ostream &os) {
    return os << object;
}

template <>
std::ostream &write_object(special_object object, std::ostream &os) {
    return object.write(os);
}

それは1つのタイプには問題ありません。ひどくしたい場合は、「ストリーム<<オブジェクト」をサポートしないタイプにさらに特殊化を追加できますが、(たとえば)ユーザーが新しいタイプを追加するとすぐに`stream << object`をサポートしていないため、事態は再び中断します。

私たちが望むのは、 `stream << object;`をサポートするすべてのオブジェクトに最初の特殊化を使用する方法ですが、他のものには2番目を使用する方法です(ただし、 `x.print(stream ); `代わりに)。

SFINAEを使用してその決定を行うことができます。 そのためには、通常、C ++の他のいくつかの奇妙な詳細に依存します。 1つは、「sizeof」演算子を使用することです。 `sizeof`は、型または式のサイズを決定しますが、式自体を評価せずに、関連する_types_を調べることにより、コンパイル時に完全に決定します。 たとえば、次のようなものがある場合:

int func() { return -1; }

sizeof(func())`を使用できます。 この場合、 `func()`は `int`を返すため、 sizeof(func()) sizeof(int) `と同等です。

よく使用される2番目の興味深い項目は、配列のサイズがゼロではなく、正でなければならないという事実です。

これらをまとめて、次のようなことができます。

// stolen, more or less intact from:
//     http://stackoverflow.com/questions/2127693/sfinae-sizeof-detect-if-expression-compiles
template T& ref();
template T  val();

template
struct has_inserter
{
    template
    static char test(char(*)[sizeof(ref() << val())]);

    template
    static long test(...);

    enum { value = 1 == sizeof test(0) };
    typedef boost::integral_constant type;
};

ここには、 test`の2つのオーバーロードがあります。 これらの2番目は変数の引数リスト( `…​)を取ります。これは、任意の型に一致できることを意味しますが、コンパイラがオーバーロードを選択する際に行う最後の選択でもあるため、最初の場合にのみ一致しますしないでください。 test`のもう1つのオーバーロードはもう少し興味深いものです。1つのパラメーターを受け取る関数を定義します。つまり、配列のサイズが(本質的に) sizeof(stream < <オブジェクト) `。 「stream << object」が有効な式でない場合、「sizeof」は0になります。これは、サイズ0の配列を作成したことを意味しますが、これは許可されていません。 これはSFINAE自体が登場する場所です。 「operator <<」をサポートしない型を「U」の代わりにしようとすると、サイズがゼロの配列が生成されるため失敗します。 しかし、それはエラーではありません-それは単に関数がオーバーロードセットから削除されることを意味します。 したがって、このような場合に使用できるのは他の関数のみです。

それは以下の「enum」式で使用されます-「test」の選択されたオーバーロードからの戻り値を見て、それが1に等しいかどうかをチェックします(等しい場合は、「char」を返す関数が選択されたことを意味し、それ以外の場合は、「long」を返す関数が選択されました)。

その結果、「some_ostream << object;」を使用できれば「has_inserter

value」は「l」になり、コンパイルできなければ「0」になります。 次に、その値を使用してテンプレートの特殊化を制御し、特定のタイプの値を書き出す正しい方法を選択できます。


10


オーバーロードされたテンプレート関数がある場合、テンプレートの置換が実行されると、使用される可能性のある候補の一部がコンパイルに失敗する可能性があります。 これはプログラミングエラーとは見なされません。失敗したテンプレートは、その特定のパラメーターで使用可能なセットから単に削除されます。

Pythonに同様の機能があるかどうかはわかりませんが、C 以外のプログラマがこの機能を気にする必要がある理由はわかりません。 ただし、テンプレートの詳細については、http://www.josuttis.com/tmplbook/ [C Templates:The Complete Guide]をご覧ください。


7


  • SFINAEは、C ++コンパイラがオーバーロード解決中にテンプレート化された関数オーバーロードを除外するために使用する原則です(1)*

コンパイラは、特定の関数呼び出しを解決するときに、使用可能な関数と関数テンプレート宣言のセットを考慮して、使用されるものを見つけます。 基本的に、それを行うには2つのメカニズムがあります。 1つは構文として説明できます。 与えられた宣言:

template  void f(T);                 //1
template  void f(T*);                //2
template  void f(std::complex);   //3
f((int)1)`を解決すると、バージョン2と3が削除されます。これは、 `int`が complex`や一部の T`の T * `と等しくないためです。 同様に、 `f(std

complex(1))`は2番目のバリアントを削除し、 `f((int *)&x)`は3番目のバリアントを削除します。 コンパイラーは、関数の引数からテンプレートパラメーターを推測することでこれを行います。 推論が失敗した場合(「int」に対する「T *」のように)、オーバーロードは破棄されます。

これが必要な理由は明らかです。タイプごとに少し異なることをしたい場合があります(例: 複素数の絶対値は `x * conj(x)`によって計算され、複素数ではなく実数を生成します。これはfloatの計算とは異なります)。

以前に宣言的プログラミングを行ったことがある場合、このメカニズムは(Haskell)に似ています:

f Complex x y = ...
f _           = ...

C ++がこれをさらに進める方法は、演typesされた型がOKであっても演theが失敗するかもしれないが、他への逆代入が何らかの「無意味な」結果をもたらすことです(詳細は後述)。 例えば:

template  void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);

`f( 'c')`を推定するとき(2番目の引数は暗黙的であるため、1つの引数で呼び出します):

  1. コンパイラは、 T`を char`と照合します。 「char」

  2. コンパイラは、宣言内のすべての T`を char`に置き換えます。 これにより、「void f(char t、int(*)[sizeof(char)-sizeof(int)] = 0)」が得られます。

  3. 2番目の引数の型は配列へのポインターです int [sizeof(char)-sizeof(int)]。 この配列のサイズは、たとえば -3(プラットフォームによって異なります)。

  4. 長さ「⇐ 0」の配列は無効であるため、コンパイラは 過負荷。 代替エラーはエラーではありません、コンパイラーはプログラムを拒否しません。

最終的に、複数の関数のオーバーロードが残っている場合、コンパイラは変換シーケンスの比較とテンプレートの半順序を使用して、「最適な」ものを選択します。

このように機能するこのような「無意味な」結果がさらにあり、それらは標準(C 03)のリストに列挙されています。 C 0xでは、SFINAEの領域はほとんどすべてのタイプエラーに拡張されています。

SFINAEエラーの詳細なリストは作成しませんが、最も一般的なエラーのいくつかは次のとおりです。

  • ネストされていないタイプのタイプを選択します。 eg.

    「T = int」または「T = A」の場合、「typename T

    type」。「A」は「type」と呼ばれるネストされたタイプのないクラスです。

  • 非正のサイズの配列タイプを作成します。 例については、を参照してください this litb’s answer

  • クラスではない型へのメンバーポインターを作成します。 eg. int C :: * `C = int`の場合

このメカニズムは、私が知っている他のプログラミング言語のものとは似ていません。 Haskellで同様のことをする場合は、より強力なガードを使用しますが、C ++では不可能です。

'' '' '

1:クラステンプレートについて説明する場合は、テンプレートの部分的な専門化


5


Pythonはまったく役に立ちません。 しかし、あなたはすでに基本的にテンプレートに精通していると言います。

最も基本的なSFINAEコンストラクトは、「enable_if」の使用です。 唯一のトリッキーな部分は、 `class enable_if`はSFINAEを_encapsulate__せず、単に公開するだけです。

template< bool enable >
class enable_if { }; // enable_if contains nothing…

template<>
class enable_if< true > { // … unless argument is true…
public:
    typedef void type; // … in which case there is a dummy definition
};

template< bool b > // if "b" is true,
typename enable_if< b >::type function() {} //the dummy exists: success

template< bool b >
typename enable_if< ! b >::type function() {} // dummy does not exist: failure
    /* But Substitution Failure Is Not An Error!
     So, first definition is used and second, although redundant and
     nonsensical, is quietly ignored. */

int main() {
    function< true >();
}

SFINAEには、エラー条件(ここでは「クラスenable_if」)といくつかの並行する、そうでなければ競合する定義を設定する構造があります。 1つの定義を除くすべての定義でエラーが発生し、コンパイラはそれを選択して、他の定義について文句を言うことなく使用します。

どのような種類のエラーが許容されるかは、最近標準化されたばかりの主要な詳細ですが、あなたはそれについて尋ねていないようです。


3


Pythonには、SFINAEにリモートで似ているものは何もありません。 Pythonにはテンプレートがなく、テンプレートの専門化を解決するときに発生するようなパラメーターベースの関数解決も確かにありません。 関数検索は、Pythonで名前だけで行われます。