関数に対するconst について
関数に対するconstですが、ここではconstのメソッドではなく戻り値に対するconstに関して見ていこうかと思います。意図をもってconstをつけることで関数のユーザに関数の機能を明確に伝えることができますが、逆に意味のないconstをつけることで関数のユーザを悩ましたりしますので一度整理しておこうかと思います。
- void型の関数に対するconst
const void v_func(void);
この記述はコンパイルこそ通りますが、何の意味もないconstになります。思考のノイズになるかと思いますので消しましょう。
- ポインタ(参照)でない関数に対するconst
const int add(int, int); int _tmain(int argc, _TCHAR* argv[]) { const int foo = add(1, 2); // add(3, 5) = 10; //const がついていなくともNG return 0; } const int add(int lhs, int rhs) { return(lhs + rhs); }
const int fooの宣言時初期化部分がなんとなく意味がありそうな書き方ですが、ポインタや参照でない関数に対するconstは基本的には無意味な記述となります。const変数に対する宣言時初期化で右辺がconstである必要はありません(上の場合はmain関数が呼び出されたタイミングで初期化されます。) またadd(3, 5)=10;の部分ですが関数の戻り値は基本的には書き換えることができません。戻り値の実態は呼び出す側のstackないし汎用レジスタに一時的に格納されますが、それを書き換えることができたとしても再利用できません。なお基本的にはと書きましたが、実験すると下記のようなケースではコンパイルが通りました。
- 構造体変数の戻り値に対するconst
typedef struct StByte_{ char cLsb; char cMsb; }StByte; const StByte c_func(void); StByte d_func(void); int _tmain(int argc, _TCHAR* argv[]) { // c_func().cMsb = 0x34;//constの値に対する代入 d_func().cLsb = 0x38; std::cout << d_func().cLsb << std::endl; return 0; } const StByte c_func(void) { StByte stT = { 0x33, 0x34 }; return(stT); } StByte d_func(void) { StByte stT = { 0x33, 0x34 }; return(stT); }
c_func.cMsb = 0x34 の部分は関数にconstがついている為にerrorになります。しかしd_func.cLsb=0x38 の部分は代入ができてしまいます。しかしその後のコンソールに表示されるのは関数で設定した値0x33になりますので、この代入式が意味を持つことはありません。stackないし汎用レジスタを書き換える式がOKな場合もあるということですが結局再利用することはないので意味はないかと思います。さて最後にポインタを返す関数に対するconstを見たいと思います。
- ポインタを返す関数に対するconst
typedef struct StByte_{ char cLsb; char cMsb; }StByte; const char* a_func(void); char * const b_func(void); const StByte * e_func(void); StByte * const f_func(void); char cSt1[5] = "abcd"; char cSt2[5] = "efgh"; char *pcSt = cSt1; int _tmain(int argc, _TCHAR* argv[]) { // *a_func() = 'e'; //関数に対するconstがポインタの指す先のchar型の値への代入を禁止している *b_func() = 'e'; // a_func()[1] = 'e';//関数に対するconstがポインタの指す先のchar型の値への代入を禁止している b_func()[1] = 'e'; // a_func() = cSt2; //ポインタ自体を書き換えることはconstがついていなくても結局できない // b_func() = cSt2; //constがついているので当然禁止だが、ついていなくても結果は同じ // e_func()->cLsb = 56; //ポインタの指す先の値への代入を禁止している f_func()->cLsb = 65; return 0; } const char * a_func(void) { return(pcSt); } char * const b_func(void) { return(pcSt); } const StByte *e_func(void) { static StByte stT = { 0x33, 0x34 }; return(&stT); } StByte * const f_func(void) { static StByte stT = { 0x33, 0x34 }; return(&stT); }
ポインタを返す関数の場合はconstをつけることで関数を通して値を変更するなという意思表示ができます。(アドレスを返すと間接的にいくらでも値を変更することができるので、そもそもポインタを返していいのか?という議論はあります。) ただしポインタ自体の変更はconstをつけようがつけまいが変更できないので b_funcのようなポインタに対するconstは意味がありません。
まとめ
MathJaxに関するメモ
TEXの表示がおかしくなっていました。サイドバーのアドレスを変えたら直ったので記載しておきます。CDNと書いてあるから近場のサーバからサービスを供給してくれるのだと思います。
- 変更前のアドレス
<script src="https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>
- 変更後のアドレス
<script src="http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML" type="text/javascript"></script>
参考にさせていただいたエントリ
ポインタでやってしまうミス④
C++は型Checkが厳密で暗黙の型変換で行けると思っていたらキャストをつけないといけないパターンに良く出くわします。しかし気軽にキャストしていると思わぬ落とし穴がありますので切り取っておきます。
Pattern5
#include "stdafx.h" typedef short LEVEL; void getLevel( LEVEL *iLev); int _tmain(int argc, _TCHAR* argv[]) { unsigned char iLev = 0; getLevel((LEVEL*)&iLev); return 0; } void getLevel( LEVEL *iLev) { *iLev = 5; }
想定としては、関数getLevelは何かの設定値を教えてくれる隠蔽された関数だとします。データ型のLEVELも良く検索しないとその型は不明なんですが、設定値の上限だけは知っていてuint8で充分な大きさである事を知っているためiLevの型はunsigned char型にしました。
ローカルに宣言した値iLevに設定値をもらいたいのでポインタにして渡しているだけですが、このプログラムを実行すると何が起こるでしょうか?
iLevがunsinged char型なので汎用ポインタもしくはStackには1byteだけが割り当てられます。しかしLEVEL型はshortなので*iLev = 5; は 2byteの代入が行われます。という事はiLevの隣のアドレスが00で破壊されてしまいgetLevelを呼び出した後の動作が不定になります。この場合は宣言時にunsigned char iLev; ではなくLEVEL型を使用することが推奨されます。
まとめ
キャストを使う前に使用する型に合わせた宣言を心掛ける。
constの位置の覚え方
constの置く位置はいつも「どこに置くんだっけな?」と迷うものですが、僕の覚え方を紹介したいと思います。C言語の文献やサイトでは特に言及されているのを見たことがないので、内容の正しさには少し自信がありません。ひょっとするとコンパイラの作成者の中では当たり前の事なのかもしれないし、はたまた見当違いなことなのかもしれませんので予めご了承下さい。
どういったケースで頭を悩ますかというと次のようなケース
char foo[] = "abcd"; char * const baz = foo; // ① const char * bar = foo; // ②
この辺は分かっている人も多いと思いますが、
①はポインタ変数に対する代入が禁止になります。baz = bar; が禁止
②はポインタの指し示す先のchar型の変数に対する代入が禁止になります。bar[0] = 'e';が禁止
となります。天下り的に覚えていても問題ないのですが、忘れがちなのではないかと思います。では次のようなケースはどの代入が禁止になるでしょうか?
char foo[] = "abcd"; char bar[] = "efga"; char *baz = foo; char *qux = foo; char * const * foobar = &baz; foobar = &qux; // これが禁止?① *foobar = bar; // これが禁止?② *foobar[0] = 'h';// これが禁止?③
答えは②ですが、前出のケースを天下り的に覚えた場合はこのケースに対応するのが難しいんじゃないでしょうか? これから紹介する二つのルールを覚える事でconstの位置を正しく選ぶことが出来るようになります。
1) const は 「① const ② ③ ④ ‥」という順序で修飾する型情報を探索し、変数名のTokenにたどりつくと探索をやめる。
※ データ型、および間接演算子”*”の場合に探索を続ける。
※ const等の修飾子は無視する。
2) constが修飾できる型は以下の三つに分類される。
\(データ型\) | 任意のデータ型の変数に対する代入を禁止する |
\(データ型 *^i\) | ポインタの指し示す先の任意のデータ型の変数に対する代入を禁止する |
\(*^i\) | ポインタ変数に対する代入を禁止する |
※間接演算子"*" が二個以上ある場合は”ポインタの指し示す先”を付け加える事になります。たとえば** であればポインタの指し示す先のポインタ変数への代入が禁止になります。
以上のルールを踏まえて先ほどのコードを見てみましょう char * const * foobar; ですがconst の修飾先の優先度①には* があります、さらに探索を続けると優先度②にも*があります。優先度③は変数名のTokenなので探索が中止されます。という事でこのconst が修飾するのは ** となり ポインタの指し示す先のポインタ変数への代入が禁止、すなわち *foobar = bar;が禁止になります。以上を踏まえてまとめると
constの位置 | 修飾する型 | foobar = &qux; | *foobar = bar; | *foobar[0] = 'h'; |
char ** const foobar | * | × | ○ | ○ |
char * const * foobar | ** | ○ | × | ○ |
const char ** foobar (=char const ** foobar) |
char** | ○ | ○ | × |
なおダブルポインタで全ての代入を禁止するには const char * const * const foobar = &baz; となります。例えば const char const * const * foobar = &baz; 等とやってしまうとそれは、同じ意味を持つconstが存在することになりwarningにはなりますがerrorにはなりません。しかもこの場合は foobar = &qux; は代入OKとなるのでconstの運用はしっかり理解して行いましょう。
乗算と除算
前回のエントリで乗算器と除算器の話題に触れました、今回はコーヒーブレークとして実践的ではないですが乗算と除算をハードウェアではなくてソフトウェアで実現する場合どうするかを見ていきたいと思います。
- 乗算(掛け算)
筆算の手順をそのまま再現しますので例として0b1001 × 0b0110 (9×6)を見ていきましょう。
\begin{align*}\begin{array}{rr} & 1001 \\ × & 0110 \\ \hline & 0000 \\ & 1001~~ \\ & 1001~~~~ \\ & 0000~~~~~~\\ \hline & 0110110 \end{array}\end{align*}
答えは0b0110110 = 54 になりました。よくよく筆算の中身を見てみると0b0110を下位ビットから走査して、ビットが立っていたら0b1001をずらして足すという構造に気付くかと思います。さっそくコードにプロットしてみましょう。(固定小数点の時に作成した積の関数とリプレースできる形で書きます。)
UnFix16 operator*( CFix16 &cF ) { UnFix16 uF; __int64 i_a = 0; for(int i=0 ; i<32; i++){ if( ((__int64)cF.get().iNum >> i) & 0x01 ){ i_a += ( (__int64)m_uF.iNum << i); } } uF.iNum = i_a >> 16; return( uF ); }
32bitの計算ですが、計算過程は64bitに拡張しています。これは筆算を見ればわかるように、例えば4ケタ同志の積は8ケタになっており計算過程、結果においてはbitを拡張しています。とはいえ32bitしか表現できませんので、一端64bitまで拡張して最後に16bitづらして小数点の位置を調整しています。
- 除算(割り算)
割り算も幾つか方法はあるものの乗算同様、筆算をプログラムで再現することになります。例として0b1101 ÷ 0b0010 (13÷2)を見てみましょう。(mathjaxというサービスでTEXを表示していますがcline命令を実装していないみたいで筆算らしからぬ見た目になってしまいました。)
\begin{align*} \begin{array}{r|rr} & 0110.1 & \\ \hline 0010 & 1101.0 \\ & 10~~~~~~~ & \\ \hline & 10~~~~~ & \\ & 10~~~~~ & \\ \hline & 01~~~ &\\ & 0~~~ & \\ \hline & 10~ & \\ & 10~ & \\\hline & 0~ & \end{array}\end{align*}
答えは6.5になっていますね。掛け算と同様、構造をよく見てビットを走査していきたいのですが、割り算は最上位ビットから検算していく仕組みです。最上位ビットから一つづつ位を増やしていき、順次0b0010を引いていきます。引ける時にビットを立てて、引けない時には0を置きます。それではプログラムを見てみましょう。
UnFix16 operator/( CFix16 &cF ) { UnFix16 uF; __int64 i_a = 0; __int64 i_b = 0; __int64 i_op1 = (__int64)m_uF.iNum; __int64 i_op2 = (__int64)cF.get().iNum << 32; for(int i=1; i<=64; i++){ i_a <<= 1; i_op1 <<= 1; i_b = i_op1 - i_op2; if( i_b >= 0 ){ i_a |= 0x01; i_op1 = i_b; } } uF.iNum = i_a >> 16; return( uF ); }
m_uF.iNumが割られる数で、cF.get().iNumが割る数です。最初に32bit左シフトしていますが、本来なら割られる数は最上位ビットから走査していくため、m_uF.iNum >> 32-i みたいな項が現れます。しかし残念ながら シフト演算はX >> -1と言うようにマイナス値のオペランドを設定しても、X << 1 と解釈してくれない為、桁以上の計算に制限が出てきます。そこで左シフトだけ使えば良いようにと最初に割る数の方の位を拡張しました。掛け算同様、最後に小数点の位置を調整しています。
まとめ
ソフトだけで乗算、除算を実現しようとすると、見てきたように計算量が大きくなってしまいます。ハードウェアで実装されている乗算器、除算器はこれら手間をとってかわってくれています。他にもハードウェアが肩代わりしてくれる処理が多くあるのでアーキテクチャの資料をよく見ましょう。
固定小数点数について
何回かに渡り初等関数の計算について取り上げてきました。本ブログのテーマが組み込みである事を差し置いて変数の型にdoubleを使用していましたが、実際に組み込みにおいてdoubleを使用するには、対象マイコンのアーキテクチャにFPU(Floating point number Processing Unit)が含まれているかを確認した方が良いかと思います。(例えばRXマイコンであればFPUが搭載されていますがfloatと限定的です。その場合はあまり誤差が問題になることはないと思いますのでdoubleをfloatと読み替えてください。) 整数型の演算は僅かなCPUサイクルで実現できるようハードウェアに除算器、乗算器が搭載されていますが、浮動小数点の演算はFPUがないと多くのCPUサイクルを消費してしまいます。そのため知らないでdoubleを使用するとシステムのパフォーマンスを悪化させかねません。
さてFPUがない上に小数点を扱わなければいけないといった時どうすればよいでしょうか?その場合は固定小数点数を使います。一般的に固定小数点数の演算の方が浮動小数点数の演算より早いため精度を多少犠牲にするものの多くのDSPではいまだ固定小数点数を使用しています。
固定小数点数の型は標準では準備されていません、自分で準備しなければいけませんが、ここでは32bitで固定小数点数を表してみたいと思います。小数点以下は任意ですのでここでは16bitとします。
31 | 30 | 29 | 28 | 27 | ~ | 19 | 18 | 17 | 16 | 15 | 14 | 13 | 12 | ~ | 3 | 2 | 1 | 0 |
sign | decimal | fraction |
最上位ビットをsignにしていますが実際のマイナスの表現は2の補数(ビット反転して1足した数)を使用したいと思います。この型を以下のように準備してみました。ビットフィールドなのでエンディアンがシステム依存になりますが、よしとさせて下さい。
typedef union UnFix16_{ signed int iNum; struct StFix16{ unsigned int fraction: 16; unsigned int decimal : 15; unsigned int sign : 1; } stFix16; }UnFix16;
ここでunionでint型とstructを共存させているところがポイントとなります。なお似たようにfloat型の中身にアクセスする型は次のようになります。
typedef union UnFloat_{ signed int iNum; struct StFloat{ unsigned int fraction: 23; unsigned int exponent : 8; unsigned int sign : 1; } stFloat; }UnFloat;
型は準備できましたので次に変換や代入、演算を準備していきましょう。関数の引数としてこれらを準備しても良いのですが、クラス表現にしてみます。
#include <iostream> class CFix16{ public: CFix16(){}; CFix16(float f_Fix) : m_uF(fromfloat(f_Fix)){} virtual ~CFix16(){} UnFix16 fromfloat(float fL); float CFix16::tofloat(void); inline UnFix16 const get(void){ return m_uF; } /* 複写 */ const CFix16 &operator=( const float &fn ) { m_uF = fromfloat(fn); return *this; } const CFix16 &operator=( const UnFix16 &uF ) { m_uF = uF; return *this; } const CFix16 &operator=( CFix16 &cn ) { m_uF = cn.get(); return *this; } /* 比較 */ CFix16 operator==( const float &fn ) { if( m_uF.iNum == fromfloat(fn).iNum ){ return 1; }else{ return 0; } } CFix16 operator==( CFix16 &cF ) { if( m_uF.iNum == cF.get().iNum ){ return 1; }else{ return 0; } } /* 演算 */ UnFix16 operator+( CFix16 &cF ) { UnFix16 uF; uF.iNum=m_uF.iNum + cF.get().iNum; return( uF ); } UnFix16 operator-( CFix16 &cF ) { UnFix16 uF; uF.iNum=m_uF.iNum - cF.get().iNum; return( uF ); } UnFix16 operator*( CFix16 &cF ) { UnFix16 uF; uF.iNum = ((__int64)m_uF.iNum * (__int64)cF.get().iNum) >> 16 ; return( uF ); } UnFix16 operator/( CFix16 &cF ) { UnFix16 uF; uF.iNum = (((__int64)m_uF.iNum << 32) / cF.get().iNum) >> 16 ; return( uF ); } private: UnFix16 m_uF; }; UnFix16 CFix16::fromfloat(float fL) { UnFix16 uF; UnFloat *uFl = (UnFloat*)&fL; uF.iNum = 0; if( uFl->stFloat.sign ){ uFl->stFloat.sign = 0; uF.stFix16.decimal = (unsigned int)fL; fL = ( fL - (unsigned int)fL + 1.0); uF.stFix16.fraction |= uFl->iNum >> 7; uF.iNum = ( ~uF.iNum + 1); } else { uF.stFix16.decimal = (unsigned int)fL; fL = ( fL - (unsigned int)fL + 1.0); uF.stFix16.fraction |= uFl->iNum >> 7; } return uF; } float CFix16::tofloat(void) { UnFix16 uF = m_uF; float fL; UnFloat *uFl = (UnFloat*)&fL; uFl->iNum = 0; uFl->stFloat.exponent = 127; if( uF.stFix16.sign ){ uF.iNum = ~( uF.iNum - 1 ); uFl->stFloat.fraction |= uF.stFix16.fraction << 7; fL = fL + (float)uF.stFix16.decimal - 1.0; uFl->stFloat.sign = 1; }else{ uFl->stFloat.fraction |= uF.stFix16.fraction << 7; fL = fL + (float)uF.stFix16.decimal - 1.0; } std::cout << fL << std::endl; return fL; }
floatから固定小数点数、固定小数点数からfloatへの変換ですが 直観的に小数点を扱えるようにするための施策であって、実際のプログラム上では使用しないようにします。固定小数点数の強みが発揮できませんし、変換時の誤差が殊の外大きいです。演算子のオーバーロードをもっと準備してもよいのですが、最低限は記載していると思います。よくよく見てみると足し算や引き算は普通の足し算、引き算を使っているし、乗算や除算は計算時に64bitに拡張しているだけで、いつもの掛け算、割り算と対して変わりありません。固定小数点数というのはつまるところ演算には整数型の計算機を使用して、表現だけ小数点にしたものに他ならないのです。補足ですが整数計算と同じ計算機を使用することから浮動小数点の時のような情報落ち、桁落ちという誤差は発生しません。このクラスを使用するクライアントコードは次のようになります。
#include "CFix16.h" int _tmain(int argc, _TCHAR* argv[]) { CFix16 x, y, z, w; x = 12; y = 16; z = ( x * y ); w = ( x / y ); x.tofloat(); z.tofloat(); w.tofloat(); return 0; }
この様なクラスを使用して、今まで紹介した初等関数の計算を書き換えてみると固定小数点数でのライブラリが作成できるかと思います。
XMLを作ってみる②
今回はデザインパターンの一つであるコンポジットパターンを応用してXMLを作ってみます。C言語で四苦八苦したソースが大分すっきりした形でかけます。前回と同様次のXMLを作成する事を目標とします。
<root> <tag1> <tag2>element_start</tag2> <tag2>element_middle</tag2> </tag1> <tag1> <tag3>element_last</tag3> </tag1> </root>
コンポジットパターンに当てはめてXMLの構造を拾ってみましょう。クラス図で書くと。
一般にコンポジットパターンはインターフェースを継承しますが、今回はElementを持つ最終ノード(CLastNode)もTagしか持たない通常ノード(CNode)もすべてTag情報を保持する構造なので、sTagメンバを格上げして抽象クラス(CAbstractNode)にしました。通常ノードは下位に同じく通常ノード、もしくは最終ノードを従えますのでCAbstractNode型のメンバ nodesを持ちます(コンテナクラスを保持してる場合、集約関係を使います。) showNode()メソッドは深さ優先探索に使います。判りにくいかもしれませんが、とりあえずこのクラス図をコードにプロットしてみます。
<Composite.h>
#pragma once #include <iostream> #include <string> #include <vector> using namespace std; class CAbstractNode { public: virtual void showNode(void) = 0; virtual void setTag(string sTag); protected: string sTag; }; class CNode : public CAbstractNode { public: virtual ~CNode(); virtual void showNode(); void addNode(CAbstractNode* nodes); private: vector<CAbstractNode*> nodes; }; class CLastNode : public CAbstractNode { public: virtual void showNode(); void setElement(string sEle); private: string sElement; };
つぎにこれらのメソッドは
<Composite.cpp>
#include "Composite.h" void CAbstractNode::setTag(string sTag) { this->sTag = sTag; } CNode::~CNode() { for(int i= 0; i<(int)nodes.size(); i++){ delete nodes[i]; } } void CNode::addNode(CAbstractNode* nodes){ this->nodes.push_back(nodes); } void CNode::showNode() { cout << "<" << sTag << ">" << endl; for(int i=0; i<(int)nodes.size(); i++){ nodes[i]->showNode(); } cout << "</" << sTag << ">" << endl; } void CLastNode::setElement(string sEle) { sElement = sEle; } void CLastNode::showNode() { cout << "<" << sTag << ">" << sElement << "</" << sTag << ">" << endl; }
ポイントはaddNodeメソッドでアロケートしてメンバを増やしているところです。ここで木構造を実現しています。また最終ノードと通常ノードの型の違いを抽象クラスを使う事で多相的に処理をしています。デストラクタがありますがここではあまり注目しなくてもかまいません。では最後にクライアントコードを見てみましょう。
<cliant.cpp>
#include "Composite.h" int _tmain(int argc, _TCHAR* argv[]) { const string s_root = "root"; const string s_tag1 = "tag1"; const string s_tag2 = "tag2"; const string s_tag3 = "tag3"; CNode *cRoot = new CNode; CNode *cNode1 = new CNode; CNode *cNode2 = new CNode; CLastNode *cElem1 = new CLastNode; CLastNode *cElem2 = new CLastNode; CLastNode *cElem3 = new CLastNode; cRoot->setTag(s_root); cRoot->addNode(cNode1); cRoot->addNode(cNode2); cNode1->setTag(s_tag1); cNode1->addNode(cElem1); cNode1->addNode(cElem2); cNode2->setTag(s_tag1); cNode2->addNode(cElem3); cElem1->setTag(s_tag2); cElem1->setElement("element_start"); cElem2->setTag(s_tag2); cElem2->setElement("element_middle"); cElem3->setTag(s_tag3); cElem3->setElement("element_last"); cRoot->showNode(); delete cRoot; return 0; }
C言語でXMLを実現したときと比べ非常にすっきりしているかと思います。最後のshowNode()が実行されると木構造の深さ優先探索が始まりコンソールにお題のXMLが出力されます。
※はてなキーワードをよける為ところどころ全角文字が含まれてますので注意下さい。