こんにちは。田原です。

今回まで可変長引数テンプレートのパラメータ・パックの加工方法の解説を続けます。「std::forwardを使うケースが多いのでそれも書いた方が良いのでは?」との意見を頂きましたのでこれと、テプレート・パラメータ・パックの複雑な加工をする方法例を紹介します。今回はあまり難しいことはやっていませんので復習がてら気楽に御覧ください。

1.std::forwardを使うパラメータ・パックの加工

前回、const&を追加して、パラメータ受け取りの際に無駄なコピーをしないようにしました。
今回は前々回の簡易tupleのようにコンストラクタで受け取ってメンバ変数に設定する場合を考えてみます。

簡易tupleもそうですが多くの場合、このようなケースのメンバ変数は普通の変数として、初期化後も変更できるようにしたい場合がほとんどです。このような場合、コンストラクタでconst参照で各要素の初期値を受け取って各対応するメンバへコピーします。
しかし、コピーではなくてムーブしたい時もありますね。以前解説したように const参照で受け取ると、実引数を修正できないためムーブできません。そこで、T&&で受け取るとムーブでき、ムーブしたくない時は参照で受け取れるようになります。

因みに、このような使い方のT&&をユニバーサル参照と呼ぶそうです。型推論時に左辺値参照と右辺値参照のどちらにでも対応できるという意味です。

さて、テンプレート・パラメータ・パックに && を付加するものも(私としては意外だったのですが)定番のようです。言われてみればSTLで良く見かけたような気がします。std::vector<T>などのコンテナ群のemplaceシリーズや、std::threadのコンストラクタなど。

そこで、前々回の簡易tupleを修正し、ムーブ・コンストラクトに対応してみました。部分特殊化のコンストラクタのみの修正となります。

// 簡易tupleの部分特殊化
template<typename tFirst, typename... tRest>
class tuple<tFirst, tRest...> : public tuple<tRest...>
{
    template<std::size_t N, typename... tTypes> friend struct get_helper;
    tFirst  mMember;
public:
#if 0
    tuple(tFirst const& iFirst, tRest const&... iRest) :
        tuple<tRest...>(iRest...),
        mMember(iFirst)
    { }
#else
    template<typename tFirst2, typename... tRest2>
    tuple(tFirst2&& iFirst, tRest2&&... iRest) :
        tuple<tRest...>(std::forward<tRest2>(iRest)...),
        mMember(std::forward<tFirst2>(iFirst))
    { }
#endif
};

#if 0 側が前々回のコンストラクタです。それに対して今回はメンバ関数テンプレートのコンストラクタに変わってますね。テンプレートにしないで、普通にtuple(tFirst&& iFirst, tRest&&... iRest)で良いような気がしますが、実はダメなのです。
テンプレートでない場合、このコンストラクタにとってtFirstはクラス・テンプレートのtupleを実体化する際に既に明示的に指定されています。つまり、型推論が機能する余地がありません。従って、tFirst&&は右辺値参照固定になってしまうのです。
これをユニバーサル参照にするためには、コンストラクタを実体化する際に型推論を機能させる必要があります。そのためにコンストラクタをテンプレートとして定義しています。

Wandboxで確認する。

main()関数の最後でムーブしています。

    std::cout << "\n--- my tuple(move construction)---\n";
    std::string aString("hello");
    tuple<int, short, std::string> aTuple2(123, 456, std::move(aString));
    std::cout << "size=" << sizeof(aTuple) << " aString=" << aString << "\n";
    std::cout << get<0>(aTuple2) << "\n";
    std::cout << get<1>(aTuple2) << "\n";
    std::cout << get<2>(aTuple2) << "\n";
    get<2>(aTuple2) = "world!!";
    std::cout << get<2>(aTuple2) << "\n";

最後のstd::string型の要素についてムーブしています。
aStringを”hello”で初期化しています。これをaTuple2へstd::moveにて与えたため右辺値参照で渡され、tuple内でstd::string型である最後のmMemberに渡す際に右辺値参照で渡されます。std::stringはムーブ・コンストラクタを持っているため、中身がムーブされるのでaStringの中身が空になりました。

tupleの部分特殊化の #if 0 を #if 1 へ変更してみて下さい。const&で受け取るとstd::moveで渡してもコピーになりますからaStringの中身が残ります。

このユニバーサル参照のコンストラクタがあれば十分な気がするのですが、本物のstd::tupleはconst参照とユニバーサル参照の両方のコンストラクタを持っています。ムーブできない要素に右辺値を渡したら、その要素はムーブできないのでコピーされます。その時、全ての要素をムーブせずにコピーするために(const参照コンストラクタが呼ばれる)、両方用意しているようです。
何か決定的な意味がありそうですが、良く分かりません。もし、心当たりの方がいらっしゃいましたら、是非教えて頂けると幸いです。

2.少し複雑なテンプレート・パラメータ・パックの加工

2-1.その前に加工内容の説明

int型等の基本型の多くはCPUのレジエスタ1つに収まるものが多いです。そのようなものを関数のパラメータで渡す場合、参照渡しより値渡しした方が一般には効率が良いです。

  • 参照渡しの場合
    呼び出された関数が、呼び出した側の実引数の配置されているアドレスを把握する必要があるので、実引数のアドレスを渡して、そのアドレス経由で間接的に引数をアクセスします。

  • 値渡しの場合
    関数呼び出し時に、その関数が管理する領域へ実引数の値がコピーされます。コンパイラはそのコピーした領域を直接アクセスするコードを生成できます。

そこで、型に応じてconst参照と型そのものを切り替えるヘルパーを作ってみました。
PCではレジスタ1つに入る場合が多いスカラー型を値渡し、それ以外をconst参照渡しとしました。

スカラー型(scalar):型の分類については、この表が判りやすいです。

#include <iostream>
#include <string>
#include "typename.h"

// 型修飾子表示用のヘルパ
template<typename T>
struct Type { };

// std::string型の型名が長くて判りにくいので短縮用
struct String : public std::string
{
    using std::string::string;
};

// 型に応じてtypeの定義を切り替えるヘルパ
template<typename T, class tEnable=void>
struct TypeHelperImpl
{
    typedef T const& type;
};

template<typename T>
struct TypeHelperImpl<T, typename std::enable_if<std::is_scalar<T>::value>::type>
{
    typedef T type;
};

template<typename T>
using TypeHelper = typename TypeHelperImpl<T>::type;

型推論で使えると良いのですが、複雑な型は型推論できないため、下記のような使い方は意味がありません。
明示的に型を指定すれば使えますが、当然型推論されませんので意味が無いのです。)

template<typename T>
void Foo(TypeHelper<T> iParam)
{
    std::cout << "Foo(" << TYPENAME(Type<decltype(iParam)>) << ") :  " << iParam << "\n";
}

そこで、明示的に型指定して使います。

// ダミー関数
template<typename T>
void Foo(T iParam)
{
    std::cout << "Foo(" << TYPENAME(Type<decltype(iParam)>) << ") :  " << iParam << "\n";
}

// メイン
int main()
{
    int x=123;
    Foo(x);
    x=456;
    Foo<TypeHelper<int>>(x);

    String y="Hello";
    Foo(y);
    y="world!!";
    Foo<TypeHelper<String>>(y);
}
Foo(Type<int>) :  123
Foo(Type<int>) :  456
Foo(Type<String>) :  Hello
Foo(Type<String const&>) :  world!!

ところで、ここまで書いておいて何ですが、このTypeHelperはそれ程有用なものではありません。このような処理は原則として最適化に任せておいた方が良いと思います。少し複雑なテンプレート・パラメータ・パック加工のサンプル用と考えて下さい。

String型について
上記サンプルでは、Stringというクラス(struct)をstd::stringを単純に継承して作っています。コンストラクタも継承コンストラクタを使ってますので、単純に同じものです。std::stringの型名がとても長くて判りにくいので用意しました。Stringはただそれだけのものです。
例えば、gcc 5.4.0のC++11におけるstd::stringの型名は次の通りです。
  std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >

2-2.では本番です

と言っても簡単です。上記のように明示的に使うのは面倒ですから、一度関数テンプレートで中継して、その中で明示的な指定を行います。その明示的指定する時に、テンプレート・パラメータ・パックを加工します。
const&や&&の付加は型推論と併用できました。しかし、型推論できない複雑な場合は、このように明示的に指定して加工することができます。

前回のテンプレート・パラメータ・パックの加工で使ったprintLogで使ってみます。

// 適用先
template<typename... T>
void printLogImpl()
{
    std::cout << "\n";
}
 
template<typename tFirst, typename... tRest>
void printLogImpl(tFirst first, tRest... rest)
{
    std::cout << first << "[" << TYPENAME(Type<decltype(first)>) << "]";
    if (sizeof...(rest)) std::cout << ", ";
    printLogImpl<tRest...>(rest...);
}
 
template<typename... T>
void printLog(T... t)
{
    printLogImpl<TypeHelper<T>...>(t...);
}

このように一度中継することで、プログラマがいちいち明示的に指定しなくても済みます。

後、ポイントとしては、パラメータのないprintLogImpl()関数がテンプレートになっていることです。
TypeHelperで求めた型をprintLogImpl<tRest...>(rest...);にて中継しているのですが、ここで明示的に型を指定しているため、最後にtRestが空になった時も空のテンプレート・パラメータ・パックを受け取るprintLogImpl()が必要なのです。そこでそれが可能な形式で定義しています。

呼び出してみます。クラスも渡してみたいので上で定義したString型を使いました。

int main()
{
    printLog(1, 1.2, String("three"));
}
1[Type<int>], 1.2[Type<double>], three[Type<String const&>]

スカラー型は値渡しとなり、クラスはconst参照渡しとなりました。

Wandboxで確認する。

3.まとめ

ちょっとひっぱり過ぎた感もありまが、今回でパラメータ・パックの展開は終了します。更に詳しい話は、前回リンクしたいなむのみたまさんのQiitaの記事を参考にされて下さい。

さて、そろそろネタも尽きつつあり、次回は何をするかまだ決めていません。以降は私が有用性を理解できている機能を順不同で紹介して行きたいと思います。(有用だけど有用性を理解できていないものも、結構あります。それらは把握できたタイミングで紹介させて頂きますね。)
では、また来週!