程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> 詳述trait和policy演化(一)

詳述trait和policy演化(一)

編輯:關於C語言

        開博時發了一篇“trait和policy研習”的源碼,很少有朋友關注,寫的太簡略,這一次把它展開來,發現一下子還寫不完,分兩次吧!希望和朋友們一起學習!         給定一個整型數組,要求編寫一個函數,求其內元素的和。這是一個簡單的編程,大家都能很快的編寫出如下的程序: #define SIZE 5;  
int a[SIZE]={10,11,12,13,14};  
int Toatal(const int a[], int size)  
{  
int i=0, total=0;  
while(i<size)  
{  
  total+=a[i]  
  i++;  
}  
return total;  
} 也可能有人這麼寫: int Total(const int* a0, const int* an)
{
int total=0;
while(a0!=an)
{
  total+=*a0;
  a0++;
}
return total;
}
上面的程序有問題嗎?我們向下看。 我們把第二種形式的求和函數改寫成模板,試圖使之適用於更多的類型。 template<typename T>
T Total(const T* beg, const T* end)
{
T total=0;
while(beg!=end)
{
  total+=*beg;
  beg++;
}  
return total;
}
上面的程序有問題嗎?細心的朋友可能會想到:T total=0;我們不能用0去初始化一個類型為T的對象,我們不知道T具體是什麼樣的,最好的方法是顯示的調用T的默認構造函數,於是我們可以改一下:T total=T();
由於使用了模板,我們可以對不同的類型值求和,我們測試下面兩個數組: int a[5]={0,1,2,3,4};
char b[5]={'a', 'b', 'c', 'd', 'e'};
void main()
{
int sum0=Total(a, a+5);
int sum1=Total(b, b+5);
}
結果你會發現:sum0=10(沒有問題),sum1=-17(問題在哪裡?)
問題在於,T total=T();數組b是一個char型的,total也是一個char型的,而char型值的范圍很小,累加很容易越界。於是就出現了-17.其實這種錯誤很容易出現在我們的編程中,特別是在嵌入式編程中,很多時候有人建議用char型其實也未必,內部有對齊問題),一不小心就出錯了。上面兩個C函數,其實也存在這個問題,多個整型值的和不一定還在整型范圍內。這在我們的編程中應該注意。   如此看來,我們有必要把模板函數修改一下,最直接和容易想到的是: template<typename R, typename T>
R Total(const T* beg, const T* end)
{
R total=R();
while(beg!=end)
{
  total+=*beg;
  beg++;
}  
return total;
}
這時,我們的調用大概如此:int sum1=Total<int>(b,b+5);於是,我們得到了正確的結果495。注意:這裡R和T的位置順序不能顛倒。這個方案看起來不錯,“但是我們仍然期望完全避免這個約束”《C++ Templates中文版》語)。在我看來,我更習慣於這樣的調用形式:int sum0=Total(b, b+5);而非int sum1=Total<int>(b,b+5)。   C++前輩們給我們展示了另外一種解決方案:
聲明一個類: template<typename T> class TotalTrait;
或者定義一個類:
template<typename T> class TotalTrait{}; (這裡我測試的情況看,都可以,但相關書籍上用的是聲明形式的,大概與編譯有關,因為這裡我們只關心特化的類)下面我們通過顯示特化,使得另外一種類型R與T相關聯: template<>
class TotalTrait<char>
{
public:
typedef int R;
};
這個特化的意思大體表述如下:如果T是char的話,定義R的類型為int。
相似的我們可以寫出很多特化類: template<>
class TotalTrait<int>
{
public:
typedef long R;
};
template<>
class TotalTrait<float>
{
public:
typedef double R;
};
這裡,我們就為Total創建了Trait類,並提供了3種特化。 於是我們再改寫一下上面的模板: template<typename T>
typename TotalTrait<T>::R Total(const T* beg, const T* end)
{
typedef typename TotalTrait<T>::R R;
R total=R();
while(beg!=end)
{
  total+=*beg;
  beg++;
}
return total;
}
這裡對typename的另一種用法稍作解釋:typename TotalTrait<T>::R Total(const T* beg, const T* end)和typedef typename TotalTrait<T>::R R;中的typename是一種聲明的用法,它聲明TotalTrait<T>::R是模板類TotalTrait<T>裡定義的一種類型。看下面一個例子:TotalTrait<T>::R *m;本意是定義一個TotalTrait<T>::R類型的指針m,但由於沒有typename聲明類型,編譯器會認為R是TotalTrait<T>中的一個靜態成員,於是變成了兩個變量相乘的語意。
在主程序中調用上面的模板更符合傳統風格:int sum1=Total(b, b+5);結果也是正確的495.
如果有一個新的類型,我們只需要提供一個顯示的關於Total的特化類,就能夠讓Total函數正常運行。如:我們自定義了一個類A,同時求和後結果會是B類型,於是: template<>
class TotalTrait<A>
{
public:
typedef B R;
};
已知:A c[5]={...}
調用:B sum=Total(c, c+5); 關於返回值引發的問題,我們通過一系列的嘗試,找到了幾個解決方法,都取得了不錯的結果。關於這個問題,我們暫時只討論這麼多。
上面的程序中,還有一個細節曾經被我們忽略。我們初始化R total=R()時,能夠保證總是返回一個合適的值嗎?對於多數的內建類型如char、int、long等,在我們的Total函數中都能夠產生正確的初始值。但是如果是一個用戶定義的類A或者外部引用的類B呢?或者某個
類C的C()={12,34,56}呢?顯然不行。我們再考慮,如果我們吧累和變成累積呢?顯然初始化成0不正確。所以這個問題我們必須要認真面對,我們需要一個顯式的正確的初值定義。解決這個問題,我們依然可以用TotalTrait類,但用第一個模板方式似乎無能為力。
於是我們擴充TotalTrait定義: template<>
class TotalTrait<char>
{
public:
typedef int R;
static const R zero=0;
};
template<>
class TotalTrait<int>
{
public:
typedef long R;
static const R zero=0;
};
這裡特化時,在類中增加了一個靜態常型成員zero,並初始化為0。
再修改一下上面的模板: template<typename T>
typename TotalTrait<T>::R Total(const T* beg, const T* end)
{
typedef typename TotalTrait<T>::R R;
R total=TotalTrait<T>::zero;
while(beg!=end)
{
  total+=*beg;
  beg++;
}
return total;
}
ok,一切正常。我們特化更多: class TotalTrait<float>
{
public:
typedef double R;
static const R zero=0.0;
};
sorry,編譯通不過:
d:\redwolf\redwolf\base\trait.h(156) : error C2864: “redwolf::base::TotalTrait<float>::zero”: 只有靜態常量整型數據成員才可以在類中初始化
生成日志保存在“file://d:\redwolf\redwolf\Debug\BuildLog.htm”
標准C++,只允許在類中對整型成員和枚舉類型靜態常量成員初始化。
我相信,一些朋友看著看著就有點暈頭了,不過沒關系,你如果一邊看一邊在編程測試的話,體會會深一點,然後再想想,就能記住其中的絕大部分內容了。
有朋友一定很快想出一個方法:是把初始化留個C++定義文件,而不是聲明文件。如: xxx.hpp:
class TotalTrait<float>
{
public:
typedef double R;
static const R zero;
};
xxx.cpp:
const TotalTrait<float>::R zero=0.0;
行了,這樣的確行了,但這樣的分離操作容易引發一個問題,如果編譯器不知道或者沒法知道其他文件.cpp)中對zero賦初值了,那麼一定導致了某種未知的不確定的賦值方案。你可以試試把.Cpp中的初始化注釋掉看看,如果編譯器不警告或者出錯,就說明zero不能按預想的方式初始化。 我們可以仔細的想,這裡提供一種合適的解決方案: template<>
class TotalTrait<char>
{
public:
typedef int R;
static R Zero(){
  return 0;
}
};
template<>
class TotalTrait<int>
{
public:
typedef long R;
static R Zero(){
  return 0;
}
};
class TotalTrait<float>
{
public:
typedef double R;
static R Zero(){
  return 0.0;
}
};
我們簡單的把靜態數據成員改成靜態成員函數。同時對模板簡單修改: template<typename T>
typename TotalTrait<T>::R Total(const T* beg, const T* end)
{
typedef typename TotalTrait<T>::R R;
R total=TotalTrait<T>::Zero();
while(beg!=end)
{
  total+=*beg;
  beg++;
}
return total;
}
ok,問題解決了。 一切都按照預想的進行,生活是如此的美好。人們總是在追求更美好的東西,讓我們繼續追尋前輩的足跡吧!
問:我們是否總是需要這種默認指定的trait呢?或者我們很多時候不需要無謂的擴大數據類型,比如很多時候我們能夠保證我們的int型數組元素之和仍然在int范圍內,這時候我們的返回值可以不擴大成long型。我們需要一種更靈活的配置方式,允許用戶自己簡單修改返回值類型。
一個有效的想法是提供模板參數: template<typename T, typename RE>
typename RE::R Total(const T* beg, const T* end)
{
typedef typename RE::R R;
R total=RE::Zero();
while(beg!=end)
{
  total+=*beg;
  beg++;
}
return total;
}
但這樣的話,我們每次都要指定兩個模板參數,實在煩人,因為多數情況下,我們不太關心返回值只要正確就行)。默認參數,當然可以,但現行的C++標准不支持對模板函數的模板參數提供默認值。如果不能在直接的方式下解決問題,我們就應該想到用類去包裝它: template<typename T, typename RE=TotalTrait<T> >
class CTotal
{
public:
static typename RE::R Total(const T* beg, const T* end)
{
  typedef typename RE::R R;
  R total=RE::Zero();
  while(beg!=end)
  {
   total+=*beg;
   beg++;
  }
  return total;
}
};
於是我們可以這樣調用:
 int sum1=CTotal<char>::Total(b, b+5);
或者:
 int sum1=CTotal<char, TotalTrait<T> >::Total(b, b+5); 這樣表達式太復雜,稍顯麻煩,但我們可以這樣處理一下: template<typename T>
typename TotalTrait<T>::R Total(const T* beg, const T* end)
{
return CTotal<T>::Total(beg, end);
}

template<typename T, typename RE>
typename TotalTrait<T>::R Total(const T* beg, const T* end)
{
return CTotal<T, RE>::Total(beg, end);
}

如此處理,調用形式就跟以前一樣簡單:
 int sum1=Total(b, b+5);
如果用戶指定trait,就需要這樣調用:
 int sum1=Total<char, TotalTrait<char> >(b, b+5);(所幸這種形式很少用。) 本文我們主要討論了泛型函數中的兩個問題:返回值和初始化。 最後,作為本部分的結語,讓我們看看偉大的Josuttis給出的關於trait的定義:trait提供了一種配置具體元素通常是類型)的途徑,而該途徑主要是用於泛型計算。

本文出自 “狼窩” 博客,轉載請與作者聯系!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved