サーチ…


enable_if

std::enable_ifはブール条件を使用してSFINAEをトリガする便利なユーティリティです。これは次のように定義されます。

template <bool Cond, typename Result=void>
struct enable_if { };

template <typename Result>
struct enable_if<true, Result> {
    using type = Result;
};

つまり、 enable_if<true, R>::typeRエイリアスですが、 enable_if<false, T>::typeは、 enable_if特殊化にtypeメンバー型がないためenable_ifです。

std::enable_ifを使用してテンプレートを制約できます。

int negate(int i) { return -i; }

template <class F>
auto negate(F f) { return -f(); }

ここで、 negate(1)呼び出しはあいまいさのために失敗します。しかし、2番目のオーバーロードは整数型には使用されませんので、次のように追加できます。

int negate(int i) { return -i; }

template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type>
auto negate(F f) { return -f(); }

今度は、 !std::is_arithmetic<int>::valuefalseであるため、 negate<int>をインスタンス化negate<int>置換が失敗しfalse 。 SFINAEのため、これはハードエラーではなく、この候補は単に過負荷セットから削除されます。その結果、 negate(1)は実行可能な候補が1つしかなく、それが呼び出されます。

それをいつ使用するか

std::enable_ifはSFINAE のヘルパーstd::enable_ifが、SFINAEを最初に動作させるものではありません。 std::sizeと似た機能を実装するためのこれらの2つの選択肢、つまりコンテナまたは配列のサイズを生成するオーバーロードセットsize(arg)を考えてみましょう:

// for containers
template<typename Cont>
auto size1(Cont const& cont) -> decltype( cont.size() );

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size1(Elt const(&arr)[Size]);

// implementation omitted
template<typename Cont>
struct is_sizeable;

// for containers
template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0>
auto size2(Cont const& cont);

// for arrays
template<typename Elt, std::size_t Size>
std::size_t size2(Elt const(&arr)[Size]);

is_sizeableが適切に記述されていると仮定すると、これら2つの宣言はSFINAEに関して正確に同等でなければなりません。書くのが一番簡単で、一目でレビューと理解が一番簡単ですか?

ここで、ラップアラウンドまたはモジュラー動作を優先して符号付き整数オーバーフローを回避する算術ヘルパーを実装する方法を検討してみましょう。つまり、たとえiが値INT_MAX持つintであっても、結果が常に定義されるという事実に対して、例えばincr(i, 3)i += 3と同じになります。これらは2つの可能な選択肢です:

// handle signed types
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>;

// handle unsigned types by just doing target += amount
// since unsigned arithmetic already behaves as intended
template<typename Int>
auto incr1(Int& target, Int amount)
-> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>;
 
template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0>
void incr2(Int& target, Int amount);
 
template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0>
void incr2(Int& target, Int amount);

もう一度書くのが一番簡単で、一目でレビューと理解が一番簡単ですか?

std::enable_ifは、リファクタリングとAPI設計でどのように機能するかです。 is_sizeable<Cont>::valueは、 cont.size()が有効かどうかを反映するためのもので、 size1に表示される式を使用するだけでより簡潔になりますが、 is_sizeableがいくつかの場所で使用されるかどうかによって異なります。インプリメントがincr1の宣言に漏れるときよりもはっきりとその意図を反映するstd::is_signedと対照的です。

void_t

C ++ 11

void_tは任意の数の型をvoid型にマップするメタ関数です。 void_t第一の目的は、型特性の記述を容易にすることです。

std::void_tはC ++ 17の一部ですが、それまでは実装するのが非常に簡単です:

template <class...> using void_t = void;

いくつかのコンパイラで少し異なる実装が必要です

template <class...>
struct make_void { using type = void; };

template <typename... T>
using void_t = typename make_void<T...>::type;

void_tの主な用途は、文の妥当性をチェックする型の特性を書くことです。たとえば、型に引数を取らないメンバー関数foo()があるかどうかを調べてみましょう。

template <class T, class=void>
struct has_foo : std::false_type {};

template <class T>
struct has_foo<T, void_t<decltype(std::declval<T&>().foo())>> : std::true_type {};

