メインコンテンツに移動

原理主義者のメモリー管理

コンピュータは, そもそも自分のメモリーを1次元の数直線に並べて管理しています. ですので, 「多次元配列」というのは全て見せかけの虚構, 嘘で塗り固められた幻のようなものです.そう聞くと,「やはり本質を使わねばならん!」と叫び出す人が湧きます.では,その方法を学びましょう.

どうやって1次元配列を2次元配列に見せかけるのかというと:

a0

単純ですね.これで\([0:8]\times[0:3]\)の2次元配列ができます.1次元配列の範囲は\([0:36-1]=[0:35]\)です.

一般に, \([0:n-1]\times [0:m-1]\)の2次元配列であれば, 

  • 要素数は\(n\times m\)
  • \(k\)番目の要素は
    • \(i=k\%n\)  \(k\)を\(n\)で割った余りが, \(i\)
      • 例えば\(k=24\)ならば\(k\%9=6=i\)
    • \(j=k/n\)  \(k\)を\(n\)で割った結果が, \(j\)
      • 例えば\(k=24\)ならば\(k/9=2=j\)
  • \((i,j)\)要素は\(i+n*j\)番目にある.
    • 例えば\((i,j)=(6,2)\)ならば\(k=6+9\times2=24\)

これを使って, 2次元配列を管理します.3次元以上の配列も, まったく同様です.

いちいち上の計算を書くのが面倒であれば, クラスにすればよい. これをプロが書いたものが, Boost/Array にあるので, 書くだけ無駄ですが, まあ練習だと思って書いてみましょう. 

#ifndef myArray_hpp
#define myArray_hpp
#include <iostream>
#include <vector>
class myDoubleArray
{
  std::vector<int> _sizes;   //各次元の要素数を入れるところ
  std::vector<double> _data; //実際のデータを入れる1次元配列
public:
  double* data()             //myArray.data()で1次元配列のデータにアクセスできる 
  {
    return _data.data();     //std::vectorでは, data()で1次元配列のデータにアクセスできる
  }
  const int size()           //myArray.size()でデータの総数
  {
    int sz=1;
    for(auto k:_sizes) sz*=k;
    return sz;
  }
  const int size(const int index)  //myArray.size(dim)でdim次元目の要素数
  {
    return _sizes.at(index);
  }
  double& operator[](const int k)  //オペレータ0: myArray[k]がk番目のデータ
  {
    return _data[k];
  }
  const int index(const int dim,const int k)
  {
    int _index=k;
    for(int i=0;i<dim;i++) _index/=_sizes[i];
    if (dim==_sizes.size()-1) return _index;
    return _index%_sizes[dim];
  }
  //2次元配列プログラム
  myDoubleArray(const int n0,const int n1)     //コンストラクタ1: 2次元配列
  {
    _sizes.resize(2); //2次元配列である
    _sizes[0]=n0; _sizes[1]=n1;
    _data.resize(size());
  }
  const int serial(const int i0,const int i1)
  {
    return i0+_sizes[0]*i1;
  }
  double& operator()(const int i0,const int i1) //オペレータ1:(i,j)を呼び出す
  {
    return _data[serial(i0,i1)];
  }
  //3次元配列プログラム
  myDoubleArray(const int n0,const int n1,const int n2)
  {
    _sizes.resize(3); //3次元配列である
    _sizes[0]=n0; _sizes[1]=n1; _sizes[2]=n2;
    _data.resize(size());
  }
  const int serial(const  int i0,const int i1,const int i2)
  {
    return i0+_sizes[0]*i1+_sizes[0]*_sizes[1]*i2;
  }
  double& operator()(const  int i0,const int i1,const int i2)
  {
    return _data[serial(i0,i1,i2)];
  }
};
#endif /* myArray_hpp */

こんな感じですね. constは散りばめれば散りばめるほど計算が早くなるので,コッテコッテに厚化粧で塗りたくっています.このプログラムで, 2次元配列と3次元配列が可能になります.

myDoubleArray My2D(10,12);

この宣言では, コンストラクタ1が起動します. _sizes()に, 各次元の要素数(10,12)が記録され, _data()のサイズが設定されていますね! ここで

My2D(3,4)=3.1415926535897932;

と書くと, オペレータ1が起動します. 説明通りに\(i+n*j\)番目の_data()の参照が返却され, そこに3.1415926535897932を代入しています. オペレータ1はdouble& のように参照を返却しますので,  そこに3.14...を代入すると, _data()配列の該当箇所に3.14....が書き込まれます.一方,

My2D[43]=3.141592...;

と書くと, オペレータ0が起動してしまいますので, オリジナルの1次元配列_data()配列の43番目に3.141592...が書き込まれます.43番目に対応する\(i,j\)の値は, このindex()関数で取得できます.こいつは, index(0,k)でk番データの0次元目の添字が何か, index(1,k)でk番データの1次元目の添字が何か,...を計算します.

このfor(auto k:_sizes)の部分は新しい文法です. これは従来の書き方では

