value_type : The first template parameter (T)
定義這種 type 其實用途不是為了 container 本身,而是為了使用這個 container 的 user,特別是使用這個 container 的地方是在 template class / function 時,定義這個 type 尤為重要,細節可以參考我之前寫的這篇。然而 C++ template 雖然可以讓程式撰寫上有更好的泛用性,但是因為需要做型態推導上,在使用上只要推導過程中會有疑義都會導致編譯失敗,也因此需要多做些手腳以便能夠順利編譯。這次要講的就是上次那篇 type function 的延伸在進入主題前先來看個例子:假設今天要寫一個 function 是傳入一串數字,回傳值是這串數字的總合,我相信就算剛學寫程式沒多久的人都寫得出來這個版本 (這邊先假設傳進來的 type 都會是整數)
int computeSum(const int* val, const int count)
{
int sum = 0;
for(int i=0; i < count; ++i) { sum += val[i]; }
return sum;
}
當然,只能是整數的話這重用性太低了,所以我們使用 template 來重寫一次:template <typename T>
T computeSum(const T* val, const int count)
{
T sum = T();
for(int i=0; i < count; ++i) { sum += val[i]; }
return sum;
}
好的,這樣就能針對各種不同的 type 了,但是這樣其實還有缺陷:如果傳進來的不是一般的 array 呢? 我想傳 vector 進來行不行? 只是計算總合的話,傳 linked list 進來應該也可以吧?針對這些問題,我想只要參考 STL 的 document 就會知道答案了:參數不要傳 pointer 跟 array size,改成傳指向起點跟終點的 iterator,所以程式可以改寫成這樣:
template <typename Iterator, typename T>
T computeSum(Iterator first, Iterator last)
{
T sum = T();
for(Iterator iter=first; iter!=last; ++iter) { sum += *iter; }
return sum;
}
這樣就可以傳任何種類的 container 進來了,不過還是有缺陷:這邊多用了一個 template parameter 把 container 中 element 的 type 傳進 function,但其實傳 iterator 進 function 時我們就該知道 element 的 type 了。針對這問題,STL 有 iterator_traits 可以讓我們做到這件事,所以這小程式可以進一步改寫成這樣:template <typename Iterator>
typename std::iterator_traits<Iterator>::value_type computeSum(Iterator first, Iterator last)
{
typedef typename std::iterator_traits<Iterator>::value_type value_type;
value_type sum = value_type();
for(Iterator iter=first; iter!=last; ++iter) { sum += *iter; }
return sum;
}
前面提到的 value_type 就是用在類似這種地方上。這種做法其實也就跟之前我寫的這篇 type erasure 是相關的。好的,雖然前言有點冗長,不過我們接下來要進入正題了。這次來考慮一個更複雜點的功能:如果 container 裡的 element 不是 primitive type 而是其他 user defined 的 structure 或 class 呢? 比方說是個像下面這樣的 structure:
template <typename T>
struct Coor
{
T x, y;
typedef T value_type;
Coor(T val = T()) : x(val), y(val) {}
};
當然,為了方便說明與略過無謂的細節,我們先假設已經有 overload operator+= 了,template <typename Iterator>
typename std::iterator_traits<Iterator>::value_type::value_type computeSum(Iterator first, Iterator last)
{
typedef typename std::iterator_traits<Iterator>::value_type::value_type value_type;
value_type sum = value_type();
for(Iterator iter=first; iter!=last; ++iter) { sum += *iter; }
return sum;
}
這段 code 如果直接拿去編譯是編不過的,因為 compiler 看到這行
typename std::iterator_traits<Iterator>::value_type::value_type 時不會認為這是一種型態,所以型態推導過程就出錯啦。解決方法其實也很簡單,多一層包裝就可以了。首先我們先定義一個 template struct:
template <typename T>
struct value_type
{
typedef typename T::value_type type;
};
用途很簡單看傳進來的 type 是什麼,把他裡面定義好的 value_type 重新定義一次,如此一來我們可以把剛剛那個 function 重新改寫這樣:template <typename Iterator>
typename value_type<typename std::iterator_traits<Iterator>::value_type>::type computeSum(Iterator first, Iterator last)
{
typedef typename value_type<typename std::iterator_traits<Iterator>::value_type>::type value_type;
value_type sum = value_type();
for(Iterator iter=first; iter!=last; ++iter) { sum += *iter; }
return sum;
}
好的,現在又浮出了另一個問題:primitive type 可不會定義 value_type 這種東西,這是 user-defined 的 structure / class 才會有的,所以這樣寫會變成無法運用到儲存 primitive type 的 container。
不過這也是有辦法可以解決的,只要改寫 value_type 這個 structure,讓他的 type 會隨著 T 是 structure / class 還是 primitive type 做出不同的定義即可。所以修改如下:
// 當 T 是 container 時,value 會是 true
// 反之,T 是 primitive type 時,value 是 false
template <typename T>
struct has_value_type
{
typedef char true_type;
typedef char false_type[2];
//template not available if there's no nested value_type in U's scope
template <class U>
static true_type test(typename U::value_type*);
//fallback
template <class U>
static false_type& test(...);
//tests which overload of test is chosen for T
static const bool value = sizeof(test<T>(0)) == sizeof(true_type);
};
// predeclaration for partial specialization
template <typename T, bool b>
struct value_type_impl;
// 這邊定義當 T 是 primitive type 時,type 就是 T 本身
template <typename T>
struct value_type_impl<T, false> //if T doesn't define value_type
{
typedef T type;
};
// 如果 T 是個 structure / class,那麼 type 就該定義成 T::value_type
template <typename T>
struct value_type_impl<T, true> //if T defines value_type
{
typedef typename T::value_type type;
};
// 接下來這個 structure 就會先將 T 丟給 has_value_type 去判斷是不是 primitive type
// 再根據 has_value_type 裡 value 的值去決定要繼承 value_type_impl<T, false> 還是 value_type_impl<T, true>
// 如此一來 value_type<T>::type 就會根據 T 是不是 primitive type 而有所變化
template <typename T>
struct value_type : value_type_impl<T, has_value_type<T>::value> {};
所以現在我們可以這樣寫了:
int main()
{
// primitive type array
int v1[] = {1, 2, 3, 4, 5};
int sum1 = computeSum(v1, v1+5);
// vector
std::vector<int> v2;
for(int i=1; i < 5; ++i) { v2.push_back(i); }
int sum2 = computeSum(v2.begin(), v2.end());
// list
std::list<double> v3;
for(int i=1; i < 5; ++i) { v3.push_back(i); }
int sum3 = computeSum(v3.begin(), v3.end());
// Coor,當然,假設有 operload operator+=
std::vector< Coor<unsigned> > v4;
for(int i=1; i < 5; ++i) { v4.emplace(i); }
int sum2 = computeSum(v4.begin(), v4.end());
return 0;
}
本篇作法參考自這篇文章:
Can C++'s value_type be extended from iterator_traits to all types?
至於之前說的 overloading resolution 跟 name mangling 就讓我繼續拖稿吧XDD
沒有留言:
張貼留言