これはどのように作動しますか? has_foo<T>::valueをインスタンス化しようとすると、コンパイラはhas_foo<T, void>最適化を探します。私たちには2つの選択肢があります:プライマリと、この基本的な式をインスタンス化しなければならないセカンダリです。

  • 場合Tメンバ関数持って foo() 、次にどのように変換されます戻り型void 、及び特殊を鋳型半順序規則に基づいて一次に好ましいです。したがって、 has_foo<T>::valuetrueになりtrue
  • Tがそのようなメンバ関数を持たない (または複数の引数を必要とする)場合、特殊化のための置換は失敗し、フォールバックするプライマリテンプレートのみがあります。したがって、 has_foo<T>::valuefalseです。

より単純なケース:

template<class T, class=void>
struct can_reference : std::false_type {};

template<class T>
struct can_reference<T, std::void_t<T&>> : std::true_type {};

これはstd::declvalまたはdecltype使用しません。

void引数の共通のパターンに気付くかもしれません。これを因数分解することができます:

struct details {
  template<template<class...>class Z, class=void, class...Ts>
  struct can_apply:
    std::false_type
  {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
    std::true_type
  {};
};

template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

これはstd::void_t使用を隠し、最初のテンプレート引数としてcan_applyた型が他の型に置き換えられたかどうかを示すインジケータのように動作します。前の例は、 can_applyを使ってcan_applyように書き直すことができます:

template<class T>
using ref_t = T&;

template<class T>
using can_reference = can_apply<ref_t, T>;    // Is T& well formed for T?

そして:

template<class T>
using dot_foo_r = decltype(std::declval<T&>().foo());

template<class T>
using can_dot_foo = can_apply< dot_foo_r, T >;    // Is T.foo() well formed for T?

元のバージョンよりもシンプルに見えます。

can_apply似たstd形質のpost-C ++ 17の提案があります。

void_tの有用性はWalter Brownによって発見されました。彼はCppCon2016で素晴らしいプレゼンテーションをしました。

関数テンプレートの後続のdecltype

C ++ 11

制約関数の1つは、末尾のdecltypeを使用して戻り値の型を指定することです。

namespace details {
   using std::to_string;

   // this one is constrained on being able to call to_string(T)
   template <class T>
   auto convert_to_string(T const& val, int )
       -> decltype(to_string(val))
   {
       return to_string(val);
   }

