関数テンプレート - テンプレート引数を推測するか、明示的に渡すか?

関数テンプレートを使用すると、複数の異なる型を処理できる単一の定義を作成できます。これは、C++ の静的ポリモーフィズムの非常に強力な形式です。

クラス テンプレートをインスタンス化するときは、型を明示的に渡す必要があります (少なくとも C++17 まで):

std::vector<int> vec;
std::basic_string<my_char, std::char_traits<my_char>> str;
std::tuple<int, bool, std::string> tuple;

ただし、関数テンプレートをインスタンス化するとき、コンパイラは多くの場合、型を把握できます:

template <typename A, typename B, typename C>
void func(const A& a, const B& b, const C& c);
…
int x;
func(x, 'A', "hello");
// equivalent to:
func<int, char, const char*>(x, 'A', "hello");

このプロセスをもう少し詳しく見て、いくつかのガイドラインを確立し、引数のテンプレート引数推定を禁止する方法を見てみましょう.

テンプレート引数推定 101

テンプレート引数が推定されるとき、2 つの異なる型があります:テンプレート引数の型とパラメーターの型で、それらは引数の型に依存します。パラメータが宣言されています。

1) 04 または 16

値またはポインタ パラメータがある場合、テンプレート引数の型は decayed です。 引数のタイプ。 27 なし /30 または参照:

template <typename T>
void func(T param);
…
int x;
const int cx = 0;

func(x); // argument is int&, T is int
func(cx); // argument is const int&, T is int

まったく新しい変数であるため、型からすべての修飾子を取り除きます。そのため、44 を保持する必要はありません。 -ness、たとえば。関数パラメーターの修飾子に応じて、関数パラメーターの型は 54 の型になります。 これらの修飾子を使用しますが、これは 66 の型を変更しません .

template <typename T>
void func(const T param); // type will be const T
template <typename T>
void func(T* param); // type will be pointer to T

ポインターがある場合、引数の型はそのポインターに変換可能でなければならないことに注意してください。 /87 へのポインタがある場合、ポインタの一部が削除されます 92 、これは残ります:

template <typename T>
void func(T* param);
…
int* ptr;
const int* cptr;
int* const ptrc;
func(ptr); // argument is int*&, T is int, param is int*
func(cptr); // argument is const int*&, T is const int, param is const int*
func(ptrc); // argument is int* const&, T is int, param is int*

2) 105

左辺値参照であるパラメーターがある場合、引数の型から参照を削除するだけですが、119 は保持します。 /123137 の型のポインタなど :

template <typename T>
void func(T& param);
…
int x;
const int cx = 0;
int* ptr = &x;

func(x); // argument is int&, T is int
func(cx); // argument is const int&, T is const int
func(ptr); // argument is int*, T is int*

パラメータの型は 145 の型になります 参照を再度追加します。153 がある場合 、これにより、参照が 160 への参照であることも保証されます .If 171 181 への参照ではありません 、引数は左辺値でなければなりませんが、 191 への参照にすることができることに注意してください プレーンな 202 で :

template <typename T>
void func1(T& param);
template <typename T>
void func2(const T& param);
…
int a = 0;
const int b = 0;

func1(std::move(a)); // argument is int&&, T is int, param is int&, cannot bind
func2(std::move(a)); // argument is int&&, T is int, param is const int&, can bind

func1(std::move(b)); // argument is const int&&, T is const int, param is const int&, can bind
func2(std::move(b)); // argument is const int&&, T is const int, param is const int&, can bind

3) 219

228 の形式のパラメータがある場合 、ここで 239 関数の直接のテンプレート パラメータです。実際には右辺値参照ではなく、転送参照です。 .これは 245 では起こりません または 258 または 267 、上記のような場合のみ。その後、引数演繹規則は特別で、270 の型です。 引数とまったく同じ型になります (引数が通常の参照のように推測される単純な右辺値でない限り、それは奇妙です):

template <typename T>
void func(T&& param);
…
int x;
const int cx = 0;
int* ptr = &x;

func(x); // argument is int&, T is int&
func(cx); // argument is const int&, T is const int&
func(ptr); // argument is int*&, T is int*&
func(0); // argument is int&&, T is int (param will be int&& anyway)

Scott Meyers を言い換えると:これは ハック です 特別ルール 引数の完全な転送を許可します。

なぜなら参照崩壊と呼ばれるものにより、 281 の型は 292 の型と同じになります したがって、引数の型と同じです。これを使用すると、引数を完全に転送できますが、それはこの投稿の範囲を超えているため、先に進みましょう.

テンプレート引数の推論は素晴らしい

これらのルールを理解するずっと前に、おそらく関数テンプレートとテンプレート引数推定を使用したことがあります。これは、ルールが「機能する」ためです。ほとんどの場合、ルールは期待どおりに動作し、希望どおりに動作します。