for(int i=0;i<_sizes.size();i++)
{
    auto k=_sizes[i];
    ...

と書いていたところです._sizes配列の全ての要素で, なにかの作業をするわけです・・・が,実はほぼ全てのfor文が,配列の全ての要素で, なにかの作業をするわけですから,プログラムがfor(int i=0;i<_sizes.size();i++)で埋め尽くされて読みにくい.そこで,「要は_sizes配列の各要素kで,これをしなはれ」と書けるようになっています.これを「範囲指定(range based) for」と呼びます.

少し考えると分かることですが, 配列の要素を使うのであれば for(auto temp_name:array_name) で良いわけですが, その配列要素を書き換えてしまう場合には,  for(auto& temp_name:array_name) のように参照にしておかなければなりません. autoだけだとtemp_nameはオリジナルのコピーであるからです. 配列要素を書き換える場合でなくても, 要素が巨大でコピーが大変な場合, 参照にしておけば良いです.というか, いつも黙って  for(auto& temp_name:array_name) のように書いておいて間違いはないっす.

mainプログラムの方は

#include "myArray.hpp"
#include <iomanip>
int main(int argc, const char * argv[]) {
  std::cout << "テスト2D配列3x4" << std::endl;
  myDoubleArray MDA(3,4);  //2次元配列を作成
  for(int i=0;i<MDA.size(0);i++) for(int j=0;j<MDA.size(1);j++) MDA(i,j)=i+j*j;
  for(int k=0;k<MDA.size();k++)
    std::cout << "Value[" << std::setw(3) << k << "]="
    << "Value("<<MDA.index(0,k)<<","<<MDA.index(1,k) << ")="
    << MDA[k] << std::endl;
  std::cout<<" At(0,2) Value " << MDA(0,2) << std::endl;
  std::cout<<" At(2,3) Value " << MDA(2,3) << std::endl;
  std::cout << "テスト3D配列3x4x2" << std::endl;
  myDoubleArray MDB(3,4,2); //3次元配列を作成
  for(int i=0;i<MDB.size(0);i++) for(int j=0;j<MDB.size(1);j++) for(int k=0;k<MDB.size(2);k++)
    MDB(i,j,k)=i+j*j+100*(k+1);
  for(int k=0;k<MDB.size();k++)
    std::cout << "Value[" << std::setw(3) << k << "]="
    << "Value("<<MDB.index(0,k)<<","<<MDB.index(1,k) << "," << MDB.index(2,k) << ")="
    << MDB[k] << std::endl;
  std::cout<<" At(0,2,0) Value " << MDB(0,2,0) << std::endl;
  std::cout<<" At(2,3,1) Value " << MDB(2,3,1) << std::endl;
  return 0;
}

これを実行してみると:

テスト2D配列3x4
Value[  0]=Value(0,0)=0
Value[  1]=Value(1,0)=1
Value[  2]=Value(2,0)=2
...
Value[ 10]=Value(1,3)=10
Value[ 11]=Value(2,3)=11
At(0,2) Value 4
At(2,3) Value 11
テスト2D配列3x4x2
Value[  0]=Value(0,0,0)=100
Value[  1]=Value(1,0,0)=101
...
Value[ 22]=Value(1,3,1)=210
Value[ 23]=Value(2,3,1)=211
At(0,2,0) Value 104
At(2,3,1) Value 211

とまあ, こんな感じで動くわけです.あ,std::setw(2) は新要素ですね.これは #include <iomanip> に含まれている関数で,「出力する文字の桁幅を設定」というものです.

クラステンプレート

さてmyDoubleArrayでは, double型の多次元配列を定義できました.そうすると次は整数 int の多次元配列が欲しくなります.さらに言えば, 僕が今から解こうという気体領域の多次元配列や気体分子の速度分布関数の多次元配列も,毎回これ書かなきゃダメなの?という疑問が出てきます.疑問のもとは

  • いや〜ほとんど同じなんで.書けいうなら書きますけどね.時間の無駄です.全部テンプレですから.
  • 本当は時間の無駄どころではない.一つにバグがある場合,コピペで作成した残りも全部,原理的にバグを含むことになる.それらが多数あり,全部直すという作業が入ると,できる人とできない人がいる.人類の場合,後者が98%程度を占めるので,要するにプロジェクトが停滞し人件費がかかる

これを解決するのがクラステンプレートです.

まず, myDoubleArrayクラスから,コピペによってmyIntArrayクラスを作成する手順を復習しましょう.

  • myDoubleArrayのコードを「ぺろろーん」とコピーして,どこかにペースト
  • myDoubleArrayと書いてある部分を検索置換でmyIntArrayに変更
  • double と書いてある部分を検索置換でintに変更

ですよね.これをプログラムとして書いちゃうわけです.こんな感じですね:

template <class T> class myArray // ここの部分が「検索置換しなさい」という部分. クラス名は変えておいた
{
  std::vector<int> _sizes;
  std::vector<T> _data;    //ここの<T>が置換される
public:
  T* data()                //double --> T
  {
    return _data.data();
  }
  ...
  T& operator[](const int k)
  {
    return _data[k];
  }
  ...
  myArray(const int n0,const int n1)
  {
  ...
  T& operator()(const int i0,const int i1)
  {
    return _data[i0+_sizes[0]*i1];
  }
  ...
};

こんな感じですね. Tのところが double になったり int になったり, あるいはあなたの定義したクラスになったりします.で,仕込みは上々ってわけで, mainで実際に使う時には, こんなふうに書きます:

int main(int argc, const char * argv[])
{
  std::cout << "テスト2D配列3x4" << std::endl;
  myArray<double> MDA(3,4); //2次元配列を作成
  ...

了解でしょうか. std::vector<double> みたいな感じですね.あれも,テンプレートなんですよ.