   // this one is unconstrained, but less preferred due to the ellipsis argument
   template <class T>
   std::string convert_to_string(T const& val, ... )
   {
       std::ostringstream oss;
       oss << val;
       return oss.str();
   }
}

template <class T>
std::string convert_to_string(T const& val)
{
    return details::convert_to_string(val, 0);
}

私がto_string()呼び出すことができる引数でconvert_to_string()を呼び出すと、 details::convert_to_string()は2つの実行可能な関数がありdetails::convert_to_string() 。以下からの変換以来初めてであることが好ましい0intからの変換よりも良いの暗黙的な変換シーケンスである0...

to_string()呼び出すことができない引数でconvert_to_string()を呼び出すと、最初の関数テンプレートのインスタンス化によって置換が失敗します( decltype(to_string(val))はありません)。その結果、その候補はオーバーロードセットから削除されます。 2番目の関数テンプレートは拘束されていないので選択され、代わりにoperator<<(std::ostream&, T)ます。それが未定義の場合は、 oss << valという行にテンプレートスタックのハードコンパイルエラーがあります。

SFINAEとは

SFINAEは、 私は N A N Eの rrorをotのよSの ubstitution Fの ailureの略です。関数テンプレートまたはクラステンプレートをインスタンス化するために型(または値)を代入することによって生成される不正なコード 、ハードコンパイルエラーではなく、控除エラーとしてのみ扱われます。

関数テンプレートのインスタンス化またはクラステンプレートの特殊化での控除の失敗は、その候補を考慮セットから削除します。これは、失敗した候補が最初から存在しなかったかのようです。

template <class T>
auto begin(T& c) -> decltype(c.begin()) { return c.begin(); }

template <class T, size_t N>
T* begin(T (&arr)[N]) { return arr; }

int vals[10];
begin(vals); // OK. The first function template substitution fails because
             // vals.begin() is ill-formed. This is not an error! That function
             // is just removed from consideration as a viable overload candidate,
             // leaving us with the array overload. 

直接的なコンテキストでは置換の失敗のみが控除の失敗とみなされ、他はすべてハードエラーとみなされます。

template <class T>
void add_one(T& val) { val += 1; }

int i = 4;
add_one(i); // ok

std::string msg = "Hello";
add_one(msg); // error. msg += 1 is ill-formed for std::string, but this
              // failure is NOT in the immediate context of substituting T

enable_if_all / enable_if_any

C ++ 11

意欲的な例


次のコードスニペットのように、テンプレートパラメータリストにバリデーションテンプレートパックがある場合:

template<typename ...Args> void func(Args &&...args) { //... };

(以前のC ++ 17)標準ライブラリは、内のすべてのパラメータにSFINAEの制約を課すことenable_ifを書くための直接的な方法提供しないArgsかのパラメータのいずれかの Args 。 C ++ 17が提供していますstd::conjunctionstd::disjunctionこの問題を解決します。例えば:

/// C++17: SFINAE constraints on all of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::conjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

/// C++17: SFINAE constraints on any of the parameters in Args.
template<typename ...Args,
         std::enable_if_t<std::disjunction_v<custom_conditions_v<Args>...>>* = nullptr>
void func(Args &&...args) { //... };

C ++ 17を使用できない場合は、これを達成するためのいくつかのソリューションがあります。そのうちの1つは、この質問の回答に示されているように、ベースケースのクラスと部分的な特殊化を使用することです。

あるいは、また、手作業での動作を実装してもよいstd::conjunctionstd::disjunctionかなりストレートフォワードな方法で。次の例では、実装をデモンストレーションし、 std::enable_ifと組み合わせて2つの別名enable_if_allenable_if_anyを生成します。これらは、意味的に想定されているものとまったく同じです。これにより、よりスケーラブルなソリューションが提供される可能性があります。


enable_if_all および enable_if_any 実装


最初のは、エミュレートしましょうstd::conjunctionしてstd::disjunctionカスタマイズし使用してseq_andseq_orそれぞれ:

/// Helper for prior to C++14.
template<bool B, class T, class F >
using conditional_t = typename std::conditional<B,T,F>::type;

/// Emulate C++17 std::conjunction.
template<bool...> struct seq_or: std::false_type {};
template<bool...> struct seq_and: std::true_type {};

template<bool B1, bool... Bs>
struct seq_or<B1,Bs...>: 
  conditional_t<B1,std::true_type,seq_or<Bs...>> {};

template<bool B1, bool... Bs>
struct seq_and<B1,Bs...>:
  conditional_t<B1,seq_and<Bs...>,std::false_type> {};  

その実装はかなり単純です:

template<bool... Bs>
using enable_if_any = std::enable_if<seq_or<Bs...>::value>;

template<bool... Bs>
using enable_if_all = std::enable_if<seq_and<Bs...>::value>;

最終的にいくつかのヘルパー:

template<bool... Bs>
using enable_if_any_t = typename enable_if_any<Bs...>::type;

template<bool... Bs>
using enable_if_all_t = typename enable_if_all<Bs...>::type;

使用法


使い方も簡単です:

    /// SFINAE constraints on all of the parameters in Args.
    template<typename ...Args,
             enable_if_all_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

    /// SFINAE constraints on any of the parameters in Args.
    template<typename ...Args,
             enable_if_any_t<custom_conditions_v<Args>...>* = nullptr>
    void func(Args &&...args) { //... };

is_detected

type_traitの作成を一般化するために:SFINAEに基づいて、実験的な特性がdetected_or detected_tis_detected detected_ordetected_t

テンプレートパラメータtypename Defaulttemplate <typename...> Optypename ... Args

  • is_detectedOp<Args...> std::false_type Op<Args...>有効性に応じてstd::true_typeまたはstd::false_typeエイリアス
  • detected_t :のエイリアスOp<Args...>nonesuchの妥当性に依存Op<Args...>
  • detected_or :を持つ構造体の別名value_tis_detected 、およびtypeであるOp<Args...>またはDefaultの有効性に依存Op<Args...>

次のように、SFINAEのstd::void_tを使用して実装できます。

C ++ 17
namespace detail {
    template <class Default, class AlwaysVoid,
              template<class...> class Op, class... Args>
    struct detector
    {
        using value_t = std::false_type;
        using type = Default;
    };

    template <class Default, template<class...> class Op, class... Args>
    struct detector<Default, std::void_t<Op<Args...>>, Op, Args...>
    {
        using value_t = std::true_type;
        using type = Op<Args...>;
    };

} // namespace detail

// special type to indicate detection failure
struct nonesuch {
    nonesuch() = delete;
    ~nonesuch() = delete;
    nonesuch(nonesuch const&) = delete;
    void operator=(nonesuch const&) = delete;
};

template <template<class...> class Op, class... Args>
using is_detected =
    typename detail::detector<nonesuch, void, Op, Args...>::value_t;

template <template<class...> class Op, class... Args>
using detected_t = typename detail::detector<nonesuch, void, Op, Args...>::type;

template <class Default, template<class...> class Op, class... Args>
using detected_or = detail::detector<Default, void, Op, Args...>;

メソッドの存在を検出するための特性は、単純に実装できます。

typename <typename T, typename ...Ts>
using foo_type = decltype(std::declval<T>().foo(std::declval<Ts>()...));

struct C1 {};

struct C2 {
    int foo(char) const;
};

template <typename T>
using has_foo_char = is_detected<foo_type, T, char>;

static_assert(!has_foo_char<C1>::value, "Unexpected");
static_assert(has_foo_char<C2>::value, "Unexpected");

static_assert(std::is_same<int, detected_t<foo_type, C2, char>>::value,
              "Unexpected");

static_assert(std::is_same<void, // Default
                           detected_or<void, foo_type, C1, char>>::value,
              "Unexpected");
static_assert(std::is_same<int, detected_or<void, foo_type, C2, char>>::value,
              "Unexpected");

多数のオプションを使用した過負荷解決

いくつかのオプションの中から選択する必要がある場合は、 enable_if<>経由で1つだけを有効にすることは、いくつかの条件も無効にする必要があるため、かなり面倒なことがあります。

オーバーロード間の順序付けは、代わりに継承、つまりタグディスパッチを使用して選択することができます。

整形式を必要とするものをテストするのではなく、他のすべてのバージョン条件の否定をテストする代わりに、必要なものだけをテストします。好ましくは、後続のリターンのdecltypedecltypeします。
これにより、いくつかのオプションが適切に形成され、イテレータの特性タグ( random_access_tag )と同様に、「タグ」を使用するものを区別することができます。これは、基本クラスの基本クラスよりも優れている基本クラスよりも、直接一致が優れているために機能します。

#include <algorithm>
#include <iterator>

namespace detail
{
    // this gives us infinite types, that inherit from each other
    template<std::size_t N>
    struct pick : pick<N-1> {};
    template<>
    struct pick<0> {};

    // the overload we want to be preferred have a higher N in pick<N>
    // this is the first helper template function
    template<typename T>
    auto stable_sort(T& t, pick<2>)
        -> decltype( t.stable_sort(), void() )
    {
        // if the container have a member stable_sort, use that
        t.stable_sort();
    }

    // this helper will be second best match
    template<typename T>
    auto stable_sort(T& t, pick<1>)
        -> decltype( t.sort(), void() )
    {
        // if the container have a member sort, but no member stable_sort
        // it's customary that the sort member is stable
        t.sort();
    }

    // this helper will be picked last
    template<typename T>
    auto stable_sort(T& t, pick<0>)
        -> decltype( std::stable_sort(std::begin(t), std::end(t)), void() )
    {
        // the container have neither a member sort, nor member stable_sort
        std::stable_sort(std::begin(t), std::end(t));
    }

}

// this is the function the user calls. it will dispatch the call
// to the correct implementation with the help of 'tags'.
template<typename T>
void stable_sort(T& t)
{
    // use an N that is higher that any used above.
    // this will pick the highest overload that is well formed.
    detail::stable_sort(t, detail::pick<10>{});
}

完全一致が変換より優れている、省略記号よりも優れているなど、オーバーロードを区別するために一般的に使用される他の方法があります。

しかし、タグディスパッチは任意の数の選択に拡張することができ、インテントではもう少し明確です。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow