最終更新日時:
が更新

履歴 編集

可変引数テンプレート(C++11)

概要

「可変引数テンプレート (variadic templates)」は、任意の型とそのオブジェクトを任意の数だけ受け取る機能である。これによって、「最大でN個のパラメータを受け取る関数テンプレートやクラステンプレート」を実装する際に、N個のオーバーロードをユーザーが用意する必要なく、ひとつの実装だけで済むようになる。

可変引数テンプレートとしてテンプレートパラメータを受け取る場合、テンプレートパラメータを宣言するclassまたはtypenameとテンプレートパラメータの間に、省略記号 ... を入力する:

// 0個以上のテンプレートパラメータを受け取る
template <class... Args>
struct X;

// 0個以上の任意の型のパラメータを受け取る
template <class... Args>
void f(Args... args);

可変引数テンプレートで宣言したパラメータ(ここではArgsargs)は「パラメータパック (parameter pack)」と呼ばれ、複数の型またはオブジェクトがまとめられた状態となっている。

パラメータパックになっているパラメータは、「展開 (expansion)」して使用する必要がある。これは他の関数やクラステンプレートにパラメータパックを転送するような状況で必要となる。パラメータパックの展開には、パラメータパックの後ろに省略記号 ... を入力する:

template <class... Args>
struct X {
  // パラメータパックを ... で展開して、
  // std::tupleクラステンプレートの引数として渡す
  std::tuple<Args...> values;
};

void g(int, char, const std::string&) {}

template <class... Args>
void f(Args... args)
{
  // パラメータパックを ... で展開して、
  // 関数g()の引数として渡す
  g(args...);
}

f(3, 'a', "hello");

パラメータパックを最初の要素から順番に処理していきたい場合には、「任意の型のパラメータをひとつと、任意の個数の任意の型のパラメータを受け取る」というような形式のパラメータリストとし、再帰によって処理をする:

#include <iostream>
#include <utility>

// パラメータパックが空になったら終了
void print() {}

// ひとつ以上のパラメータを受け取るようにし、
// 可変引数を先頭とそれ以外に分割する
template <class Head, class... Tail>
void print(Head&& head, Tail&&... tail)
{
  std::cout << head << std::endl;

  // パラメータパックtailをさらにheadとtailに分割する
  print(std::move(tail)...);
}

int main()
{
  print(1, 'a', "hello");
}

出力:

1
a
hello

仕様

  • template <class... Args>void f(Args... args)での、Argsおよびargsは、複数のパラメータをまとめた状態となっている。このまとまった状態のパラメータを「パラメータパック (parameter pack)」という
    • テンプレートパラメータのパラメータパックを「テンプレートパラメータパック (template parameter pack)」、関数のパラメータとしてのパラメータパックを「関数パラメータパック (function parameter pack)」という
  • パラメータパックは複数のパラメータがまとめられた状態となっているため、そのままでは個々のパラメータを取り出せない。それらを取り出すには、パラメータパックを「展開 (expansion)」する必要がある。パラメータパックの展開には、パラメータパック名に続いて「...」を記述する。例:

    template <class... Args>
    void f(Args... args)
    {
      g(args...); // パラメータパックargsを...で展開して、
                  // 関数g()にそれらのパラメータを転送する
    }
    

  • パラメータパックの宣言、および展開に使用する「...」は、「省略記号 (ellipsis, エリプシス)」という

  • パラメータパックには、ゼロ個以上のパラメータが含まれる

    template <class... Args>
    struct X {};
    
    int main()
    {
      X<int, char, double> a; // OK
      X<> b;                  // OK
    }
    

    template <class... Args>
    struct X {};
    
    // パラメータパックが0要素だった場合の特殊化
    template <>
    struct X<> {};
    

    template <class... Args>
    void f(Args... args) {}
    
    int main()
    {
      f(1, 3.14f, "hello"); // OK
      f();                  // OK
    }
    

  • パラメータパックの宣言をする際、パラメータパックはパラメータリストの最後になければならない

    // OK
    template <class Head, class... Tail>
    struct X {};
    
    // コンパイルエラー!パラメータパックは最後に置かなければならない
    template <class... Init, class Last>
    struct Y {};
    

  • 関数パラメータパックは、型推論の補助として、パラメータパックの全ての型に対して共通の修飾を付加できる:

    // パラメータパックArgsに含まれる全ての型のパラメータを、
    // const左辺値参照として受け取る
    template <class... Args>
    void f(const Args&... args) {}
    

  • sizeof...(identifier)演算子にパラメータパックを指定することで、パラメータパックに含まれるパラメータの要素数を取得できる:

    #include <cstddef>
    
    template <class... Args>
    void f(Args... args)
    {
      constexpr std::size_t template_parameter_pack_size = sizeof...(Args);
      constexpr std::size_t function_parameter_pack_size = sizeof...(args);
    
      static_assert(template_parameter_pack_size == 3, "");
      static_assert(function_parameter_pack_size == 3, "");
    }
    
    int main()
    {
      f(1, 'a', "hello");
    }
    

