コンテンツにスキップ

C++/SFINAE

出典: フリー教科書『ウィキブックス(Wikibooks)』
< C++

SFINAE (Substitution Failure Is Not An Error) は C++ の強力な機能の一つで、テンプレートとオーバーロード解決の過程で使われます。コンパイラが型パラメータを推論する際、特定の関数呼び出しが不適切な場合、その関数は無視(排除)されます。これにより、コンパイルエラーを回避して、より適切な関数を選択することができます。

SFINAEの動作原理は、テンプレートのパターンマッチングを利用しています。もしテンプレートの実体化に失敗すると、その実体はオーバーロード解決から外されます。つまり、エラーにはなりませんが、その実装は無視されます。

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

template<typename T>
typename T::type foo(T t) { /*...*/ } 

struct A {};

int main() {
  A a;
  foo(a); // <-- エラーにはならない。候補から外される。
}

上のコードでは、Aにはtypeメンバがないためfooのインスタンス化に失敗しますが、エラーにはなりません。単に候補から外されるだけです。

SFINAEはよく、テンプレートメタプログラミングでトレイト(trait)を実装するのに使われます。型の性質を検査して、その型に対して適切な実装を選ぶことができます。

複雑な適用例

[編集]

SFINAE はテンプレートメタプログラミングにおいて非常に重要な役割を果たしますが、その振る舞いを理解するのは簡単ではありません。なぜなら、コンパイラの振る舞いは直感的ではないからです。

次の例でより複雑な SFINAE の適用例を見てみましょう。

template <typename T>
typename T::type foo(T t, typename T::type* = nullptr) {
    // 実装...
}

template <typename T>
void foo(T t) {
    // 実装...  
}

struct X { typedef int type; };

int main() {
    X x;
    foo(x); // T::typeが存在するので、第1の関数が呼び出される
    foo(1); // 整数型にはT::typeがないので、第2の関数が呼び出される
}

この例では、2つの foo のオーバーロードがあります。最初の foo はデフォルト引数を持つテンプレート関数で、第2引数の型が T::type によって導出されます。2番目の foo は単なるテンプレート関数です。

X 型のオブジェクト x に対して foo(x) を呼び出すと、コンパイラは最初の foo を試みます。X には type があるので、この実体化は成功します。

一方、foo(1) の呼び出しに対しては、最初の foo の実体化に失敗します。なぜなら、整数型には type がないからです。この場合、SFINAE の規則により最初の foo は無視され、2番目の foo が選ばれます。

このように、SFINAE は柔軟性のあるコード記述を可能にしますが、期待する動作とコンパイラの実際の振る舞いが一致しないことがあり、混乱を招く可能性があります。

SFINAE はジェネリックプログラミングに不可欠な機能ですが、うまく活用するには経験とテクニックが必要です。コードの複雑さを避けるために、可能な限り SFINAE を使わずに済むよう設計することも大切です。

注意点

[編集]

SFINAEは非常に強力な機能ですが、時として予期せぬ振る舞いを引き起こすことがあります。そのため、SFINAEを使う際は細心の注意を払う必要があります。

ここでは、SFINAEに関連するいくつかの注意点を挙げます。

部分的な一致
SFINAEは、テンプレートパラメータの代入失敗だけでなく、部分的な一致にも影響します。つまり、一部のパラメータが一致しても、他が失敗すれば除外されます。
template <class T, class U> struct A;         // #1
template <class T>           struct A<T,T>;   // #2 (部分特殊化)

A<int,int> a;     // #2が一致。#1は除外される
A<int,double> b;  // 両方が除外される
関数テンプレートの特殊化
関数テンプレートの特殊化では、SFINAEの効果は及びません。つまり、不適切な特殊化は単にコンパイルエラーになります。
エイリアステンプレート
C++11から導入されたエイリアステンプレートは、SFINAEの影響を受けます。
template <class T>
using ptr = T*;

template <class T>
void func(ptr<T> p) {} // ptr<T>がSFINAEに影響される

func(nullptr); // OK、T=nullptr_tで実体化される
trailing関数シンタックス
C++11のtrailing返り値型とC++14のauto関数返り値型は、SFINAEの影響を受けません。
制御フロー
SFINAEは関数の実装には影響せず、単にオーバーロード解決にのみ影響します。テンプレート関数内のコードには影響しません。

SFINAEは強力ですが、正しく扱わないと予期せぬ動作の原因になります。SFINAE関連のバグを回避するには、可能な限りSFINAEを使わないようにするか、テストケースを十分に用意する必要があります。

SFINAEは上級C++プログラマにとって重要な概念ですが、安全で移植性の高いコードを書くには、SFINAEの利用を最小限に抑えることも選択肢の一つです。

実践的な応用例

[編集]

これまでの説明では、SFINAEの基本的な動作と注意点について解説しました。ここからは、SFINAEの実践的な応用例をいくつか挙げていきます。

トレイト(trait)の実装
SFINAEは、型の性質を検査するトレイトクラスを実装するのに最も広く使われています。代表的なものとして、std::is_integralやstd::is_polymorphicなどがあります。
template <typename T>
struct is_pointer : std::false_type {};

template <typename T>
struct is_pointer<T*> : std::true_type {};

template <typename T>
void func(T val, std::true_type) {
    // ポインタ型の処理
}

template <typename T>
void func(T val, std::false_type) {
    // 非ポインタ型の処理  
}

template <typename T>
void run(T val) {
    func(val, is_pointer<T>{});
}
上記の例では、is_pointerトレイトクラスを定義し、funcの適切なオーバーロードを選択するのにSFINAEを利用しています。
タグディスパッチ(Tag Dispatch)
SFINAEを使うと、ある条件に基づいてオーバーロード関数を選択するタグディスパッチパターンを実装できます。
struct tag1 {};
struct tag2 {};

template <typename T>
auto dispatch(T val, tag1) { /* 処理1 */ }

template <typename T>  
auto dispatch(T val, tag2) { /* 処理2 */ }

template <typename T>
auto run(T val) {
    if (条件) 
        return dispatch(val, tag1{});
    else
        return dispatch(val, tag2{});
}
コンパイル時計算
SFINAEを使えば、コンパイル時に値を計算したり、特定の条件を評価できます。こうした計算は、テンプレートメタプログラミングと組み合わせて利用されます。
template <unsigned N>
struct factorial {
    static const unsigned value = N * factorial<N - 1>::value;
};

template <>
struct factorial<0> {
    static const unsigned value = 1;
};

int main() {
    constexpr unsigned f6 = factorial<6>::value; // 720
    return 0;
}
上記では、コンパイル時にN!を計算しています。0!のケースはSFINAEにより特殊化されています。

これらに加え、SFINAEは型の是非を検査したり、ユーザ定義型変換を制御したり、最適なアルゴリズムを選択したりと、様々な場面で活躍します。

しかしながら、SFINAEを過剰に使うと、コードが難解になり保守性が落ちる恐れがあります。そのため、状況に応じて適切に使うことが重要です。