こんにちは。田原です。
今回まで可変長引数テンプレートのパラメータ・パックの加工方法の解説を続けます。「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&&
は右辺値参照固定になってしまうのです。
これをユニバーサル参照にするためには、コンストラクタを実体化する際に型推論を機能させる必要があります。そのためにコンストラクタをテンプレートとして定義しています。
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()関数がテンプレートになっていることです。
TypeHelperprintLogImpl<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参照渡しとなりました。
3.まとめ
ちょっとひっぱり過ぎた感もありまが、今回でパラメータ・パックの展開は終了します。更に詳しい話は、前回リンクしたいなむのみたまさんのQiitaの記事を参考にされて下さい。
さて、そろそろネタも尽きつつあり、次回は何をするかまだ決めていません。以降は私が有用性を理解できている機能を順不同で紹介して行きたいと思います。(有用だけど有用性を理解できていないものも、結構あります。それらは把握できたタイミングで紹介させて頂きますね。)
では、また来週!
コンストラクタは出来る限りexplicit指定した方がいいですよ
出来る限りconstexpr指定もnoexpcept指定もした方がいいですよ。
いなむ先生、コメントありがとうございます!
引数が1つしかないものはexplicitした方がよいと Effective C++で見て意味を把握はしているのですが、必要性をあまり感じたことがないので理解が浅いです。constexprも同様です。コンパイル時処理の追求が甘いですね。もう少し頑張らねば。
noexceptは指定ミスを静的チェックしてくれないからリスキーなので必要時のみ使う方針です。(バグるとログが残らなくなるって致命的。バグの撲滅は不可能ですから、ログを取っていないプログラムの信頼性は悲惨と考えています。)https://wandbox.org/permlink/hWLr7Z1MJiHdkx4V
今のところ、これらについて当ブログで扱う予定がないのですよ。explicitとconstexprは解説出来るほど理解が追いついていないと言うのが辛いところです。noexceptは有用な場面が限定的と考えているので取り扱わないだろうと思います。std::vectorに食わせるリソース管理クラスの話をした方が良いと感じたら解説するかもです。(ムーブ時の例外拒否機能としては有用と考えています。でも、std::unique_ptrを使っておけばまず問題ないですし。)
速度を犠牲にしてまで安全性を担保したいなら、もはやC++を書くべきではないでしょう。[要出典]
>noexceptは有用な場面が限定的
ありえないです。
速度がC++のほとんど唯一の強みです。
例外テーブルを消し去り、プログラムの速度を向上させることができるnoexceptは常に有用です。
QoIのためには当然書くべきだと考えています。
関数内で呼ばれる関数がnoexcept指定されているかどうかでnoexceptを切り替える事ができます。
noexcept(式)と書けば式が無例外保証付きで呼べるかがbool値で返ります。
noexcept(true)、noexcept(false)がそれぞれ無例外保証・無例外保証なしとなるので、
noexcept(noexcept( 式 )) と二重のnoexceptを書きます。
https://wandbox.org/permlink/Uso1qLZH0PPvIzna
constexprはコンパル時処理が可能であれば、
ユーザーがコンパイル時処理を選択するという選択の余地を与えます。
コンパイル時処理不可能でないなら常にconstpexrをつけるべきであると考えます。
また、今回の例はtupleであったので流石にconstexprは必要かなと思いました。
tupleでコンパイル時に値を処理することはtupleに与えられた重要な使命なので(ほんまか?
あちゃ~、すいません。見落としてました。
> 速度がC++のほとんど唯一の強みです。
> 速度を犠牲にしてまで安全性を担保したいなら、もはやC++を書くべきではないでしょう。[要出典]
以前、gccとmsvcで測ったことがあるのですが、printf()とstd::coutではprintf()の方が高速でした。 いなむ先生はstd::cout否定派でしょうか? しかし、std::cout肯定派も少なくないと思います。(私もこちらです。)
そもそも、もし、速度だけを追求するならアセンブラが最強です。メタ・プログラミングは生産性を無視するなら、手で事前に処理してハードコーディングすれば最強です。つまりC++は速度だけでなく生産性も追求する言語なのです。
C++はクラスやテンプレート、メタ・プログラミング等により生産性と速度を高いレベルでバランスする非常に優れた言語と考えています。(ただし学習コストがたいへん大きいのが巨大な難点)
さて、noexcept(noexcept(bar()))の方法で呼び出している関数全てを指定するのは現実的ではないと思います。関数テンプレートの指定は憂鬱ですね。オーバーロード関数を全て指定するのも手間ですね。更に呼び出す関数が追加される度に指定を追加しないといけません。呼び出す関数のシグニチャが変わった時も同様です。そして、指定漏れし、かつ、その指定漏れした関数が例外を投げるとそれはログに残らないのです。noexceptを書いてなければログに残りバグ解析できるのにです。ですので指定は慎重に慎重に行う必要があります。
そこまでの注意力を消費し、工数をかけることを正当化出来るほどの効果が期待できるのでしょうか?
呼び出している関数のどれか1つでもnoexceptが指定されていなければ、これらの苦労は水の泡です。
更に、例外を投げない限り大きく速度が低下するわけではありませんので、例外を一切投げないからと言って大きな速度アップが期待できる筈もないです。
従って、生産性と速度のバランス的にnoexceptを使うのが妥当な場面は限定的と考えます。
(とはいえ、標準ライブラリが積極的に使うことには大賛成です。非常に使われる機会が多いものですし、仕様変更は頻繁にはありませんから、上記工数は余裕で正当化できると思います。)
> 今回の例はtupleであったので流石にconstexprは必要かなと思いました。
この辺はご勘弁。多くのC++コンパイラは優秀なのでconstexprが無くても可能な場合はコンパイル時に処理されるケースが多いと思います。constexprを付けることでそれ以上の効果が期待できる状況もあるだろうと予想していますが、その状況を把握できていないのですよ。
また、今回はtupleそのものの解説というわけではなくて、パラメータ・パックの展開方法の解説が目的ですのでconstexprに触れる必要はないかなと考えています。
>常に有用
すいません嘘でした。
限られた場合に有用で特にムーブやswapでの強い例外保証では必須。
それ以外では例外処理を書いたほうが圧倒的コスト安。
>std::coutとprintf
iostreamは擁護できないほどクソなのでprintf肯定派です。
(iostream否定派ではない)。
>速度だけを追求するならアセンブラが最強です
嬉しいことにそろそろ人間がコンパイラにまさるアセンブラを書ける時代は終わりそうです。
>onstexprが無くても可能な場合はコンパイル時に処理されるケースが多い
精々が整数くらいでは?
constexpr関数なら実数も扱えます。
実際、constexpr関数でコンパイル時にハッシュテーブルを生成することで
実行時の処理を大幅に減らしたというプログラマもいましたし、有用。
> iostreamは擁護できないほどクソなので
確かに問題点多いですよね。(私は特にlocaleが嫌いです。書式指定が面倒なのも嫌いです。<<も好きではありません。)私的には、scanf()/printf()シリーズも別の意味でクソ(不正メモリアクセス回避が無茶苦茶たいへん)なので、結果iostreamに軍配が上がっている状況です。
> 精々が整数くらいでは?
> constexpr関数なら実数も扱えます。
なるほど!! その辺の情報収集が追いついてないのです。
> constexpr関数でコンパイル時にハッシュテーブルを生成する
私が自力でやる時は、C++で普通の構文でハッシュテーブルを生成するプログラムを書いて、CMakeでプリ処理させそうです。でも、constexpr関数で素直な記述で作れるなら、その方が生産性が上がるので絶対好ましいです。
問題は私にとってそんな処理が必要になったのは大昔(15年近く前)なので、学習する機会が訪れていないのです。機会が来たらトライしてみます。ありがとうです。