2014. 4. 8.
石立 喬

Visual Studio Express 2013 の新しい使い方(2)

――― コンソールアプリケーションで、range-based forを使う ―――

 コンソールアプリケーションは、応用面での便利さは劣るものの、UI(User Interface)のコードに割く労力が要らないので、言語本来の学習には向いている。 ISO C++11の規格がVisual Studio Express 2013に本格的に取りいれられているので、新しい機能の使用に努める。
 ここでは、新しく採用されたrange-based forとautoを使って、入力データから回帰直線を求めるプログラムを試してみた。

最小二乗法を用いて回帰直線を求める
 実験などで得たデータから、それに近似する直線や多項式、またはその他の関数を求めることを、曲線のあてはめ(Curve Fitting)と呼び、数値計算の応用の一つである。特に、統計学の回帰分析では、あてはめた直線のことを「回帰直線」と呼び、多く使用される。
 統計学では、回帰直線を、
   
の一次式で表現する。代数学で使われる形式と異なるので、注意されたい。
 回帰直線は、最小二乗法によって求める。すなわち、回帰直線を仮定し、それと実際の測定データとの誤差を二乗して累計したものが最小になるように、回帰直線の係数を求める(aは切片、bは回帰係数と呼ばれる)。
 下式が二乗誤差で、これが最小になるaとbの値は、aとbについての偏微分を0と置いてできる二元連立一次方程式を解いて求まる。
   
 途中を省略して、最終的に、回帰係数bと切片aは以下の式によって求まるので、ここではこの式を使用する。

   


ここで使用する新しい言語機能の簡単な説明
 本サイトは、従来から、文法よりも理工系技術者が関心を持つ応用に重点を置き、文法の解説は他のサイトや書籍に頼ってきた。この方針は現在でも変わらないが、ここで初めて取り上げた機能を簡単に説明する。

◎range-based for(範囲ベースのfor、範囲に基づくfor)
 C#やJavaのforeachに相当する。range-based forは、文字通り、範囲が決まっている配列、文字列、STLコンテナ(vectorが代表的)に対して、頭から最後まで、連続的にすべてを対象に処理する場合に用いる。
 通常のforループに比べて、自由度が少ないが、1)目的がはっきり分かる、2)コードが簡潔である、の理由で使われる。
 一例として、vectorの名称をvectとし、要素の値をyとすると、次の3種類の書き方があり、
 1) for(auto y : vect){ ----- }
    要素をコピーして使うので、元の要素が書き換えられる心配はない。
    しかし、要素の値が単純な型でない場合には、コピーの時間がロスになる。
 2) for(auto &y : vect){ ----- }
    要素への参照なので、元の要素を変更できる。しかし、危険を伴う。
 3) for(const auto &y : vect){ ----- }
    要素への参照であるが、元の要素に書き込めないので安全である。
用途に応じて使い分ける。一般には、3)が推奨され、for_each関数と同程度の性能と言われる。
◎autoキーワード
 上の説明でautoを使用したが、autoは、コードの簡略化に有効である。コンパイラが型を推論(type inference)してくれるので、誤りも少なくなる。上の場合は、vectの型を確認しなくても使用できる。ここでは使用していないが、for_each関数で、イテレータを宣言するのに、vector<int>::iterator の代わりにautoを使うと、コードが簡潔になり、間違いも少なくなる。
◎cin標準入力ストリームオブジェクトの関数
 cinを使ってキーボード入力をさせる場合、期待する型以外の入力があった時に、プログラムが無限ループに入る恐れがある。これを回避するために、いくつかの関数がある。cin.fail()関数は、このような際にtrueを返し、エラーフラグを立てる。cin.clear()は、エラーフラグを消すのに用いる。cin.ignore(streamsize, delim)関数は、誤って入力された文字列をバッファから破棄する。
 streamsizeには、numeric_limits<streamsize>::max()を使っておけば確実で、delimには、'\n'が一般的である。numeric_limitsを使用するには、#include <limits>が必要と言われているが、Visual Studio Express 2013では必要がなかった。

プログラム
 このプログラムを実行すると、まず、データの入力が要求される。「1番目のデータ X=」のプロンプトに対してxの値を入力し、「1番目のデータ Y=」に対してyの値を入力する。順次、「2番目のデータ ・・・」と続き、xとyの値に共に0を入力すると、入力が終わり、確認のメッセージが出る。必要に応じて、削除や追加ができる。変更がない場合には、回帰直線の係数を計算して表示する。
 以下に、順を追って説明する。
 1)まず、Pointクラスを作成する。メンバー変数にはxとyがあり、入力するデータの二次元座標に対応する。あと、コンストラクタを用意しておく。この宣言は、メイン関数の前に記述しておく(図1参照)。


図1 インクルードファイルと、作成したPointクラス


 2)メイン関数では、まず、図2に示すように、データを入力させる。入力時に、文字などを誤って入力してしまった場合は、ci.fail()がtrueになり、cin.clear()でエラーをリセットし、cin.ignore()で入力を破棄して、最初に戻る。プログラムを簡単にしたため、yの入力時に誤っても、xから入力し直す必要がある。正常な入力であれば、Point型のvectorとして宣言した配列pdataに、順次データを入力して行く。xとyに共に0を入れると、入力を打ち切る。vectorを使っているので、最初に要素数を決めて置く必要はなく、pdata.push_back(<入力値>);で順次設定されて行く。


図2 データを入力させる部分


 3)入力されたデータを表示して、確認を取る。最初は、repeat_flagがtrueになっているので、確認を繰り返すように設定されているが、「変更なし --- 1」を入力すると、repeat_flagがfalseになって、以後は確認を繰り返さない。データの消去はpdata.erase()を使う。何番目かは、最初の要素のあるpdata.begin()から数えて指定する。「追加 --- 3」に対しては、pdata.push_back()で、最後の要素の後ろに追加する。図3は、以上述べた入力データ確認と修正の部分を示す。ここでも、選択肢の誤入力に対しては、再入力をさせる。ただし、case3:では、誤入力の対策をしてないので、ご注意を。


図3 データの変更ができるようにする


4) 図4に示す回帰直線の係数を計算する部分に、range-based forを使っている。pdataと言う名前のvectorコンテナの要素を順次読み出し、その要素をptとして、{  }内の処理を行う。後は、先に述べた回帰直線の係数を求める計算式を実行するのみである。


図4 range-based forを用いて繰り返し計算をする


得られた結果
 図5は、上述したプログラムを実行した結果である。まず、3組のデータを入力し、0を入力してデータ入力を終了する。2番目の入力データの誤りに気が付いたので、「変更しますか?」の問いに「2」を2回続けて入力して、一旦、2番目を消去し、2回目の「変更しますか?」で「3」を入力して、新しいデータを追加する。回帰直線の計算には、データの入力順は関係ないので、これで十分修正ができる。


図5 実行結果の例


 最も標準的な、単純forループ、「新しい使い方(1)」で紹介したfor_each関数、いずれを使っても同じ結果が得られる。図6に、それらを示す。慣れれば、やっぱり、range-based forが一番簡単かな、と思う。


図6 等価なforループとfor_each関数