筆者大多情況用 C 語言,較少碰 C++,所以從頭自己寫 class 之經驗也不多,
這算是筆者第一份較完整的 class ,對於 C++ 裡之 complex 可能有所差異,
一份初版之 code 連結於此,ComplexConsole 、ComplexDialog 分別為於
Console 及 Win32 Dialog 下之測試 (使用之 Complex 為同一份),
裡面有幾份狀況沒再做處理,光是在 declare 貼上來就不短。
由於是第一份 class,在開發過程中有許多想法,於此筆記紀錄。
1. 基本運算
overload operator + - * / : 複數 op 複數 與 複數 op 實數
overload operator += -= */ /= : 複數 op= 複數 與 複數 op= 實數
overload operator + - : 正負號運算子,非加減法運算子。
值得一提的是,乘法和減法是可以減少使用之實數乘法計算量的。
除此之外,還有全域做
Complex<T> operator + - * / (const T & , const Complex<T> & >
2. 比較運算
operator== 比較運算和 c++ 內附的 complex 有所不同,
大多之作法是 return m_imag==imag && m_real==real ;
但筆者設定了一 const static T CPX_EPS = 1E-9 ; 也就是
return std::fabs(m_imag - imag) <= CPX_EPS && std::fabs(m_real-real)<=CPX_EPS;
同時為了維持統一性,operator!= 便以 !operator== 方式調用。
設定 CPX_EPS 是好是壞全看個人,目前看到的 sample code 確實沒採用此機制,
於是筆者用多開了一個 bool IsEqu(const Complex<T> & cpx, const T & eps = 0);
之 member function,做為比較之指定誤差侯補用。
一樣地,也有做域做
bool Complex<T> operator==(const T & v_real, const Complex<T> & cpx);
bool Complex<T> operator!=(const T & v_real, const Complex<T> & cpx);
至於大於、小於,一度想以模數 (norm) 做為比較,但後來想想語意上不夠直覺,便予以作罷。
3. 基本函式
簡單說公式如下,注意事項後註。
norm () : m_imag * m_imag + m_real * m_real;
abs() : std::sqrt(norm) ; [ no good ]
args() : std :: atan2( m_imag , m_real);
imag() : return m_imag ;
real() : return m_real;
polar() : 傳入 rad, theta ; 該 complex<T> 依據此二參數做變更
inv() : 做倒數運算。
上面在 cmath 裡都有,做法上大同小異,只差沒有 inv。
另在 cmath 裡面,imag() 與 real() 同時兼具 getter 與 setter 功能,
為避免使用上之誤會困擾,故習慣上是弄成 get_real() / set_real,
對 image 也如此。
較有問題、要注意的大概屬 abs 函式,展開來後長這樣
sqrt ( m_real * m_real + m_imag * m_imag) ;
實際上若 m_real 與 m_imag 都很小,諸如 1E-9 時,
算出來的 sqrt 會等於零,這在其他 function 之應用容易出問題,
於是加一點小技巧。
假設 fabs(m_real) > fabs(m_imag),則將 m_real 提出根號外面,變成
m_real * sqrt( 1.0 + m_imag * m_imag / (m_real * m_real) );
反之則提出 m_imag,如此盡量減低浮點數誤差所帶來之影響。
這點是開發完後想到的修正,原始碼裡沒做這個。
4. 三角、雙曲
三角函式:包含了 sin、cos、tan。注意到這裡可以做得可難可易,複雜的方法從切必雪夫(Chebyshev)公式之常數系數做計算分析,這裡是直接從 exponment 化 sin, cos 公式去做化簡。
雙曲函式:包含 sinh、cosh、tanh。要算 sinh、cosh、tanh 也有數種作法,也包含級數和公式,唯筆者直接拿他們與 sin, cos, tan 之關係式做推導切換。
反三角函式、反雙曲函式:反三角函式與反雙曲函式也可以用公式慢慢推過來,但求出來的解可能不只一個,故筆者沒再針對這二系列函式做處理。
5. 幕次、指數
nroot (const size_t n) : 假設已知 z ,w^n = z,求 w。這裡用的是棣美佛定理,唯答案算出來會有 n 個,所以需要傳一個 vector 或 array 進去接結果出來。
sqrt() : 複數開根號其解答有二個,其實只要將 nroot,將 n 代入 2 後化簡便可得到。注意到 c++ cmath 裡,對於 sqrt 二個根裡只挑一個出來,可查 c++ reference 觀看其規則。
這裡使用之方式較低效,純粹是調用 nroot(2),再依據 c++ reference 給定之規則判斷要傳回何值,complex sqrt 在 wiki 上已有說明其結果,有興趣做較高效者可試著化簡。
pow( ) : pow 函式共做三個, pow(const T & real) , pow(const Complex<T> & cpx) , 還有一個是 global function ( not member function) , pow(const T & v_real, const Complex<T> & cpx); 若是 (複數) ^ (實數) ,這個用棣美佛定理就行;但若為 ( 複數 ) ^ ( 複數),要花點時間推導公式。
exp() : 依定義做,exp(x+iy) 可化成 exp(x) * cos(y) + i exp(x) * sin(y),代入公式即可。
log() 、log10()、log2():一樣從公式推導出來,log () 算出為 ln(z) = ln( |z| ) + i [ arg(z) ] ,其他的 log10、log2 只是再多乘常數而已。
--- Improve ---
有幾點是後來想到要做的,但這些應在一開始規劃時就做好。
1. 例外處理部份
這份 class 幾乎沒在做例外處理部份,只有用 if-else 在做。其實 complex 在例外處理方面大多只是抓除零錯誤,筆者是以 assert 方式做,事後認為用 throw 一份 class error object 應也不錯。
2. 與 C++ complex 不同之處
拿筆者手邊之 vs 為例,它的 data member 是直接弄成一個二個元素之 array ( 如 double _Val[2] ),可能在其它之設計函式庫上調用會較方便。
較大不同處為,筆者將所有 cmath 裡支援 complex 之函式都做成 member function,再額外全域做成函式 ( 也沒用到 friend ),所以下面這兩種呼叫都是可行的。
Complex<double> c1(1.1, 2.2);
Complex<double> c2;
const double theata = 1.732;
c2 = c1.sin(); /* call member function */
c2 = sin(c1) ; /* call non-member function */
但一般 c++ 裡之 complex 顯得較簡單,除了 + - * / , += -= *= /= 、imag()、real() 外,其他都屬 non-member function。
單以 complex 使用上之習慣,或許就如標準函式庫所設計即可,即 member function 可不用那麼多。
另外在浮點數誤差處理那裡與標準函式庫做的處理也不同,這在上述有說明過。唯應注意的是,由於 CPX_EPS 筆者預設成 1E-9,在 Complex<float> 時可能會被截成 0 ,到時做比較時或許必須調用 Complex<float>::IsEqu() 。
3. 新增 fabs
abs 和 fabs 在 c++ 裡面是一樣的意思,只是部份習慣寫 C 之 coder,較習慣調用 fabs 。complex 在 fabs 與 abs 之行為也一樣,只是多開一個 function 出來而已。
* 4. 表頭 / 實作分離
這問題記得在 C++ Primer 裡有提到一點,但最後沒再深入研究。
具 template 之 class,有些編譯器可分開 .h / .cpp 獨立撰寫,
即 .h 是放宣告, .cpp 是實作 < 前提是要加上某個關鍵字,待查證 >
但 google 結果,目前似乎沒有任何 compiler 可支援到這裡,
換句話說,template 之 class / function,
目前是全塞在同一個 file 裡面,沒辦法 declare、implement 分在不同檔案裡,
這點筆者倒蠻不習慣的。
* 5. 特偏化問題
這部份其實沒有很有效的方法達到目標。
trace vc 裡之 complex ,裡面針對了 float、double、long double 三種資料型態做特偏化。
開發到後半段時已在思考,像是 template<typename T> T Complex<T>::arg() const ;
假設為 Complex<int>,在求 arg() 時傳回也為 int,但認為實在不怎麼合理,
即使 m_imag、m_real 本身為整數,但化為極式 (polar) 時,
其長度(radu.) 與角度 (args) 實在不該再傳回整數,
故一度認為 complex 是較適合「只」實作 float、double、long double 三種 POD 資料型態,
即
template<typename T> class Complex<T> {}
template<> class Complex<float} { ....}
template<> class Complex<double} { ....}
template<> class Complex<long double} { ....}
問題來了,要特偏化這三種資料型態,筆者不知道有沒有什麼簡便的方式,
目前筆者想到的是一份 code 寫三次,做細部修改;
另一種方式顯得較噁心,是用 C 語言以 #define 方式實現 template 實作,
這也是筆者較不願意使用之方式。
有沒有技巧能將 source 只寫一次,但一次只做 n 種資料型態 (或 class) 之特偏化,
這問題目前筆者還沒得到解答。
留言列表