コンテンツにスキップ

英文维基 | 中文维基 | 日文维基 | 草榴社区

SFINAE

出典: フリー百科事典『ウィキペディア(Wikipedia)』

SFINAESubstitution failure is not an errorの略、スフィネェ[1])とは、C++において多重定義の解決時、引数型がテンプレートである候補関数の展開に不正があっても、エラーにならない状況のことである。

このような状態は、テンプレートメタプログラミングなどの際に、テンプレートの実体化の時にそのテンプレート引数が必要な特性を持っていることを検知する目的に応用される。

概要

[編集]

SFINAEという語が最初に登場したのは、David VandevoordeのC++テンプレートに関する技術書である[2]

具体的には、

  1. 多重定義解決の時に、
  2. テンプレート引数を対応する箇所に展開して実体化ものが解決先の候補となるが、
  3. その展開時にエラー[4]が発生したとしても、
  4. そのエラーはコンパイル時エラーとならず、単に候補から除外されて、コンパイルは続行される

となる状況を表す。 このような状況になり、残った候補の中から多重定義解決が成功すれば、実際に実行可能である。

[編集]
struct Test {
    typedef int foo;
};

template <typename T> 
void f(typename T::foo) {} // 定義#1

template <typename T> 
void f(T) {}               // 定義#2

int main() {
    f<Test>(10); // #1の呼び出し
    f<int>(10);  // #2の呼び出し(int::fooがないので#1では失敗するはずだが、SFINAEのおかげでコンパイルエラーにならず、#2が評価される)
}

この例のように修飾名T::fooに非クラス型を適用しようとした時、intにはfooというメンバーがないので型推論に失敗するが、有効な解決先の候補が残っているため、このプログラムは不正とならない。

応用

[編集]

SFINAEは元々、無関係なテンプレート宣言が(例えばヘッダーファイルのインクルードなどによって)存在する時に、不正なプログラムが作成されないようにするために導入された。しかし、その後、この挙動はコンパイル時のイントロスペクションに使うことができる、特に、テンプレートの実体化の際に、そのテンプレート引数が必要な特性を持っていることを検知することができることが発見された。

例えば、次の例では、必要なtypedefが存在するかどうかを調べることができる。

#include <iostream>

template <typename T>
struct has_typedef_foobar {
    // yes型とno型はそれぞれサイズが異なり、sizeof(yes) == 1でsizeof(no) == 2である。
    typedef char yes[1];
    typedef char no[2];

    template <typename C>
    static yes& test(typename C::foobar*);

    template <typename>
    static no& test(...);

    // test<T>(nullptr)を呼んだ結果のsizeofがsizeof(yes)と等しい場合、最初の多重定義が有効であるという意味で、
    // つまりTはfoobarという入れ子型を持つことを検知できる
    static const bool value = sizeof(test<T>(nullptr)) == sizeof(yes);
};

struct foo {    
    typedef float foobar;
};

int main() {
    std::cout << std::boolalpha;
    std::cout << has_typedef_foobar<int>::value << std::endl; // false(intは子にfoobarを持たない)
    std::cout << has_typedef_foobar<foo>::value << std::endl; // true(foorは子にfoobarを持つ)
}

もし、型Tに入れ子型foobarが定義されていれば、最初の関数testの実体化が成功し、ヌルポインタ定数を渡すことができる(ので、返り値の型はyes型である)。 一方、もし実体化に失敗すれば、有効な関数は2番目のtestだけであり、返り値の型はno型になる。

なお、この2番目の関数引数が...になっているのは、任意の引数を受け付けるためだけではなく、型変換の優先度が低いためである。これにより、2番目の関数より最初の関数が優先的に呼ばれることになり、型推論の曖昧さを回避することができている。

C++11であれば、上記のコードは以下のように簡単にすることができる。

#include <iostream>
#include <type_traits>

// どのようなテンプレート引数であってもvoidになる
template <typename...>
using void_t = void;

template <typename T, typename = void>
struct has_typedef_foobar : std::false_type {};

template <typename T>
struct has_typedef_foobar<T, void_t<typename T::foobar>> : std::true_type {}; // T::foobarが存在すれば、こちらが有効になる

struct foo {
  using foobar = float;
};

int main() {
  std::cout << std::boolalpha;
  std::cout << has_typedef_foobar<int>::value << std::endl;
  std::cout << has_typedef_foobar<foo>::value << std::endl;
}

C++17ではこの技法が標準化され、以下のようにさらに簡潔になった。

#include <iostream>
#include <type_traits>

template <typename T>
using has_typedef_foobar_t = decltype(T::foobar);

struct foo {
  using foobar = float;
};

int main() {
  std::cout << std::boolalpha;
  std::cout << std::is_detected<has_typedef_foobar_t, int>::value << std::endl;
  std::cout << std::is_detected<has_typedef_foobar_t, foo>::value << std::endl;
}

Boostライブラリでは、boost::enable_if[5]などをSFINAEを使って実装している。

参考文献・脚注

[編集]
  1. ^ cpprefjp - C++日本語リファレンス. “任意の式によるSFINAE”. 2017年2月1日閲覧。
  2. ^ Vandevoorde, David; Nicolai M. Josuttis (2002). C++ Templates: The Complete Guide. Addison-Wesley Professional. ISBN 0-201-73484-2 
  3. ^ International Organization for Standardization. "ISO/IEC 14882:2003, Programming languages — C++", § 14.8.2.
  4. ^ ただしC++規格でこのような取り扱いが規定されている場合のみ[3]
  5. ^ Boost Enable If