C++
SFINAE(代替失敗は誤りではない)
サーチ…
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>::type
はR
エイリアスですが、 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>::value
がfalse
であるため、 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
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>::value
はtrue
になりtrue
-
T
がそのようなメンバ関数を持たない (または複数の引数を必要とする)場合、特殊化のための置換は失敗し、フォールバックするプライマリテンプレートのみがあります。したがって、has_foo<T>::value
はfalse
です。
より単純なケース:
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
制約関数の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()
。以下からの変換以来初めてであることが好ましい0
のint
からの変換よりも良いの暗黙的な変換シーケンスである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
意欲的な例
次のコードスニペットのように、テンプレートパラメータリストにバリデーションテンプレートパックがある場合:
template<typename ...Args> void func(Args &&...args) { //... };
(以前のC ++ 17)標準ライブラリは、内のすべてのパラメータにSFINAEの制約を課すことenable_ifを書くための直接的な方法提供しないArgs
かのパラメータのいずれかの Args
。 C ++ 17が提供していますstd::conjunction
とstd::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::conjunction
とstd::disjunction
かなりストレートフォワードな方法で。次の例では、実装をデモンストレーションし、 std::enable_if
と組み合わせて2つの別名enable_if_all
とenable_if_any
を生成します。これらは、意味的に想定されているものとまったく同じです。これにより、よりスケーラブルなソリューションが提供される可能性があります。
enable_if_all
および enable_if_any
実装
最初のは、エミュレートしましょうstd::conjunction
してstd::disjunction
カスタマイズし使用してseq_and
とseq_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_t
、 is_detected
detected_or
、 detected_t
。
テンプレートパラメータtypename Default
、 template <typename...> Op
とtypename ... Args
:
-
is_detected
:Op<Args...>
std::false_type
Op<Args...>
有効性に応じてstd::true_type
またはstd::false_type
エイリアス -
detected_t
:のエイリアスOp<Args...>
やnonesuch
の妥当性に依存Op<Args...>
-
detected_or
:を持つ構造体の別名value_t
さis_detected
、およびtype
であるOp<Args...>
またはDefault
の有効性に依存Op<Args...>
次のように、SFINAEのstd::void_t
を使用して実装できます。
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つだけを有効にすることは、いくつかの条件も無効にする必要があるため、かなり面倒なことがあります。
オーバーロード間の順序付けは、代わりに継承、つまりタグディスパッチを使用して選択することができます。
整形式を必要とするものをテストするのではなく、他のすべてのバージョン条件の否定をテストする代わりに、必要なものだけをテストします。好ましくは、後続のリターンのdecltype
でdecltype
します。
これにより、いくつかのオプションが適切に形成され、イテレータの特性タグ( 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>{});
}
完全一致が変換より優れている、省略記号よりも優れているなど、オーバーロードを区別するために一般的に使用される他の方法があります。
しかし、タグディスパッチは任意の数の選択に拡張することができ、インテントではもう少し明確です。