最終更新日時(UTC):
が更新

履歴 編集

クラステンプレートのテンプレート引数推論(C++17)

概要

コンストラクタに渡される値によって、クラステンプレートのテンプレート引数を推論する。

// std::vectorクラステンプレートのテンプレート引数を省略。
// 初期化時に代入される初期化子リストの要素型が、std::vectorの要素型となる
std::vector v = {1, 2, 3}; // 変数vの型はstd::vector<int>

単純なケースでは、コンストラクタで受け取る値の型がクラスのテンプレートパラメータである場合に、クラスのテンプレート引数が推論される。

template <class T>
struct Point {
  T x, y;

  // コンストラクタは、クラスのテンプレートパラメータT型のパラメータをとる
  Point(T x_, T y_)
    : x(x_), y(y_) {}
};

int main()
{
  Point p {1.0f, 3.0f}; // 変数pの型はPoint<float>に推論される
  Point q {1.0, 3.0};   // 変数qの型はPoint<double>に推論される
}

より複雑な例として、コンストラクタがクラスのテンプレートパラメータ型の値を直接受け取らない場合は、「推論補助 (deduction guide)」をクラス外に記述する。

#include <iostream>
#include <vector>
#include <list>
#include <iterator>

namespace mine {

// std::vectorをラップした可変長配列クラス
template <class T>
class MyVector {
  std::vector<T> data_;

public:
  // コンストラクタではクラステンプレートパラメータTを直接使用せず、
  // イテレータ範囲で初期化する
  template <class InputIterator>
  MyVector(InputIterator first, InputIterator last)
    : data_(first, last) {}

  void print()
  {
    for (const T& x : data_) {
      std::cout << x << std::endl;
    }
  }
};

// 推論補助宣言。クラスが所属する名前空間の、クラス外に記述する。
// イテレータの要素型をMyVectorのテンプレート引数として使用する。
template <class InputIterator>
MyVector(InputIterator, InputIterator)
  -> MyVector<typename std::iterator_traits<InputIterator>::value_type>;

} // namespace mine

int main()
{
  std::list ls = {1, 2, 3};

  // 変数vの型はmine::MyVector<int>に推論される
  mine::MyVector v {ls.begin(), ls.end()};

  v.print(); // 1, 2, 3が順に出力される
}

コンストラクタのパラメータ型から、直接あるいは間接的にクラステンプレートの引数を求められない場合は、推論できない。

仕様

  • クラステンプレートのコンストラクタが、パラメータにクラスのテンプレートパラメータをとる場合、コンストラクタの引数からクラステンプレートのテンプレート引数を推論できる
  • クラスのテンプレート引数を推論できる場合、そのクラステンプレートを使用するユーザーコードは、テンプレート引数を省略できる:

    template <class T>
    struct AnyValue {
      T x;
    
      // コンストラクタがパラメータとして
      // クラステンプレートのテンプレートパラメータ型の
      // オブジェクトを受け取る。
      // この状況では、コンストラクタの引数からクラスの
      // テンプレート引数を推論できる
      AnyValue(T x_) : x(x_) {}
    };
    
    AnyValue<int> v {3}; // 型推論しない従来の書き方
    AnyValue w {3};      // 型推論してテンプレート引数を省略。wの型はAnyValue<int>
    

  • テンプレート引数を省略したクラステンプレート名は、プレースホルダーとして扱われる。型推論の結果として、テンプレート引数を補完した完全な型名に置き換えられる

  • コンストラクタのパラメータ型からクラステンプレート引数を直接推論できない場合、「推論補助 (deduction guide)」を宣言する。推論補助は、クラス外のクラスと同じスコープ、同じアクセス修飾内に宣言する。構文は、以下のようになる:

    クラステンプレート名(パラメータリスト) -> クラステンプレート名<テンプレート引数>;
    
    // -> のうしろは、推論結果としての完全な型名
    

  • 推論補助宣言は、パラメータにデフォルト引数を持ってはならない

  • 同じ翻訳単位に2つの推論補助がある場合、それらの推論補助は、同じパラメータリストを持ってはならない
  • 推論補助には、先頭にexplicitを任意に付けられる。しかし、noexceptや属性といった修飾は付けられない
  • コンストラクタの引数を、コンストラクタに渡される前の状態 (配列からポインタへの変換などが行われる前の状態) で推論補助に転送したとして推論が行われる。そのため、推論補助はクラスのコンストラクタと同一のシグニチャである必要はない
  • この機能にともなって、デフォルトテンプレート引数のみを持つクラステンプレートは、インスタンス化時に山カッコを省略できる
    template <class=int>
    struct X {};
    
    auto x = X{}; // X<>{}; と書く必要がない
    

