c ++ 11-17テンプレートのコアナレッジ(8)-enable_if <>およびSFINAE
- 前書き
- enable_if <>を使用して、テンプレートを無効にします
- enable_if <>例
- 概念を使用してenable_if <>を簡素化します
- SFINAE(置換の失敗はエラーではありません)
前書き
class Person {
private:
std::string name;
public:
// generic constructor for passed initial name:
template <typename STR>
explicit Person(STR &&n) : name(std::forward<STR>(n)) {
std::cout << "TMPL-CONSTR for '" << name << "'\n";
}
// copy and move constructor:
Person(Person const &p) : name(p.name) {
std::cout << "COPY-CONSTR Person '" << name << "'\n";
}
Person(Person &&p) : name(std::move(p.name)) {
std::cout << "MOVE-CONSTR Person '" << name << "'\n";
}
};
コンストラクターは完全な転送なので、次のようになります。
std::string s = "sname";
Person p1(s); // init with string object => calls TMPL-CONSTR
Person p2("tmp"); // init with string literal => calls TMPL-CONSTR
ただし、コピーコンストラクターを呼び出そうとすると、エラーが報告されます。
Person p3(p1); // ERROR
ただし、パラメーターがconst Personまたはmoveコンストラクターの場合、それは正しいです。
Person const p2c("ctmp"); // init constant object with string literal
Person p3c(p2c); // OK: copy constant Person => calls COPY-CONSTR
Person p4(std::move(p1)); // OK: move Person => calls MOVE-CONST
理由は次のとおりです。C++のオーバーロードルールによると、1つのnonconstant lvalue Person p
メンバーテンプレート
template<typename STR>
Person(STR&& n)
コピーコンストラクタよりも優れている
Person (Person const& p)
STRはPerson&として直接置き換えられるため、コピーコンストラクターもconst変換が必要です。
非定数のコピーコンストラクターを提供することでこの問題を解決できるかもしれませんが、実際に実行したいのは、パラメーターがPerson型の場合にメンバーテンプレートを無効にすることです。これは、を介して実現できますstd::enable_if<>
。
enable_if <>を使用して、テンプレートを無効にします
template<typename T>
typename std::enable_if<(sizeof(T) > 4)>::type
foo() {
}
場合はsizeof(T) > 4
Falseを、テンプレートが無視されます。場合はsizeof(T) > 4
真、テンプレートがに展開されます。
void foo() {
}
std :: enable_if <>は型特性であり、コンパイル時に指定された式(最初のパラメーター)に基づいてその動作を決定します。
- この式が真の場合、次の式が
std::enable_if<>::type
返されます。- 2番目のテンプレートパラメータがない場合、戻り値の型はvoidです。
- それ以外の場合、戻り値の型は2番目のパラメーターの型です。
- 式の結果がfalseの場合、
std::enable_if<>::type
定義されません。以下で説明するSFINAE(代替の失敗はエラーではありません)によると、
これにより、std :: enable_if <>を含むテンプレートが無視されます。
2番目のパラメーターをstd :: enable_if <>に渡す例:
template<typename T>
std::enable_if_t<(sizeof(T) > 4), T>
foo() {
return T();
}
式がtrueの場合、テンプレートは次のように展開されます。
MyType foo();
enable_if <>を宣言に入れるのが少し醜いと思う場合、通常のアプローチは次のとおりです。
template<typename T,
typename = std::enable_if_t<(sizeof(T) > 4)>>
void foo() {
}
ときsizeof(T) > 4
の時間は、これはに拡張されます。
template<typename T,
typename = void>
void foo() {
}
より一般的な方法は、以下の使用に協力することです。
template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>;
template<typename T,
typename = EnableIfSizeGreater4<T>>
void foo() {
}
enable_if <>例
はじめに、enable_if <>を使用して問題を解決します。
template <typename T>
using EnableIfString = std::enable_if_t<std::is_convertible_v<T, std::string>>;
class Person {
private:
std::string name;
public:
// generic constructor for passed initial name:
template <typename STR, typename = EnableIfString<STR>>
explicit Person(STR &&n) : name(std::forward<STR>(n)) {
std::cout << "TMPL-CONSTR for '" << name << "'\n";
}
// copy and move constructor:
Person(Person const &p) : name(p.name) {
std::cout << "COPY-CONSTR Person '" << name << "'\n";
}
Person(Person &&p) : name(std::move(p.name)) {
std::cout << "MOVE-CONSTR Person '" << name << "'\n";
}
};
コアポイント:
- を使用して、メンバーテンプレート関数でのstd :: enable_if <>の記述を簡略化します。
- コンストラクターのパラメーターを文字列に変換できない場合、関数は無効になります。
したがって、次の呼び出しは期待どおりに実行されます。
int main() {
std::string s = "sname";
Person p1(s); // init with string object => calls TMPL-CONSTR
Person p2("tmp"); // init with string literal => calls TMPL-CONSTR
Person p3(p1); // OK => calls COPY-CONSTR
Person p4(std::move(p1)); // OK => calls MOVE-CONST
}
異なるバージョンの文言に注意してください。
- C ++ 17:
using EnableIfString = std::enable_if_t<std::is_convertible_v<T, std::string>>
- C ++ 14:
using EnableIfString = std::enable_if_t<std::is_convertible<T, std::string>::value>
- C ++ 11:
using EnableIfString = typename std::enable_if<std::is_convertible<T, std::string>::value>::type
概念を使用してenable_if <>を簡素化します
それでもenable_if <>が直感的ではないと感じる場合は、前の記事で説明したC ++ 20によって導入された概念を使用できます。
template<typename STR>
requires std::is_convertible_v<STR,std::string>
Person(STR&& n) : name(std::forward<STR>(n)) {
...
}
条件を一般的な概念として定義することもできます。
template<typename T>
concept ConvertibleToString = std::is_convertible_v<T,std::string>;
...
template<typename STR>
requires ConvertibleToString<STR>
Person(STR&& n) : name(std::forward<STR>(n)) {
...
}
次のように変更することもできます。
template<ConvertibleToString STR>
Person(STR&& n) : name(std::forward<STR>(n)) {
...
}
SFINAE(置換の失敗はエラーではありません)
C ++では、さまざまなパラメータータイプに対して関数のオーバーロードを実行するのが非常に一般的です。コンパイラは、呼び出しに最適な関数を選択する必要があります。
これらのオーバーロードされた関数にテンプレート関数が含まれている場合、コンパイラは通常、次の手順を実行します。
- テンプレートパラメータタイプを決定します。
- 関数パラメーターリストと戻り値のテンプレートパラメーターを置き換えます(置換)
- ルールに基づいて、どの関数が最適であるかを決定します。
しかし、交換の結果は無意味かもしれません。現時点では、コンパイラはエラーを報告しませんが、この関数テンプレートを無視します。
この原則をSFINAEと呼びます(「置換の失敗はエラーではありません)
ただし、置換はインスタンス化と同じではありません。テンプレートを最終的にインスタンス化する必要がない場合でも、置き換える必要があります(そうしないと、上記の手順3を実行できません)。ただし、関数宣言に直接表示される関連コンテンツのみが置き換えられます(関数本体は含まれません)。
次の例を考えてみましょう。
// number of elements in a raw array:
template <typename T, unsigned N>
std::size_t len(T (&)[N]) {
return N;
}
// number of elements for a type having size_type:
template <typename T>
typename T::size_type len(T const &t) {
return t.size();
}
配列または文字列を渡す場合、T::size_type
2番目のテンプレート関数は無視されるため、最初の関数テンプレートのみが一致します。
int a[10];
std::cout << len(a); // OK: only len() for array matches
std::cout << len("tmp"); // OK: only len() for array matches
同様に、ベクトルを渡すと、2番目の関数テンプレートにのみ一致します。
std::vector<int> v;
std::cout << len(v); // OK: only len() for a type with size_type matches
これは、size_typeメンバーはあるが、size()メンバー関数がないオブジェクトを渡すこととは異なることに注意してください。例えば:
std::allocator<int> x;
std::cout << len(x); // ERROR: len() function found, but can’t size()
コンパイラーはSFINAEの原則に従って2番目の関数と一致しますが、コンパイラーは見つからないstd::allocator<int>
size()メンバー関数を報告します。2番目の関数は、マッチングプロセス中に無視されませんが、インスタンス化プロセス中にエラーが報告されます。
enable_if <>の使用は、SFINAEを実装するための最も直接的な方法です。
decltypeを使用したSFINAE
テンプレートに適した式を定義することはまれな場合があります。
たとえば、上記の例では、パラメーターにsize_typeメンバーがあり、sizeメンバー関数がない場合、テンプレートは無視されます。以前の定義は次のとおりです。
template<typename T>
typename T::size_type len (T const& t) {
return t.size();
}
std::allocator<int> x;
std::cout << len(x) << '\n'; // ERROR: len() selected, but x has no size()
このような定義により、コンパイラは関数を選択しますが、インスタンス化フェーズ中にエラーを報告します。
この状況に対処するには、通常、次のようにします。
trailing return type
戻り値の型を指定するために使用します(auto-> decltype)- 真である必要があるすべての式をコンマ演算子の前に置きます。
- 型がコンマ演算子の最後の戻り値の型であるオブジェクトを定義します。
といった:
template<typename T>
auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() ) {
return t.size();
}
ここで、decltypeのパラメータはコンマ式なので、最後のパラメータはT::size_type()
関数の戻り値型です。前のコンマを設定する(void)(t.size())
必要があります。