☆C++11テクニック☆ 配列を配列で初期化する方法+α
メンバー変数の初期化
例えば、こんなコードがあったとします。
template <class T> struct Hoge { Hoge(const T &v) : value(v) {} T value; }; template <class T> Hoge<T> make_hoge(const T &v) { return Hoge<T>(v); } #include <iostream> #include <cxxabi.h> #include <cstdlib> #include <memory> template <class T> std::ostream &print(std::ostream &os, const T &value) { int status; std::unique_ptr<char, decltype(&std::free)> demangled(abi::__cxa_demangle(typeid(value).name(), 0, 0, &status), &std::free); return os << demangled.get() << ": " << value << std::endl; } int main() { auto hoge = make_hoge(142857); print(std::cout, hoge.value); return 0; }
結果はもちろん、こうなります。
int: 142857
メンバー変数が配列になると……?
では次に、make_hoge()
に渡す実引数を次のように変えてみましょう。
constexpr auto hoge = make_hoge("142857");
コンパイルすると、次のようなエラーが発生します。
main.cpp: In instantiation of 'Hoge<T>::Hoge(const T&) [with T = char [7]]': main.cpp:12:19: required from 'Hoge<T> make_hoge(const T&) [with T = char [7]]' main.cpp:30:29: required from here main.cpp:4:29: error: array used as initializer Hoge(const T &v) : value(v) {}
どうやら、「配列をvalue(v)
の形式で初期化できないよ」と言われているようです。
仕方ないので、配列の時だけコンストラクタを特殊化して、次のようにしてみましょう:
template <class T, int N> struct Hoge<T[N]> { Hoge(const T (&v)[N]) { std::copy_n(&v[0], N, &value[0]); } T value[N]; };
出力は、こうです。
char [7]: 142857
おお、上手く行きました!
では次は、このクラスをコンパイル時に構築できるよう、Hoge
のコンストラクタやmake_hoge()
にconstexpr
をつけてみましょう。
コンパイル結果は、こうなります:
main.cpp: In constructor 'constexpr Hoge<T [N]>::Hoge(const T (&)[N])': main.cpp:14:71: error: constexpr constructor does not have empty body constexpr Hoge(const T (&v)[N]) { std::copy_n(&v[0], N, &value[0]); } ^ main.cpp: In function 'int main()': main.cpp:40:39: error: the type 'const Hoge<char [7]>' of constexpr variable 'hoge' is not literal constexpr auto hoge = make_hoge("142857"); ^ main.cpp:12:8: note: 'Hoge<char [7]>' is not literal because: struct Hoge<T[N]> ^ main.cpp:12:8: note: 'Hoge<char [7]>' is not an aggregate, does not have a trivial default constructor, and has no constexpr constructor that is not a copy or move constructor
どうやら、constexpr
なコンストラクタに中身がある事がお気に召さない様子。C++14では(std::copy_n()
がconstexpr
でさえあれば)大丈夫なのかもしれませんが、少なくともgcc4.9.0では-std=c++1y
をつけてやってもダメです。
では、どうすればよいのでしょうか?
さて、ここでC++の配列初期化方法を思い出してみます。
int hoge[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
つまり、この方法を利用すれば、コンストラクタの中身を空にする事ができるのでは? ……はい、その通りです。
template <class T, int N> struct Hoge<T[N]> { constexpr Hoge(const T (&v)[N]) : value{/* 0, 1, 2, ... */} {} T value[N]; };
それではこのvalue{}
の中身は、どう書けばよいのでしょうか……というのが今回の本題になります。ああ長かった。
N要素の配列メンバー変数の初期化
配列の要素数ごとにvalue
の初期化子の要素数が変わるので、任意のN
に対して適切な初期化子を書くのは非常に困難なように見えます。
C++でメタプログラミングを始めたばかりの人であれば、最初に思いつくのはこんなコードでしょう:
template <class T> struct Hoge<T[1]> { constexpr Hoge(const T (&v)[1]) : value{v[0]} {} T value[1]; }; template <class T> struct Hoge<T[2]> { constexpr Hoge(const T (&v)[2]) : value{v[0], v[1]} {} T value[2]; }; template <class T> struct Hoge<T[3]> { constexpr Hoge(const T (&v)[3]) : value{v[0], v[1], v[2]} {} T value[3]; };
利用する全てのN
について、コンストラクタを特殊化してしまえばよろしい。解決ですな。
……もちろん、そんな頭の悪い事など推奨できるはずもありません。だってこれ、#define
すらできないじゃん。できてもやりませんが。
C++11erであれば、当然次のように書くでしょう:
template <class T, int N> struct Hoge<T[N]> { template <int ... i> constexpr Hoge(const T (&v)[N]) : value{v[i] ...} {} T value[N]; };
さて問題は、このint ... i
をどのように生成するか、になります。
もちろん、Hoge<T[N]><0, 1, 2>("10")
なんて事を手動で書きたくはありません。i
の数を間違えると、余裕で配列外にアクセスしてしまいますし。
Nから0...N-1を作る方法
こんな時、N
から0...N-1
のリストを作れるようにすれば、物事は万事解決です。
ちょっとだけクラス数は増えますが、こんな感じになればよいでしょうか?
template <int N> class make_indices { public: template <int ... i> struct indices {}; typedef indices<i ...> type; // ←ココ }; template <class T, int N> class Hoge_impl { friend class Hoge<T[N]>; // コンストラクタのみHoge<T[N]>に公開する private: template <int ... i> constexpr Hoge_impl(const T (&v)[N], indices<i...>) : value{v[i] ...} {} public: T value[N]; }; template <class T, int N> struct Hoge<T[N]> : public Hoge_impl<T, N> { constexpr Hoge(const T (&v)[N]) : Hoge_impl<T, N>(v, typename make_indices<N>::type()) {} };
この、make_indices<N>::type
を生成する方法さえ見つかれば、全ては解決です。
アイディアとしては、テンプレートの再帰を使います。例えばこんな感じ:
template <int N> class make_indices { private: // 最初はM==Nで、Mを1つ減らすごとにsizeof...(i)を1増やす template <int M, int ... i> struct make_indices_impl : public make_indices_impl<M - 1, i ... , sizeof...(i)> {} ; // M==0となったので、i...には「0〜N-1」のN連続の値が入っている // この時のindices<i...>が、求めるindices<i...> template <int ... i> struct make_indices_impl<0, i...> { typedef indices<i ...> type ; } ; public: typedef typename make_indices_impl<N>::type type; };
実行してみると、
char [7]: 142857
この通り、予期した通りに動作する事がわかります。
オマケ:今のの応用
これを応用すると、配列をコンパイル時に逆転させてみたり、
template <class T, int N> class Hoge_impl { template <int M, int ... i1, int ... i2> constexpr Hoge_impl(const T (&v1)[M], indices<i1...>, const T (&v2)[N-M], indices<i2...>) : value{v1[i1] ..., v2[i2] ...} {} public: T value[N]; };
のようにすると配列をコンパイル時に結合してみたりと、テンプレートメタプログラミングの幅が広がります。 もっとも、そこまで高度なメタプログラミングをしなければならないのなら、わざわざ車輪を再発明しなくても素直にSproutライブラリなりCELなりを使った方がずっと良いと思いますが。