altebute.hatenablog.com

犬も歩けば規格にあたる

はじめてのSFINAE

ここ数日、初めてSFINAEに触った。
templateの技法を駆使する故、見た目の記法が複雑になり面食らうが、冷静に一つ一つ読み解けばそこまで難しくはない。
自分も使い始めたばかりなので間違いがあるかも知れないので、間違ってたらご指摘お願いします。

SFINAEによる様々なテクニックや記法、それを実現するための標準ライブラリのメタ関数群の解説はWEB上に多数あるが、初めてSFINAEに触れる人に向けた分かりやすい解説というのは殆ど見ないので自分用にメモしておくことにする。

SFINAE(substitution failure is not an error) - Faith and Brave - C++で遊ぼう
http://faithandbrave.hateblo.jp/entry/20071108/1194519897

直訳すると「置き換え失敗はエラーじゃない」...ダサイ
テンプレートの置き換えに失敗してもエラーにならないようにする

これが基本。「テンプレートの置換に失敗したら、そのテンプレートをオーバーロード解決の候補から除外する」というルールが肝となる。

enable_if (C++11) - cpprefjp - C++ Library Reference
https://sites.google.com/site/cpprefjp/reference/type_traits/enable_if

enable_ifは、SFINAEと組み合わせて使用する。関数のパラメータ、戻り値型、デフォルトテンプレートパラメータ等のいずれかでenable_ifのメンバ型typeを使用することにより、テンプレートの置き換え失敗が発生し、SFINAEによってその関数がオーバーロード解決の候補から除外される

勝手に引用を強調したが、原文では強調されていない。強調部分が重要である。

SFINAEの最も一般的な利用法は、型推論とSFINAEを用いた、オーバーロードの解決である。

まず、以下のような関数について考える。

template<typename T>
void Function(T & arg)
{
    ++arg;
}

上記の関数は、引数の型から型Tを推論し、関数テンプレートを具体化する。
この時、引数の型が算術型でない時は、関数テンプレートを具体化したくないとする。
上記の関数に対して、算術型でない適当な型を渡すと「おい、++なんて演算子定義されてねーぞ!!」というエラーになる。
算術型以外は具体化しないようにしておけば、その関数はオーバーロードの解決の候補から除外される。
他にオーバーロードされた候補が無ければ、「その型を引数とするFunctionはないぞ」というエラーになる。

#include <type_traits>
#include <iostream>

using namespace std;

template<typename T>
typename enable_if<is_arithmetic<T>::value>::type Function(T & arg)
{
    ++arg;
}

int main()
{
    int num = 10;
    
    cout << num << endl;
    
    Function(num);
    
    cout << num << endl;
    
    return 0;
}

上記の関数では、関数の戻り値型として、enable_ifを用いている。

is_arithmetic<T>::valueは、Tが算術型であればtrue、そうでなければfalseとなる。
enable_ifは、第一引数がtrueであれば、第二引数の型をメンバ型typeとして定義する。
デフォルト第二引数はvoidなので、上記の関数の返り値は、引数が算術型の時voidとなる。
もし、算術型でない場合は、typeが定義されないので、関数テンプレートの具体化に失敗するが、SFINAEによりエラーにならず、「オーバーロード解決の候補から除外される」のである。
あくまで除外されるので、もし他の関数テンプレートが存在すれば、そちらの関数で具体化出来るかをコンパイラが調べることになる。

標準ライブラリには、この他にも様々なメタ関数群が定義されている。
それらを用いることで、「整数型か浮動小数点実数型かで分岐する」とか、その他様々なテンプレート関数を記述出来る。便利。