web-dev-qa-db-ja.com

関数シグネチャでstd :: enable_ifを避けるべき理由

Scott Meyersは、次の本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を避ける"は、戻り値型としての使用に関係します(通常の関数シグネチャの一部ではなく、テンプレートの特殊化の一部です) )?
  • メンバー関数テンプレートと非メンバー関数テンプレートに違いはありますか?
160
hansmaad

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

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

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

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

私にとって、読みやすさの側面は、この選択の大きな動機付け要因です。

103

std::enable_ifは、「 Substition Failure Is Not Not Error "(別名SFINAE)原則template argument控除。これは非常に壊れやすい言語機能であるため、正しく機能させるには細心の注意が必要です。

  1. enable_if内の条件にネストされたテンプレートまたは型定義が含まれる場合(ヒント:::トークンを探す)、これらのネストされたテンプレートまたは型の解決は通常non -推定コンテキスト。このような非推論コンテキストでの置換の失敗は、errorです。
  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が必要になるかもしれませんが、コンパイル時の条件のほとんどの通常の使用には推奨されません。

55
TemplateRex

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

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

    • コンストラクターで使用できます。
    • ユーザー定義の変換演算子で使用できます。
    • 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に注意してください

  • 戻りタイプ:

    • コンストラクターでは使用できません。 (戻りタイプなし)
    • ユーザー定義の変換演算子では使用できません。 (演not不可)
    • 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-qualification、および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) {}
};

デモ

4
Jarod42