はじめに
数値計算を行う際、入力に依存しない計算や操作は全てコンパイル時に済ませてしまいたい。特にログ出力における文字列の結合とか。
ということで「コンパイル時文字列結合」について検索してみるとSprout というconstexpr に関するライブラリが存在することを知る。その中に文字列を扱うクラスsprout::string なるものもあるではないか。このsprout::string クラスを利用すればコンパイル時文字列結合ができるので目的は達成される。これを使えば良い。
せっかくなのでもう少し検索してみるとStack Overflowでstd::array を使ったコンパイル時文字列結合のコードが簡潔に書かれているのを見つけた(そのページへのリンク)。それを読んだところ勉強になったので、まとめノートを作成することにした。
使用したもの
- macOS Big Sur (ver. 11.2.2)
- clang++ (version 12.0.0)
- コンパイルオプション -std=c++11
実行してみる
元々のソースコード
Stack Overflowに記載されていたソースコードを、この記事のために少々手を加えて書いておく。変更点は以下。
-
char constをconst charにした。後者の方が書き慣れているから。好みの問題。 - 記事で参照しやすいように関数や構造体に異なる呼び名をコメントにて付与した。こうするとオーバーロードした関数のどれが呼ばれるのかがわかりやすいと思った。
-
unsignedをstd::size_tに変更した。元のコードを改良してみようと色々いじっている時に「unsigined intの部分がunsigned longになっている」という旨のコンパイルエラーと遭遇したので変更した(逆だったかも)。コードを改良しようとしなければunsignedのままでも問題なく動くので、今回に関して言えば好みの問題。
# include <array>
// 記事中での名称: テンプレート構造体seq
template<std::size_t... Is> struct seq{};
// 記事中での名称: テンプレート構造体gen_seq#N
template<std::size_t N, std::size_t... Is>
struct gen_seq : gen_seq<N-1, N-1, Is...>{};
// 記事中での名称: テンプレート構造体gen_seq#0
template<std::size_t... Is>
struct gen_seq<0, Is...> : seq<Is...>{};
// 記事中での名称: 可変引数テンプレート関数concat
template<std::size_t N1, std::size_t... I1, std::size_t N2, std::size_t... I2>
constexpr std::array<const char, N1+N2-1> concat(const char (&a1)[N1], const char (&a2)[N2], seq<I1...>, seq<I2...>){
return {{ a1[I1]..., a2[I2]... }};
}
// 記事中での名称: テンプレート関数concat
template<std::size_t N1, std::size_t N2>
constexpr std::array<const char, N1+N2-1> concat(const char (&a1)[N1], const char (&a2)[N2]){
return concat(a1, a2, gen_seq<N1-1>{}, gen_seq<N2>{});
}
実際に動かしてみる
実際にテンプレート関数concat を使ってコンパイル時文字配列を結合してみよう。以下のコードでは文字列"Hello, " とworld." を持つ文字配列を結合している。テンプレート関数concat の戻り値はstd::array<const char, std::size_t N> となっているので関数std::puts で出力するときはdata メソッドで先頭ポインタを取得する。
# include <cstdio>
# include "original.hpp"
int main ( const int argc, const char* const argv[] ){
static constexpr char s1[] = "Hello, ";
static constexpr char s2[] = "world.";
static constexpr const auto s_array = concat( s1, s2 );
std::puts( s_array.data() );
return 0;
}
実行すると以下の出力が得られる。
Hello, world.
なおテンプレート関数concat にはコンパイル時文字配列ではなく文字列リテラルをそのまま渡しても動く。
解説
実行順序
関数concat(s1, s2) が呼ばれたとき以下の順に呼ばれる。
- テンプレート関数concat
- 可変引数テンプレート関数concat
テンプレート関数concat
// 記事中での名称: テンプレート関数concat
template<std::size_t N1, std::size_t N2>
constexpr std::array<const char, N1+N2-1> concat(const char (&a1)[N1], const char (&a2)[N2]){
return concat(a1, a2, gen_seq<N1-1>{}, gen_seq<N2>{});
}
この関数は「要素数_N1_ のchar 型配列への参照」と「要素数_N2_ のchar 型配列への参照」を引数に取り、N1 と_N2_ は非型テンプレートになっている。戻り値の要素数-1 がついているのはヌル文字をひとつ減らすからだ。
この関数内では可変引数テンプレート関数concat を呼ぶとき引数にgen_seq<N1-1>{} とgen_seq<N2>{} を渡しているが、これはともにテンプレート構造体gen_seq#N のオブジェクトである。
テンプレート構造体seq_gen#N
// 記事中での名称: テンプレート構造体seq
template<std::size_t... Is> struct seq{};
// 記事中での名称: テンプレート構造体gen_seq#N
template<std::size_t N, std::size_t... Is>
struct gen_seq : gen_seq<N-1, N-1, Is...>{};
// 記事中での名称: テンプレート構造体gen_seq#0
template<std::size_t... Is>
struct gen_seq<0, Is...> : seq<Is...>{};
テンプレート構造体seq_gen#N では可変引数テンプレートが使われている。std::size_t... Is の部分で先頭以外のテンプレート引数をまとめて受け取り(空の場合もある)、基底構造体の指定で書かれているIs... の部分で展開される。このテンプレート構造体はテンプレート引数は異なるが自身を継承している。つまり、このテンプレート構造体の継承関係は再帰的になっているのだ。再帰構造を抽象的なまま理解するのは難しいのでまずは具体的に_N_ = 3 として展開してみよう。
-
seq_gen<3>は非型テンプレートはN= 3 で可変引数テンプレートは空なのでseq_gen<2,2>から派生している。 -
seq_gen<2,2>は非型テンプレートがN= 2 で可変引数テンプレートはIs...= 2 なのでseq_gen<1,1,2>から派生している。 -
seq_gen<1,1,2>は非型テンプレートはN= 1 で可変引数テンプレートはIs...= 1, 2 なのでseq_gen<0,0,1,2>から派生している。 -
seq_gen<0,0,1,2>は非型テンプレートは_N_ = 0で可変引数テンプレートはIs...= 0, 1, 2 である。このseq_gen<0,0,1,2>はテンプレート構造体seq_gen#N の部分特殊化された構造体であるテンプレート構造体seq_gen#0 である。そのためseq_gen<0,0,1,2>は構造体seq<0,1,2>から派生している。これはテンプレート構造体seqの実体のうちのひとつである。
以上より構造体seq_gen<3> は構造体seq<0,1,2> の派生構造体であることがわかる。帰納的に考えるとseq_gen<N> はseq<0,1,・・・,N-1> の派生構造体である。(注意:ここでの「・・・」は言語仕様ではなく数字が順に並んでいることを表す表記法として使用しています。以下同様。)
テンプレート関数concat 内で可変引数テンプレート関数concat を呼ぶ際にseq_gen<N1-1> とseq_gen<N2> が渡されるが、可変引数テンプレート関数concat の定義をみるとその部分はテンプレート構造体seq として受け取っている。つまりテンプレート関数concat 内で可変引数テンプレート関数concat を呼ぶ時には引数としてseq<0,1,・・・,N1-2> とseq<0,1,・・・,N2-1> を渡していることになる。
可変引数テンプレート関数concat
// 記事中での名称: 可変引数テンプレート関数concat
template<std::size_t N1, std::size_t... I1, std::size_t N2, std::size_t... I2>
constexpr std::array<const char, N1+N2-1> concat(const char (&a1)[N1], const char (&a2)[N2], seq<I1...>, seq<I2...>){
return {{ a1[I1]..., a2[I2]... }};
}
この関数は2つの「char 型配列への参照」と2つのテンプレート構造体seq を引数に取り、配列の要素数_N1_, N2 とseq の可変引数テンプレート引数I1..., I2... をテンプレート引数に取る。テンプレート関数concat から呼ばれる場合はI1..., I2... はそれぞれI1... = 0,1,・・・,N1-2 とI2... = 0,1,・・・,N2-1とに展開されるので、戻り値として渡される初期化子は{{a1[0],a1[1],・・・,a1[N1-2],a2[0],a2[1],・・・,a2[N2-1]}} と展開される。この初期化子内でa1[I1]... と書いている部分はパラメータパックの拡張という機能である。
こうして第1引数のchar 型配列のヌル文字を除いた要素と第2引数のヌル文字を含んだ要素を順に持つstd::array が戻り値として返されるのである。
まとめ
今回、以下の項目について学ぶことができた。
- 配列への参照
- 可変引数テンプレート
- 可変引数テンプレートを用いた再帰的な定義
本当はここから改良してchar 型配列もしくはchar* で受け取れる戻り値を返す関数を作りたくてstd::initializer_list を試すなどしてみたけどうまくいかなかった。しかし色々勉強できたのでとりあえずは満足した。
間違いや誤字脱字、理解不足な点があれば指摘していただけると助かります。