標準ライブラリでの使用例

#include <vector>
#include <array>
#include <set>
#include <complex>
#include <functional>
#include <memory>
#include <utility>
#include <future>
#include <mutex>

int main()
{
  // 初期化子リストからコンテナの要素型を推論
  {
    std::vector v = {1, 2, 3}; // std::vector<int>に推論される
    std::array ar = {4, 5, 6}; // std::array<int, 3>に推論される
    std::set s = {7, 8, 9};    // std::set<int>に推論される
  }

  // 複素数の要素型を推論
  {
    std::complex c {1.0, 2.0};    // std::complex<double>に推論される
    std::complex cf {1.0f, 2.0f}; // std::complex<float>に推論される
  }

  // 関数ポインタ・関数オブジェクトからstd::functionのシグニチャを推論
  {
    // std::function<int(int, double)>に推論される
    std::function f = [](int, double) { return 0; };
  }

  // スマートポインタの型推論
  {
    // std::shared_ptrstd::unique_ptrは生ポインタからの推論を許可しない。
    // std::shared_ptrからstd::weak_ptrとその逆は推論できる
    std::shared_ptr<int> sp {new int(3)};
    std::weak_ptr wp = sp;
    std::shared_ptr locked_sp = wp.lock();
  }

  // std::pairstd::tupleの型推論
  {
    // std::make_pair()やstd::make_tuple()のような単純な生成関数が不要となる例
    std::pair p {1, "Hello"};        // std::pair<int, const char*>に推論される
    std::tuple t {1, 3.14, "Hello"}; // std::tuple<int, double, const char*>に推論される
  }

  // std::lock_guardが管理するミューテックスの型を推論
  {
    std::mutex mut;
    std::lock_guard lk {mut}; // std::lock_guard<std::mutex>に推論される
  }

  // promiseから取得するfutureで、結果型を推論
  {
    // std::future<int>に推論される
    std::promise<int> pro;
    std::future fut = pro.get_future();
  }
}

出力

クラステンプレートの型推論を回避する例

// C++20 で追加された std::type_identity と同じことをするクラス
template <class T>
struct identity {
  using type = T;
};

template <class T>
struct X {
  using T_ = typename identity<T>::type;

  // テンプレートパラメータを直接使用せず、identityを介して使用する。
  // これによって、テンプレート引数を明示的に指定させられる
  X(T_) {}
};

int main()
{
//X x{1};      // コンパイルエラー!テンプレート引数を推論できない
  X<int> y{1}; // OK
}

出力

デフォルトテンプレート引数のみを持つクラステンプレートの使用例

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

int main()
{
  std::vector v = {3, 1, 4};

  // デフォルトテンプレート引数が使用され、std::greater<void>{}となる。
  // std::greater<>{}のように山カッコを書く必要がない
  std::sort(v.begin(), v.end(), std::greater{});

  for (auto x : v) {
    std::cout << x << std::endl;
  }
}

出力

4
3
1

この機能が必要になった背景・経緯

この機能は、以下のような問題を解決するために導入された:

  • make_*()のような生成関数は、冗長で、かつ標準ライブラリの非クラステンプレートの構築方法と一貫性がなかった
  • 生成関数の設計が標準ライブラリ内で一貫しておらず、std::make_pair()make_tuple()といったmake_接頭辞が付く場合もあれば、std::back_insert_iteratorに対するstd::back_inserter()関数のように、make_接頭辞が付かない場合があった。そのために、ユーザーは生成関数の細かな違いをドキュメントで調べながら使用する必要があった
  • 関数テンプレートでは引数の型からテンプレートパラメータの型を推論できるにも関わらず、クラステンプレートのコンストラクタではpair<int, double>(2, 4.5)<int, double>のように冗長な指定をする必要があった
  • 生成関数は、単に引数の型を推論するだけではない場合があった。例として、std::make_pair()make_tuple()といった関数は、型がstd::reference_wrapper<T>であった場合に、それをT&に展開する機能がある。単に引数の型を推論するだけの生成関数なのか、より複雑なことをする生成関数なのかをドキュメントで調査する必要があり、そうしない場合に予期せぬバグが発生することがあった
  • 生成関数を持たない場合、たとえばラムダ式を使用する際に、その型を記述できない問題があった
  • std::lock_guardのようにコピーもムーブもできない型は、生成関数を作るために「コピー省略」のような難解な機能を使用する必要があった
  • 循環的な複雑さ (Cyclomatic complexity) を軽減するために大きな関数をクラスで置き換える便利な手法が、関数テンプレートでは使用できなかった

関連項目

参照