したがって、関数テンプレートを呼び出すときに、明示的に引数を渡す必要はありません。逆に、害を及ぼす可能性があります!最初に示した例を考えてみましょう:

template <typename A, typename B, typename C>
void func(const A& a, const B& b, const C& c);
…
int x;
func(x, 'A', "hello");
// equivalent to:
func<int, char, const char*>(x, 'A', "hello");

参照パラメータがあるので、上記のケース 2 です。これは、テンプレート引数の型が参照なしの引数の型と同じになることを意味します。300 の型 315 です 、だから 328 336 になります .343 の型 350 です 、だから 360 374 になります .

しかし、380 の型は何ですか? ? 391 ?

違います。

文字列リテラルの型は 配列 です 、ポインターではありません。

特に 403 の型 415 です - ここに 429 があります 、理由はさまざまです。432 削除された参照は… 449 しない 457 であるため、実際の呼び出しは次のようになります:

func<int, char, const char[6]>(true, "hello");

私の主張を明確にするために、私は意図的にその間違いをしました。

この場合、この例は害を及ぼすことはありませんが、引数をコンストラクターに完全に転送する関数を考えてみましょう

  • 型を台無しにすると、不要な一時ファイルが作成されたり、移動ではなくコピーが作成されたりする可能性があります!型を台無しにすることはできます ランタイム ペナルティがあります。

これは次のガイドラインにつながります:

ガイドライン:コンパイラにテンプレート引数を推測させ、自分では行わないでください

テンプレート引数を手動で推測することは、反復的で、退屈で、エラーが発生しやすく、最も重要なことに、不必要な作業です.コンパイラは、そのようなことを行うよりもはるかに優れているため、STLの言葉を借りると、コンパイラを助けません.

したがって、テンプレート引数を明示的に渡さないでください。

ただし:テンプレート引数の推定は完全ではありません

しかし、場合によっては、テンプレート引数の推論が必要ないこともあります.

その理由を理解するには、転送参照控除のケースをもう一度詳しく調べる必要があります。

template <typename T>
void other_func(T t);

template <typename T>
void func(T&& t)
{
 // perfectly forward t to other_func
}

転送参照は、ものを転送するために使用されます。ここから 462 .476 引数のコピーが必要なので、右辺値の場合は移動され、左辺値の場合はコピーされるようにします。基本的には、次のように動作する必要があります。

other_func(val); // copy
func(val); // also copy

other_func(std::move(val)); // move
func(std::move(val)); // also move

486 の素朴な実装 次のようになります:

template <typename T>
void func(T&& t)
{
 other_func(t);
}

492とお伝えしました 引数とまったく同じになるため、引数が右辺値の場合は右辺値参照、引数が左辺値の場合は左辺値参照になります。

しかし、これは 508 という意味ではありません 511 の場合、引数を移動します 右辺値参照です。コピーします 529 、なぜなら 539 544 名前があり、割り当てることができます - 関数内では左辺値です!

したがって、この実装は常にコピーされ、移動することはありません。

559 は書けません 常に 左辺値であっても移動します!

必要なのは 562 のように動作する関数です この関数には名前があり、570 と呼ばれます。 584 のように実装できます。覚えておいてください。 、必要なのは引数をキャストすることだけです:

template <typename T>
T&& forward(T&& x)
{
 return static_cast<T&&>(x);
}

左辺値を渡す場合、593 左辺値参照、左辺値参照の参照折りたたみ、および 608 に推定されます 関数を以下と同一にする:

template <typename T>
T& forward(T& x)
{
 return static_cast<T&>(x);
}

右辺値の場合、転送参照は推論に関して通常の参照のように動作するため、 612 参照なしの引数の型になり、パラメーターは 627 への通常の右辺値参照になります .

しかし、この実装には欠陥があり、635 で使用できます。 そのように:

other_func(forward(t));

何が問題なのですか。644 と答えました 右辺値の右辺値を返します(したがって、657 を移動します )、および左辺値の左辺値 (したがって、660 をコピーします) ).

問題は前と同じです:関数 673 で は左辺値であるため、常に左辺値も返します!この場合、実際にはテンプレート引数の推論に頼ることはできません。引数を自分で指定する必要があります:

other_func(forward<T>(t));

右辺値 687 について覚えておいてください は右辺値参照であるため、右辺値を処理するために参照の折りたたみを強制します。 も左辺値であるため、左辺値を返します。

そのため、706 必要な方法で実装されている テンプレート引数を明示的に指定すると、推論が禁止されます。

テクニック:テンプレートの引数推定を防ぐ

間違った結果につながるため、テンプレート引数の推論を望まない場合があります。最も顕著な例は 711 です。 .

これは非常に簡単に実現できます。推論されていないコンテキストに置くだけです:

template <class Container>
void func(typename Container::iterator iter);
…
std::vector<int> vec;
func(vec.begin());