パラメータパックの宣言ができる場所

パラメータパックの宣言は、以下の場所でできる:

  • 関数のパラメータ

    template <class... Args>
    void f(Args... args); // ここ
    
    f(1, 'a', "hello");
    

    #include <tuple>
    
    template <class... ResultTypes>
    void f(ResultTypes(*...funcs)(int, char))
    {
      // t is std::tuple<int, float>{1, 1.23f}
      auto t = std::make_tuple(funcs(3, 'a')...);
    }
    
    int a(int, char) { return 1; }
    float b(int, char) { return 1.23f; }
    
    int main()
    {
      f(a, b);
    }
    

  • テンプレートパラメータ

    template <class... Args>
    struct X {};
    
    X<int, char, double> x;
    

    template <int... Args>
    struct Y {};
    
    Y<3, 1, 4, 5, 2, 6> y;
    

  • テンプレートテンプレートパラメータ

    template <template <class...> class Container>
    struct ContainerHolder {
      Container<int> cont;
    };
    
    ContainerHolder<std::vector> v;
    ContainerHolder<std::list> ls;
    

パラメータパックの展開ができる場所

パラメータパックの展開は、以下の場所でできる:

  • 関数の引数

    f(args...);
    

  • テンプレートの引数

    std::tuple<Args...> t;
    

  • 初期化子

    int ar[] = { args... };
    
    struct Person {
      int id;
      std::string name;
      int age;
    };
    Person person = { args... };
    

  • 継承時の基本クラスリストの指定

    template <class... Bases>
    class Derived : Bases...;
    

  • コンストラクタのメンバ初期化子

    template <class... Bases>
    class Derived : Bases... {
      Derived(Bases... bases)
        : Bases(bases)... {}
    };
    

  • 動的例外仕様

    template <class... ExceptionList>
    void f() throw(ExceptionList...);
    

パラメータパックの拡張

  • パラメータパックは、f(args...)のように省略記号で展開するほかに、f(g(args)...)のようにパラメータパックの各要素に共通の処理を適用する式を書くこともできる。これをパラメータパックの「拡張 (extend)」という

    • この例の場合、argsパラメータパックの各要素に関数g()を適用してパラメータパックの要素に対して値と型の変換を行った結果のパラメータパックを生成し、その結果となるパラメータパックを関数f()に渡している

    #include <iostream>
    #include <string>
    #include <sstream>
    
    template <class T>
    std::string to_std_string(const T& x)
    {
      std::stringstream ss;
      ss << x;
    
      std::string result;
      ss >> result;
      return result;
    }
    
    void print(std::string a, std::string b, std::string c)
    {
      std::cout << a << std::endl;
      std::cout << b << std::endl;
      std::cout << c << std::endl;
    }
    
    template <class... Args>
    void f(Args&&... args)
    {
      // パラメータパックの各要素を文字列に変換してから
      // print()関数に渡す
      print(to_std_string(args)...);
    }
    
    int main()
    {
      f(1, 'a', "hello");
    }
    

  • 複数のパラメータパックに対して拡張を行う場合、それらのパラメータパックは同じ要素数でなければならない。そうでない場合、プログラムは不適格となる

    #include <utility>
    #include <tuple>
    #include <type_traits>
    
    // 2つの型リストを綴じ合わせる
    template <class... Args1>
    struct zip {
      template <class... Args2>
      struct with {
        using type = std::tuple<std::pair<Args1, Args2>...>;
      };
    };
    
    int main()
    {
      static_assert(std::is_same<
        zip<int, long, long long>::with<float, double, long double>::type,
        std::tuple<
          std::pair<int, float>,
          std::pair<long, double>,
          std::pair<long long, long double>
        >
      >::value, ""); // OK
    }
    

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

可変引数テンプレートがなかったころ、任意の数のパラメータを受け取ってほかの処理に転送するといった処理をするために、パラメータの数だけオーバーロードを用意する必要があり、それによってコピー&ペーストのコードができあがっていた。とくに関数テンプレートの場合、const左辺値参照と非const左辺値参照の全ての組み合わせをオーバーロードとして定義する必要があったため、このコードは非常に膨大になり、コンパイル時間が増えるひとつの原因ともなっていた。

template <class T1, class T2, class T3>
void f(T1& t1, T2& t2, T3& t3);

template <class T1, class T2, class T3>
void f(const T1& t1, T2& t2, T3& t3);

template <class T1, class T2, class T3>
void f(const T1& t1, const T2& t2, T3& t3);

template <class T1, class T2, class T3>
void f(const T1& t1, const T2& t2, const T3& t3);


この問題を解決するために、任意の数だけ任意の型のパラメータを受け取る機能が必要とされ、可変引数テンプレートが導入された。

参照