関数シグネチャでstd :: enable_ifを避ける必要があるのはなぜですか


165

スコットマイヤーズは、彼の次の本EC ++ 11の内容とステータスを投稿しました。この本の1つの項目は、std::enable_if関数のシグネチャでの回避」である可能性があると書いています。

std::enable_if 関数の引数として、戻り値の型として、またはクラステンプレートまたは関数テンプレートのパラメーターとして使用して、関数またはクラスをオーバーロードの解決から条件付きで削除できます。

、この質問すべての3つの解決策を示しています。

関数パラメーターとして:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

テンプレートパラメータとして:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

戻り型として:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • どちらのソリューションが推奨され、なぜ他のソリューションを避けるべきですか?
  • れる場合、「回避std::enable_if機能シグネチャの」(通常の関数シグネチャのが、テンプレート特殊化の一部ではない)戻り型などの懸念の使用?
  • メンバー関数テンプレートと非メンバー関数テンプレートに違いはありますか?

通常、オーバーロードも同様に優れているからです。どちらかと言えば、(専用の)クラステンプレートを使用する実装に委任します。
sehe 2013年

メンバー関数は、オーバーロードセット、現在のオーバーロードのに宣言されたオーバーロードが含まれている点が異なります。これは、
さまざま

1
ええと、主観的に言うと、かなり便利な場合が多いのですがstd::enable_if、関数のシグネチャ(特に、醜い追加のnullptr関数の引数のバージョン)は乱雑にしたくありませstatic ifん。テンプレートブラックマジックを使用して、魅力的な言語機能を活用することで、はるかに美しくクリーンに実行できます。これが、可能な場合はいつでもタグディスパッチを好む理由です(まあ、まだ奇妙な引数がありますが、パブリックインターフェイスではなく、醜くて不可解です)。
Christian Rau 2013年

2
私は何を聞きたいです=0typename std::enable_if<std::is_same<U, int>::value, int>::type = 0成し遂げるのかですが?それを理解するための正しいリソースが見つかりませんでした。私は、最初の部分は前に知っている=0メンバーの種類を持っているint場合Uint同じです。どうもありがとう!
astroboylrx 2016

4
@astroboylrxおかしい、私はこれを指摘するコメントを入れるつもりだった。基本的に、その= 0はこれがデフォルトであることを示し、非タイプテンプレートパラメータであるます。これは、デフォルトのタイプテンプレートパラメータがシグネチャの一部ではないため、この方法で行われるため、オーバーロードすることはできません。
Nir Friedman

回答:


107

テンプレートパラメータにハックを入れます

enable_ifテンプレートパラメータのアプローチは、他のものよりも、少なくとも2つの利点があります。

  • 可読性:enable_ifの使用と戻り値/引数の型は、typenameの曖昧性除去器とネストされた型アクセスの1つの乱雑なチャンクにマージされません。曖昧さ回避手段とネストされた型の乱雑さはエイリアステンプレートで軽減できますが、それでも2つの無関係なものをマージします。enable_ifの使用は、戻り値の型ではなくテンプレートパラメータに関連しています。それらをテンプレートパラメータに含めると、重要なものに近くなります。

  • 普遍的な適用性:コンストラクターには戻り値の型がなく、一部の演算子は追加の引数を持つことができないため、他の2つのオプションはどこにも適用できません。いずれにしても、テンプレートでのみSFINAEを使用できるため、テンプレートパラメータにenable_ifを配置することはどこでも機能します。

私にとって、可読性の側面は、この選択における大きな動機付けの要因です。


4
ここFUNCTION_REQUIRESマクロを使用すると、読みやすくなり、C ++ 03コンパイラでも機能し、戻り値の型での使用に依存します。また、関数のシグネチャは一意ではないため、あいまいなオーバーロードエラーが発生するため、関数テンプレートパラメータで使用すると、オーバーロードの問題が発生します。enable_ifenable_if
ポールフルツII 2013

3
これは古い質問ですが、まだ読んでいる人にとっては、@ Paulによって提起された問題の解決策は、enable_ifオーバーロードを許可するデフォルトの非タイプテンプレートパラメーターを使用することです。すなわちのenable_if_t<condition, int> = 0代わりにtypename = enable_if_t<condition>
Nir Friedman

almost-static-ifへのウェイバックリンク:web.archive.org/web/20150726012736/http
//flamingdangerzone.com/…– davidbak

@ R.MartinhoFernandes flamingdangerzoneコメント内のリンクは、今スパイウェアをインストールするページにつながるようです。モデレーターの注意を引くためにフラグを立てました。
nispio

58

