デフォルトのテンプレート引数を使用したトリック

通常の関数パラメーターと同様に、テンプレート パラメーターにも既定のパラメーターを指定できます。クラス テンプレートの場合、これはほとんど既定の関数引数と同じように動作します。必要な数よりも少ないテンプレート引数を渡すと、残りの場所を埋めるために既定のテンプレート引数が使用されます。ただし、関数テンプレートの場合、関数のテンプレート パラメーターは通常の関数引数から推定できるため、より複雑になります。これにより、いくつかの興味深い副作用が生じます。特に、テンプレート パラメーターのデフォルト引数を最後に配置する必要はありません!

デフォルトのテンプレート引数でできることをいくつか見てみましょう。

トリック 1:デフォルトのテンプレート引数は他のパラメーターに依存できます

C スタイルの API を使用していくつかの文字列を取る関数を書きたいとします。単一の 03 null で終わる文字列と 15 の場合 プラス 28

void foo(const char* ptr, std::size_t size)
{
    …
}

void foo(const char* str)
{
    foo(str, std::strlen(str));
}

2 番目のオーバーロードの必要性に不満がある場合は、デフォルトの関数引数を試してください:

void foo(const char* ptr, std::size_t size = std::strlen(ptr))
{
    …
}

誰かがポインターとサイズを指定して関数を呼び出すと、そのサイズが使用されます。それ以外の場合は、文字列の長さが使用されます。コンパイルされません。デフォルトの関数引数の値は、他のパラメーターに依存することはできません。

このような (ばかげた?) 制限は、デフォルトのテンプレート引数には適用されません!そして、皆さんはこの機能に大いに依存してきました:

// Default Allocator depends on T.
template <typename T, typename Allocator = std::allocator<T>>
class vector;

// Default Traits and Allocator depend on T.
template <typename T, typename Traits = std::char_traits<T>, typename Allocator = std::allocator<T>>
class basic_string;

// etc.

私が最近経験した特定の使用例の 1 つは、イテレーター/センチネルのペアを取るクラス テンプレートです。ほとんどの場合、イテレーターとセンチネルの型は同じであるため、センチネル引数をデフォルトに設定します:

template <typename Iterator, typename Sentinel = Iterator>
struct my_range
{
    Iterator begin;
    Sentinel end;
};

トリック 2:ヘルプ タイプ推定

C++ 標準ライブラリには、35 という便利な小さな関数があります。 、オブジェクトに新しい値を割り当て、古い値を返します。

template <typename T, typename U>
T exchange(T& obj, U&& new_value)
{
  T old_value = std::move(obj);
  obj = std::forward<U>(new_value);
  return old_value;
}

この関数では、いくつかの優れたパターンを使用できます。たとえば、文字列をコンテナーから移動して、空の文字列に置き換えることができます。

std::vector<std::string> strings;
…
auto str = std::exchange(strings[i], "");

これは、オブジェクトをデフォルトで構築されたオブジェクトと交換する、より一般的なイディオムの一部と見なすことができます。Rust に精通している場合は、48 という関数によって行われます。 .C++ では、55 を使用して簡潔に記述できます。 :

auto value = std::exchange(obj, {});

66 71 と交換するデフォルトの構築済みオブジェクトを提供します .ただし、コードは実際には 89 の定義でコンパイルされません 私は上で与えました。これは 98 のためです 107 という 2 つのテンプレート パラメータがあります。 と 111 、どちらも対応する関数引数の型から推定されます。ただし、波括弧で囲まれた初期化子には型がないため、コンパイラは 120 の型を推定できません。 .

これを機能させるには、コンパイラに 134 と伝える必要があります。 149 と同じ型でなければなりません 153 の型を推測できない場合 .これは、ご想像のとおり、デフォルトのテンプレート引数で行われます:

template <typename T, typename U = T>
T exchange(T& obj, U&& new_value);

コンパイラは最初に 165 の型を推測しようとします 2 番目の引数を使用します。ブレース初期化子が原因で失敗した場合、コンパイラはデフォルトの型を使用して 174 を返します 186 への右辺値参照に .

テンプレート パラメーターを何らかの型にデフォルト設定することにより、ブレース付きイニシャライザーをサポートする必要がある関数がある場合は常に、デフォルトのテンプレート引数を使用します。標準ライブラリは 197 でそれを行います。 、また 209 でそれを行う必要があります または 214 .

// The optional value or a default constructed one.
auto value = opt.value_or({});
// Fill with default value.
std::fill(begin, end, {});

トリック 3:関数テンプレートの 2 つのパラメーター セット

関数テンプレートがある場合、一部のテンプレート パラメーターは関数の引数によって推定されることを意図しており、一部は呼び出し元によって明示的に指定されることを意図しています。例は 224 です。 :

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

タイプ 236 248 は呼び出し元が渡す必要があります。 関数の引数から推定されます。コンパイラに 257 を推定するように要求することはできません 関数の引数として表示されないため、262 の型を明示的に指定するべきではありません (最終的には間違えるでしょう)。

テンプレート パラメータを 2 つに分けるのが好きです:

// Pseudo-code.

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

template <> // no explicit
template <typename T, typename U = T> // deduced
T exchange(T& obj, U&& value);

template <typename T> // explicit
template <> // no deduced
… forward(T…); // (signature complicated)

そのように見ると、コンパイラが非末尾のデフォルト テンプレート パラメータを許可する理由がすぐに明らかになります。それらは、明示的なテンプレート パラメータ セットの最後にあるだけです。したがって、275 デフォルトは 289 です (例は難しい):

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

// or in pseudo-C++:
template <typename T = int> // explicit
template <typename ... Args> // deduced
std::unique_ptr<T> make_unique(Args&&... args);

293 を呼び出す 306 を推測します 315 を設定します 323 へ 、一方 339 341 を推測します 351 を設定します 361 へ (デフォルト)。もちろん、明示的な 372 なしで、いつでも別のオーバーロードを使用することもできます。 パラメーターですが、頭の中でオーバーロードの解決を行うのは、関数を 1 つだけ持つよりも難しいことがわかりました。