C++ の SFINAE とは?
C++に詳しくないプログラマーにもわかりやすい言葉で説明していただけませんか?また、SFINAE は Python のような言語のどの概念に対応していますか?
C++ の SFINAE とは?
C++に詳しくないプログラマーにもわかりやすい言葉で説明していただけませんか?また、SFINAE は Python のような言語のどの概念に対応していますか?
警告: これは非常に長い説明ですが、うまくいけば、SFINAE が何をするかだけでなく、いつ、なぜそれを使用するのかについてのアイデアを与えることができれば幸いです。
さて、これを説明するには、おそらくテンプレートを少しバックアップして説明する必要があります. ご存知のように、Python は一般にダック タイピングと呼ばれるものを使用します。たとえば、関数を呼び出すときに、X が関数によって使用されるすべての操作を提供している限り、オブジェクト X をその関数に渡すことができます。
C++ では、通常の (非テンプレート) 関数では、パラメーターの型を指定する必要があります。次のような関数を定義した場合:
int plus1(int x) { return x + 1; }
その機能は にのみ適用できますint
。のような他のタイプにも同様に適用できるx
方法で使用するという事実または違いはありません-とにかくにのみ適用されます。long
float
int
Python のダック タイピングに近づけるために、代わりにテンプレートを作成できます。
template <class T>
T plus1(T x) { return x + 1; }
これで、Python の場合と非常に似たものになりました。特に、が定義されている任意の型plus1
のオブジェクトに対して同様に呼び出すことができます。x
x + 1
たとえば、いくつかのオブジェクトをストリームに書き出したいとします。残念ながら、これらのオブジェクトの一部は を使用してストリームに書き込まれますがstream << object
、object.write(stream);
代わりに を使用するオブジェクトもあります。ユーザーがどちらを指定しなくても、どちらかを処理できるようにしたいと考えています。現在、テンプレートの特殊化により、特殊化されたテンプレートを作成できるため、構文を使用する型の1 つobject.write(stream)
である場合は、次のようにすることができます。
template <class T>
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
再び休憩。
私たちが望むのは、 をサポートするすべてのオブジェクトに対して最初の特殊化を使用する方法ですが、それ以外のオブジェクトにstream << object;
は 2 番目の特殊化を使用する方法です (ただし、代わりに を使用するオブジェクトに対して 3 番目の特殊化を追加したい場合もありますx.print(stream);
)。
SFINAE を使用してその決定を行うことができます。これを行うには、通常、C++ のいくつかの奇妙な詳細に依存します。sizeof
1 つは、演算子を使用することです。型または式のサイズを決定しますが、式自体を評価せずに、関連する型sizeof
を調べることによって完全にコンパイル時に決定します。たとえば、次のようなものがあるとします。
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<class T> T& ref();
template<class T> T val();
template<class T>
struct has_inserter
{
template<class U>
static char test(char(*)[sizeof(ref<std::ostream>() << val<U>())]);
template<class U>
static long test(...);
enum { value = 1 == sizeof test<T>(0) };
typedef boost::integral_constant<bool, value> type;
};
ここに の 2 つのオーバーロードがありtest
ます。これらの 2 番目は可変引数リスト ( ...
) を取ります。これは、任意の型に一致できることを意味しますが、オーバーロードを選択する際にコンパイラが行う最後の選択でもあるため、最初のものが一致しない場合にのみ一致します。のもう 1 つのオーバーロードは、もう少し興味深いものです。これは、1 つのパラメータを取る関数を定義します: を返す関数へのポインタの配列で、配列のサイズは (本質的に)です。が有効な式でない場合、は 0 を返します。これは、許可されていないサイズ 0 の配列を作成したことを意味します。ここで、SFINAE 自体の出番です。をサポートしていない型に置き換えようとしていますtest
char
sizeof(stream << object)
stream << object
sizeof
operator<<
U
サイズがゼロの配列が生成されるため、失敗します。しかし、これはエラーではありません。関数がオーバーロード セットから削除されたことを意味するだけです。したがって、そのような場合に使用できるのは他の機能だけです。
enum
次に、それが以下の式で使用されます。選択されたオーバーロードからの戻り値を見てtest
、それが 1 に等しいかどうかをチェックします (1 である場合は、返される関数char
が選択されたことを意味しますが、そうでない場合は、返される関数が選択されたことを意味しますlong
) 。 .
結果は、使用has_inserter<type>::value
できる場合はコンパイルされ、l
使用できない場合はそうなります。次に、その値を使用してテンプレートの特殊化を制御し、特定の型の値を書き出す正しい方法を選択できます。some_ostream << object;
0
オーバーロードされたテンプレート関数がある場合、置換されるものが正しい動作をしない可能性があるため、テンプレート置換が実行されたときに使用可能な候補のいくつかがコンパイルできない可能性があります。これはプログラミングエラーとは見なされません。失敗したテンプレートは、その特定のパラメータで使用可能なセットから削除されるだけです。
Pythonに同様の機能があるかどうかはわかりません。また、C++以外のプログラマーがこの機能を気にする必要がある理由もわかりません。ただし、テンプレートについて詳しく知りたい場合は、テンプレートに関する最良の本はC ++テンプレート:完全ガイドです。
SFINAE は、C++ コンパイラがオーバーロードの解決中に一部のテンプレート化された関数のオーバーロードを除外するために使用する原則です (1)
コンパイラは、特定の関数呼び出しを解決するときに、使用可能な関数と関数テンプレートの宣言のセットを考慮して、使用されるものを見つけます。基本的に、それを行うには 2 つのメカニズムがあります。1つは構文として説明できます。与えられた宣言:
template <class T> void f(T); //1
template <class T> void f(T*); //2
template <class T> void f(std::complex<T>); //3
解決f((int)1)
すると、バージョン 2 と 3 が削除さint
れます。同様に、2 番目のバリアントを削除し、3 番目のバリアントを削除します。コンパイラは、関数の引数からテンプレート パラメーターを推定しようとすることでこれを行います。演繹が失敗した場合 (に対してのように)、オーバーロードは破棄されます。complex<T>
T*
T
f(std::complex<float>(1))
f((int*)&x)
T*
int
これが必要な理由は明らかです - 異なる型に対してわずかに異なることをしたいかもしれません (例えば、複素数の絶対値はによって計算されx*conj(x)
、浮動小数点数の計算とは異なる複素数ではなく実数を生成します) )。
以前に宣言型プログラミングを行ったことがある場合、このメカニズムは (Haskell) に似ています。
f Complex x y = ...
f _ = ...
C++ がこれをさらに進める方法は、推定された型が OK の場合でも推定が失敗する可能性があることですが、別の型への逆置換は「無意味な」結果をもたらします (詳細は後述)。例えば:
template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);
推測するときf('c')
(2 番目の引数は暗黙的であるため、単一の引数で呼び出します):
T
一致しますchar
T
char
T
コンパイラは、宣言内のすべての s を s に置き換えますchar
。これにより が得られvoid f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
ます。int [sizeof(char)-sizeof(int)]
です。この配列のサイズは、例えば、-3 (プラットフォームによって異なります)。<= 0
は無効であるため、コンパイラはオーバーロードを破棄します。Substitution Failure Is Not An Error、コンパイラはプログラムを拒否しません。最終的に、複数の関数のオーバーロードが残っている場合、コンパイラは変換シーケンスの比較とテンプレートの部分的な順序付けを使用して、「最適な」ものを選択します。
このように機能する「無意味な」結果が他にもあり、それらは標準 (C++03) のリストに列挙されています。C++0x では、SFINAE の領域がほぼすべての型エラーに拡張されています。
SFINAE エラーの詳細なリストは書きませんが、最も一般的なエラーの一部を以下に示します。
typename T::type
forT = int
またはT = A
whereA
は、 というネストされた型のないクラスtype
です。int C::*
為にC = int
このメカニズムは、私が知っている他のプログラミング言語とは異なります。Haskell で同様のことを行う場合、より強力なガードを使用することになりますが、C++ では不可能です。
1: またはクラス テンプレートについて話すときの部分的なテンプレートの特殊化
Python はまったく役に立ちません。しかし、あなたはすでに基本的にテンプレートに精通していると言います。
最も基本的な SFINAE 構造は の使用ですenable_if
。唯一のトリッキーな部分は、 SFINAE をカプセル化class enable_if
せず、単に公開することです。
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 には、エラー条件 ( ここ ) と多数の並列を設定する構造がありclass enable_if
、それ以外の場合は定義が競合します。1 つの定義を除くすべての定義で何らかのエラーが発生します。コンパイラは、他の定義について文句を言うことなく選択して使用します。
許容されるエラーの種類は、最近標準化されたばかりの主要な詳細ですが、それについて質問していないようです。
Pythonには、SFINAEにリモートで似ているものはありません。Pythonにはテンプレートがなく、テンプレートの特殊化を解決するときに発生するようなパラメーターベースの関数解決もありません。関数のルックアップは、Pythonでは純粋に名前で行われます。