この呼び出しでは、コンパイラは 720 の型を推測できません。 736 です このような高度なパターン マッチングを単純に実行することはできません。テンプレート パラメーターがパラメーターとして直接使用されず、代わりにパラメーターの型が何らかのメンバー型であるか、パラメーターでインスタンス化されたテンプレートなどである場合は常に、推定されないコンテキストになります。呼び出し元は型を明示的に渡す必要があります。

これは、テンプレート引数の推定を防ぐために使用できます:

template <typename T>
struct identity
{
 using type = T;
};

template <typename T>
void func(typename identity<T>::type t);

746 の間 常に 752 の型を持ちます 、コンパイラは 766 のその後の特殊化を認識していません と仮定できないため、型を推測できません。

このテクニックは 777 でも使われています .

修正されたガイドライン:できない場合を除き、コンパイラにテンプレート引数を推測させます

これまで見てきたように、テンプレート引数の推論が不可能な場合があります:プログラマーによって防止された可能性があります。または、789 のように、パラメーターでまったく使用されていないテンプレート パラメーターによって回避された可能性があります。 :

template <typename T, typename ... Args>
std::unique_ptr<T> make_unique(Args&&... args);

こちら 797 は戻り値の型でのみ使用されるため、まったく推測できず、明示的に渡す必要があります。したがって、そのような場合:テンプレート引数を手動で指定し、それ以外の場合はコンパイラに任せてください。

このガイドラインは、最初のものほど良くないようです。以前は、803 形式の呼び出しはすべて は違反であり、フラグを立てることができましたが、現在はケースバイケースで実行する必要があります.特定の型について推論を要求する方法がないため、すべての関数は、どのテンプレートパラメータが推定されることを意図しており、どのテンプレートパラメータが意図されているかを文書化する必要があります.これは不要であり、検出されないばかげた間違いにつながる可能性があります。

それでは、every に対してテンプレート引数推定を有効にしてみましょう。 パラメータ。

テクニック:タグ テンプレート

テンプレート引数推定が不可能なさらに別の例を考えてみましょう:

template <std::size_t I, class Tuple>
some-type get(Tuple&& t);

インデックスを 814 に渡す必要があります 明示的なテンプレート引数として、引数から推測することはできません.

必要なのはタグ テンプレートです .タグのように type これは、実際には使用されず、技術的な理由から存在する関数のパラメーターです。この場合、それは型ではなくテンプレートであり、テンプレート引数推定を有効にする必要があります。

必要なのは 825 を作成する方法です 署名の一部です。そのためには、834 へのパラメーターが必要です。 その型は 845 に依存します - 850 、例:

template <std::size_t I, class Tuple>
some-type get(std::integral_constant<std::size_t, I>, Tuple&& tuple);

861 を呼び出す代わりに そのように:

get<0>(tuple);

私たちはそれを次のように呼んでいます:

get(std::integral_constant<std::size_t, 0>{}, tuple);

必要なタグ テンプレートのインスタンス化のオブジェクトを渡します。確かに、これは冗長ですが、簡単にエイリアスを付けることができます。

template <std::size_t I>
using index = std::integral_constant<std::size_t, I>;

template <std::size_t I, class Tuple>
some-type get(index<I>, Tuple&& tuple);
…
get(index<0>{}, tuple);

Boost Hana の UDL のようなものでさらに一歩進めることもできます:

get(0_c, tuple);
// _c is a user-defined literal
// it returns the integral_constant corresponding to the value

タイプについても同じことが機能します。タイプに依存するタグ テンプレートを定義する必要があるだけです:

template <typename T>
struct type {};

次のように使用します:

template <typename T, typename ... Args>
T make(type<T>, Args&&... args);
…
auto obj = make(type<std::string>{}, "hello");

これは、演繹が不要な関数でも使用できます:

template <typename T>
void non_deduced(type<T>, typename identity<T>::type x);
…
non_deduced(type<short>{}, 0);

ID トリックは、実際の引数の推定を無効にするため、パラメーターの型が競合することはありません。

タグ テンプレートは、すべてが推定可能であり、元のガイドラインがすべての場合に有効であることを保証するために、引数推定を駆動する軽量のパラメーターです。

結論

ふぅ、その投稿は長くなりました。

私が言いたいのは次のことだけです:

    <リ>

    コンパイラを助けるのではなく、テンプレートの引数推定を使用してください。 <リ>

    テンプレート引数推定がするまれなケース 失敗して、推論されていないコンテキストに引数を入れて無効にしてください。

    <リ>

    テンプレート引数推定が不可能な場合は、タグ テンプレートを使用して推定を有効にすることを検討してください。

3 番目のポイントは物議をかもしており、間違いなく奇妙に思えますが、コード ベース全体で使用すると一貫性が得られます。明示的にテンプレート引数を渡すと、ガイドラインに違反します。

しかし、私の結論に同意しない場合でも、テンプレートの引数の演繹に関連することを 1 つか 2 つ学んでいただければ幸いです。