std::enable_ifは、テンプレート引数の推定中に、「置換の失敗はエラーではない」(別名SFINAE)原則に依存しています。これは非常に脆弱な言語機能であり、正しく機能させるには非常に注意する必要があります。

  1. 内の条件にenable_ifネストされたテンプレートまたはタイプの定義(ヒント:::トークンを探す)が含まれている場合、これらのネストされたテンプレートまたはタイプの解決は通常、推定されないコンテキストです。このような推定されていないコンテキストでの置換の失敗はエラーですです。
  2. 複数のさまざまな条件 enable_ifオーバーロードの解決があいまいになるためオーバーロードオーバーラップさせることはできません。これは、作成者が自分で確認する必要があることですが、コンパイラの警告は適切です。
  3. enable_ifオーバーロードの解決中に実行可能な関数のセットを操作します。これは、他のスコープから(たとえばADLを介して)もたらされる他の関数の存在に応じて、驚くべき相互作用を持つ可能性があります。これにより、堅牢性が低下します。

要するに、それが機能するときは機能しますが、機能しないときはデバッグが非常に困難になる可能性があります。非常に優れた代替策は、タグディスパッチdetailングを使用することenable_ifです。つまり、で使用するのと同じコンパイル時の条件に基づいてダミー引数を受け取る実装関数(通常はネームスペースまたはヘルパークラス)に委譲します。

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

タグディスパッチはオーバーロードセットを操作しませんが、コンパイル時の式(たとえば、タイプトレイト)を通じて適切な引数を提供することにより、必要な関数を正確に選択するのに役立ちます。私の経験では、これはデバッグし、正しくするのがはるかに簡単です。洗練された型の特性を持つ意欲的なライブラリライターである場合は、enable_ifどういうわけか必要になるかもしれませんが、コンパイル時の条件のほとんどの通常の使用には推奨されません。


22
ただし、タグディスパッチには1つの欠点があります。関数の存在を検出する特性があり、その関数がタグディスパッチアプローチで実装されている場合、常にそのメンバーが存在すると報告され、潜在的な置換エラーの代わりにエラーが発生します。 。SFINAEは主に候補セットからオーバーロードを削除する手法であり、タグディスパッチは2つ(またはそれ以上)のオーバーロードから選択する手法です。機能にはいくつかの重複がありますが、同等ではありません。
R.マルティーニョフェルナンデス2013

@ R.MartinhoFernandesは短い例を挙げて、それをどのenable_ifように正しくするかを説明できますか?
TemplateRex

1
@ R.MartinhoFernandesこれらの点を説明する別の答えがOPに付加価値を与えると思います。:-)ところで、次のような特性の記述is_f_ableは、SFINAEを使用できるライブラリライターにとっては有利だと考えるタスクですが、「通常の」ユーザーで特性が与えられているis_f_able場合は、タグのディスパッチが簡単だと思います。
TemplateRex

1
@hansmaad私はあなたの質問に対処する短い回答を投稿し、代わりにブログ投稿で「SFINAEへまたはSFINAEへではない」という問題に対処します(この質問については少し話題から外れています)。完成する時間があるとすぐに。
R.マルティーニョフェルナンデス

8
SFINAEは「壊れやすい」ですか?何?
2013

5

どちらのソリューションが推奨され、なぜ他のソリューションを避けるべきですか?

  • テンプレートパラメータ

    • コンストラクタで使えます。
    • ユーザー定義の変換演算子で使用できます。
    • C ++ 11以降が必要です。
    • 読みやすいIMOです。
    • 簡単に誤って使用され、オーバーロードでエラーが発生する可能性があります。

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()

    お知らせtypename = std::enable_if_t<cond>の代わりに、正しいですstd::enable_if_t<cond, int>::type = 0

  • 戻り値の型:

    • コンストラクタでは使用できません。(戻り型なし)
    • ユーザー定義の変換演算子では使用できません。(推定できません)
    • C ++ 11より前のバージョンを使用できます。
    • 2番目に読みやすいIMO。
  • 最後に、関数パラメーター:

    • C ++ 11より前のバージョンを使用できます。
    • コンストラクタで使えます。
    • ユーザー定義の変換演算子では使用できません。(パラメーターなし)
    • これは、引数(単項/二項演算子の固定数の方法で使用することができません+-*、...)
    • 継承で安全に使用できます(以下を参照)。
    • 関数のシグネチャを変更します(基本的に最後の引数として追加がありますvoid* = nullptr)(そのため、関数ポインターが異なるなど)

メンバー関数テンプレートと非メンバー関数テンプレートに違いはありますか?

継承とには微妙な違いがありますusing

using-declarator(強調鉱山)によると:

namespace.udecl

using-declaratorによって導入された一連の宣言は、using-declarator内の名前に対して修飾名検索([basic.lookup.qual]、[class.member.lookup])を実行することで見つかります。未満。

...

using-declaratorが基本クラスから派生クラスに宣言をもたらす場合、派生クラスのメンバー関数とメンバー関数テンプレートは、同じ名前、parameter-type-list、cv-のメンバー関数とメンバー関数テンプレートオーバーライドまたは非表示にします。資格、および(競合するのではなく)基本クラスのref-qualifier(存在する場合)。このような非表示またはオーバーライドされた宣言は、using-declaratorによって導入された一連の宣言から除外されます。

したがって、テンプレート引数と戻り値型の両方で、メソッドは非表示になっています。

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

デモ(gccが誤って基本関数を見つける)。

一方、引数がある場合、同様のシナリオが機能します。

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

デモ

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.