筆者大多情況用 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) 之特偏化,

這問題目前筆者還沒得到解答。

edisonx 發表在 痞客邦 PIXNET 留言(1) 人氣()


留言列表 (1)

發表留言
  • novus
  • 之前一直沒注意到這篇。簡單的 comment:

    1. 設計物件的通則,如果一個功能放在類別之外也可以做得一樣好,就不應該讓它變成 member。這樣 class 不僅更好寫,而且使用上也更有彈性。這點對 operator 也不例外,除了必須操作私有成員的 operator 外,其餘的應該移到 class 外。這是C++發展二十多年來獲得的經驗,所以你會發現比較先進的程式庫並不嘗試把所有功能做在 class 中。

    以我的觀點來看,我會將三角函數或log之類全部從class介面拿掉,只保留非成員版本。

    2. 你這邊提到的偏特化問題,因為這個 class 機能很簡單,所以其實不難解決。舉例來說,operator== 不同精度浮點數 epsilon 不同,整數無須 epsilon,使用非成員實作:

    bool operator==(const Complex<int>& lhs, const Complex<int>& rhs);

    template <typename T>
    bool operator==(const Complex<T>& lhs, const Complex<T>& rhs);

    這樣不就分出來了嗎?再加上 type traits 還可以做得更好。arg 之類的函數也是同樣的道理。(這樣有沒有體會意見1.其中一個小小的好處?)

    3. 更複雜的情況下,你可以將整個 class 中隨 T 變化的部分抽取出來變成組件,透過 type traits 選擇現在該應用的組件。這裡無法詳述。
  • 謝謝 novus 的補充,這些對我很寶貴,重頭再閱讀自己的筆記和您的意見,收益匪淺,感謝。

    edisonx 於 2012/10/10 09:29 回覆

您尚未登入,將以訪客身份留言。亦可以上方服務帳號登入留言

請輸入暱稱 ( 最多顯示 6 個中文字元 )

請輸入標題 ( 最多顯示 9 個中文字元 )

請輸入內容 ( 最多 140 個中文字元 )

請輸入左方認證碼:

看不懂,換張圖

請輸入驗證碼