C ++でSFINAEを機能させる方法


40

プロジェクトで関数SFINAEを頻繁に使用しており、次の2つのアプローチ(スタイル以外)に違いがあるかどうかはわかりません。

#include <cstdlib>
#include <type_traits>
#include <iostream>

template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{
    std::cout << "method 1" << std::endl;
}

template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{
    std::cout << "method 2" << std::endl;
}

int main()
{
    foo<int>();
    foo<double>();

    std::cout << "Done...";
    std::getchar();

    return EXIT_SUCCESS;
}

プログラムの出力は期待どおりです。

method 1
method 2
Done...

私はスタックオーバーフローでより頻繁に使用される方法2を見てきましたが、方法1を好みます。

これら2つのアプローチが異なる状況はありますか?


このプログラムをどのように実行しますか?コンパイルできません。
igelを

@alter igel C ++ 17コンパイラが必要です。MSVC 2019を使用してこの例をテストしましたが、主にClangを使用しています。
キース

関連:why-should-i-avoid-stdenable-if-in-function-signaturesおよびC ++ 20は、コンセプトを持つ新しい方法も導入します:-)
Jarod42

@ Jarod42概念は、C ++ 20で私が最も必要とするものの1つです。
ヴァルはモニカを復活

回答:


35

私はスタックオーバーフローでより頻繁に使用される方法2を見てきましたが、方法1を好みます。

提案:方法2を優先します。

どちらの方法も単一の関数で機能します。この問題は、同じシグネチャを持つ複数の関数があり、セットの1つの関数のみを有効にする場合に発生します。

あなたが有効たいとしfoo()たときに、バージョン1、 bar<T>()(それはだふりconstexpr機能)されtrue、そしてfoo()時に、バージョン2 bar<T>()ですfalse

template <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1
 { }

template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2
 { }

あいまいさがあるため、コンパイルエラーが発生します。foo()同じシグネチャを持つ2つの関数(デフォルトのテンプレートパラメータはシグネチャを変更しません)。

しかし、次の解決策

template <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1
 { }

template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2
 { }

SFINAEが関数のシグネチャを変更するため、機能します。

関連のない観察:3番目のメソッドもあります:戻り値の型を有効/無効にします(クラス/構造体コンストラクターを除く)

template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1
 { }

template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2
 { }

方法2と同様に、方法3は同じシグネチャを持つ代替関数の選択と互換性があります。


1
素晴らしい説明をありがとう、これから方法2と3を優先します:-)
キース

「デフォルトのテンプレートパラメータはシグネチャを変更しません」 -デフォルトのテンプレートパラメータも使用する2番目のバリアントでこれはどのように異なりますか?
Eric

1
@Eric-言うのは簡単ではありません...他の答えがこれをよりよく説明すると思います... SFINAEがデフォルトのテンプレート引数を有効/無効にするfoo()と、明示的な2番目のテンプレートパラメーター(foo<double, double>();呼び出し)を指定して関数を呼び出すときに、関数は引き続き使用できます。そして、利用可能なままである場合、他のバージョンとのあいまいさが存在します。方法2では、SFINAEはデフォルトのパラメーターではなく、2番目の引数を有効/無効にします。したがって、2番目のパラメーターを許可しない置換の失敗があるため、パラメーターを明示的に呼び出すことはできません。そのため、バージョンは利用できず、あいまいさはありません
max66 '25

3
方法3には、通常、シンボル名にリークしないという追加の利点があります。バリアントauto foo() -> std::enable_if_t<...>は、関数シグネチャを隠すことを避け、関数引数を使用できるようにするのに役立つことがよくあります。
デュプリケータ

@ max66:重要な点は、パラメーターが指定されていてデフォルトが必要ない場合、テンプレートパラメーターのデフォルトでの置換の失敗はエラーではないということです。
Eric

21

max66の回答に加えて、方法2を選択するもう1つの理由は、方法1を使用すると、(誤って)2番目のテンプレート引数として明示的な型パラメーターを渡し、SFINAEメカニズムを完全に無効にすることができるためです。これは、タイプミス、コピー/貼り付けエラー、またはより大きなテンプレートメカニズムの見落としとして発生する可能性があります。

#include <cstdlib>
#include <type_traits>
#include <iostream>

// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){
    std::cout << "method 1" << std::endl;
}

int main(){

    // works fine
    foo<int>();

    // ERROR: subsitution failure, as expected
    // foo<double>();

    // Oops! also works, even though T != int :(
    foo<double, double>();

    return 0;
}

ここでライブデモ


いい視点ね。メカニズムは乗っ取られる可能性があります。
max66
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.