在1995年1月,Scott Meyers 在C++ Report雜志上就強調"min,max 對C++社團來說是一個很大的挑戰",他對基於macro-based實現的min,max進行認真的分析,對照基於模板的min,max實現,他得到了如下結論:
“對於實現max,什麼是最好的方法?”用一句Tevye的名言:“我將告訴你:我不知道”,基於以上分析,我有信心告訴大家宏實現的方法,也許是最好的,不過,我個人很討厭:宏。因此,大家如果有什麼更好的實現方法,請趕快告訴我。
根據我個人的知識,在今天這個時候,這個挑戰依然存在,在這篇文章中,我將面對這個挑戰,但是,在我開始之前,讓我們來回憶一下以前泛型編程,是如何糟糕實現這個算法的。
自從"Generic<Programming>: volatile - Multithreaded Programmer''s Best Friend" 發表後,我接到很多郵件。在這些郵件中,我接到很多的嘉獎,同時,也有很多對the Usenet newsgroups comp.lang.c++新聞組和 comp.programming.threads的抱怨。這個討論比較激烈,如果,你有興趣,你可以去參加。標題是:"volatile, was: memory visibility between threads."一個討論。
在這些討論中,我覺得,我學到很多的知識,在這裡很多獨立的例子,好了,長話短說,很多系統不會修改volatile 數據(例如在POSIX編譯系統中),同時在另外的一些系統中,加入volatile量,也無助於程序的質量提高,關於volatile mutexes正確使用的最重要問題,是它依賴於類似於posix mutexes,同時,在多進程系統中,這種互斥量往往是不夠的,因此,你必須使用內存保護。
另外一個更具有哲學意義的問題是,嚴格的講,volatile規則遠離變量是不合理的,就算你添加的volatile符合你應用volatile的規則。
一個系統能存儲volatile數據在不同的位置,不像non-volatile數據那樣,因此,固定其存儲地址將使得系統不穩定。volatile的正確性也是另外一個被批評的對象,盡管它可以在低水平的race condition,但是,不能在更高的層次檢測邏輯上的race condition。例如,你有一個像std::vector的類mt_vector,同時還有一些同步的成員函數。
如下:
volatile mt_vector<int> vec;
...
if (!vec.empty()) {
vec.pop_back();
}
原來的想法是從容器vector中移走最後一個元素,在單線程環境下,這段代碼會運行的很好,但是,如果你在多線程使用這樣的代碼,往往會出現異常的,就算你的empty(),pop_bock()已經適當的同步了。因此,可以說,低層次的數據一致性得到保護,但是,更高水平的數據操作,往往是不穩定的。
至少,根據以上的分析,我開始堅持我的求證volatile方法,這是個有效檢測在類似posix互斥量race conditions的好方法。但是,如果你從事多進程系統的內存存取權限安排的話,那麼,你首先應該細聽你的編譯器文檔資料。你應該知道你該干什麼。Kenneth Chiu 提到一篇很有趣的論文: http://theory.stanford.edu/~freunds/race.ps, pape講解了 "Type-Based Race Detection for Java."
由於Java類型系統只有很少的限制條件,這就使得編譯器有可能和程序員一起來在編譯時刻檢測race conditions。
Min 和Max
來讓我們再看看Scott提出的挑戰,基於宏定義的min()如下:
#define min(a, b) ((a) < (b) ? (a) : (b))
由於它是完全范型的,因此,它常常能很好的發揮作用。(只要這個表達式中 < 、?是有定義的)很不幸的是min()總是要計算它中間一個參數兩次,這樣,就導致很多令人困惑的問題。(譯注:假如如下調用,a=3,b=5 int c=min(a++,b);結果我們會得到c=4的結果。顯然不是我們所希望的)宏定義只是在表面形式上跟函數相像,但是,它的行為往往跟函數兩個樣。(如果你想得到更多的相關知識,那麼,你可以查閱Herb的有關這方面的資料)在C++標准庫中有一個更加有效的實現,它是基於模板的解決方案,如下:
const T& min(const T& lhs, const T& rhs)
{
return lhs < rhs ? lhs : rhs;
}
你可以看出這個方案中參數和返回值都是const型,這也就導致一個問題。也許,你想把兩個數值中小的那個的數值加2,那麼,你也許會這麼寫:
double a, b;
...
min(a, b) += 2;
基於宏定義min()就不會出現問題,但是,如果是模板基於模板上實現就不會那麼聽話了。因為你是不能改變const變量的值的,如同Scott所注意的一樣。我們來更新我們的實現:
T& min(T& lhs, T& rhs)
{
return lhs < rhs ? lhs : rhs;
}
但是,這樣還是不能令人滿意。因為編譯器不能處理混合類型-一個參數是const,一個是non-const。
例如:
int a;
short int b;
...
int smallest =min(a,b);// error: can''t figure out T
// in template instantiation
不用說,基於宏的min()在這樣的情況下又一次表現良好。基於模板的min(),必須這樣寫:
int smallest = min(a, int(b)); // aha, T is int
要想提供一個好的min/max,我們首先,要使得行為很像macros,同時,要避免macros帶來的麻煩。
下面是一個聰明實現的開始:
emplate <class L, class R>
class MinResult {
L& lhs_;
R& rhs_;
public:
operator L&() { return lhs_ < rhs_ ? lhs_ : rhs_; }
operator R&() { return lhs_ < rhs_ ? lhs_ : rhs_; }
MinResult(L& lhs, R& rhs) : lhs_(lhs), rhs_(rhs) {}
};
template <class LR>
class MinResult<LR, LR> {
LR& lhs_;
LR& rhs_;
public:
operator LR() { return lhs_ < rhs_ ? lhs_ : rhs_; }
MinResult(LR& lhs, LR& rhs) : lhs_(lhs), rhs_(rhs) {}
};
template <class L, class R>
MinResult min(L lhs, R rhs)
{
return MinResult(lhs, rhs);
}
為了把兩個轉換符統一為一個,在MinResult<LR,LR>要進行一些特殊局部處理。不然,L&,R&操作會被認為重復定義。這個方案推遲了計算,直到它需要的時候,同時,在結果取出之前,它表現了良好的惰性原則。例如:
int a, b;
...
min(a, b);
實際上,上面的代碼,沒有做別的事情,同時,如果,你對類型有很多的思考的話,那麼這樣的代碼將使得你對森林中的樹的概念有著更多的思考。另一方面,如果,你這麼使用:
int c = min (a,b);
那麼,編譯器將調用int&來轉化min返回值(MinResult<int,int>)的臨時變量。這個強制轉化能夠正確的工作,返回無誤的結果。
盡管你能夠解決完美的const相關問題了,但是MinResult還不是一個完美的方案。我們考慮一下下面的代碼:
int a;
short int b;
extern Fun(int);
extern Fun(short int);
...
Fun(min(a, b)); // error! Don''t know which overload to invoke!
MinResult<int,short int>支持兩種轉化:int&,short int&,結果是,編譯器可以調用平等的調用任何一個fun()函數,在這一方面,基於宏的min()又順利通過,它將調用像你想象的一樣Fun(int)函數。
Quest for a Type
那麼怎樣才能天才般的解決這個問題。假設有兩個類型L和R,考慮min(L,R)返回值的正確類型應該...。例如:L是char,R是int,毫無疑問,應該返回int。這樣,我們可以清晰的為min()定義四個函數。
template <class L, class R>
MINTYPE(L, R)
Min(L& lhs, R& rhs)
{ return lhs < rhs ? lhs : rhs; }
template <class L, class R>
MINTYPE(const L, R)
Min(const L& lhs, R& rhs)
{ return lhs < rhs ? lhs : rhs; }
template <class L, class R>
MINTYPE(L, const R)
Min(L& lhs, const R& rhs)
{ return lhs < rhs ? lhs : rhs; }
template <class L, class R>
MINTYPE(const L, const R)
Min(const L& lhs, const R& rhs)
{ return lhs < rhs ? lhs : rhs; }
這四個重載的函數對應著在const和non-const之間四個可能的組合。這個主意不錯,但是,我們該怎樣來定義MINTYPE?我們知道有一個對應的技術traits,的確我們可以使用traits來完成MINTYPE比如:
#define MINTYPE(L, R) typename MinTraits<L, R>::Result
template <class L, class R> struct MinTraits;
// Specialization for the L == R case
template <class LR> struct MinTraits<LR, LR> {
typedef LR& Result;
};
// Specialization for bool and char
template <> struct MinTraits<bool, char> {
typedef char Result;
};
...
這樣實現不錯,但是你將不得不寫很多糟糕的代碼。由於有14數據類型,因此,你將不得不為了每一個組合編MinTraits。其實,你可以簡化這個工作,就像macros一樣,但是,這不是一個很完美的方案。
這個方案還沒有完成,你必須考慮到用戶定義的類。
另外,怎樣為基類,子類調用Min()函數,看看下面的例子:
class Shape {
...
unsigned int Area() = 0;
};
bool operator<(const Shape& lhs, const Shape& rhs) {
return lhs.Area() < rhs.Area();
}
class Rectangle : public Shape { ... };
void Hatch(Shape& shape)
{
Rectangle frame;
...
Shape& smallest = Min(shape, frame);
... use smallest ...
}
事實上, 如果,min()處理Rectangle那麼它將返回一個Shape的引用,這樣就不好了。由於Rectangle會自動轉化為Shape引用類型,這樣,我們必須做很多事情。
同時,在上例中,我們也可以看出基於macro的min()函數也不會正常發揮作用,看下例子:
shape < frame ? shape : frame
它將兩個參數轉化為同樣的類型,所以它等同於
shape < frame ? shape :(Shape) frame
顯然,這不是我們所希望的(這種糟糕的行為叫slicing)
在這篇文章中實現的min(),能給予你像macro-based版本一樣的min()功能,甚至更多。同時,它的實現大概只有80行(包括Max)
我們再溫熱一下我們的coffee,同時,開始討論Loki。這些代碼使用了Loki,Loki提高比較先進的符號操作,Min()用到的Loki工具有:
1 Typelists Typelists提供正規的Lists,它存儲類型,不存儲數值。
定義如下:
typedef TYPELIST_3(float, double, long double) FloatingPointTypes;
FloatingPointTypes 中包含三個類型
假設定義一個typelist 變量FloatingPointTypes,和任意類型 T,你可以通過使用編譯時刻算法 Loki::TL::IndexOf察覺T是否在 FloatingPointTypes 。
例如:
Loki::TL::IndexOf<FloatingPointTypes, double>::value
將返回1,如果 T不在這個list中返回-1。
2。Select Class Template 它的完全說明在參考 7中,簡單的說,Select允許你選擇一兩個基於編譯時刻的Boolean 常量。例如:
typedef Loki::Select<sizeof(wchar_t) <
sizeof(short int), wchar_t, short int>::Result
SmallInt;
SmallInt將是wchar_t,short int 中最小的整數類型。
3 TypeTraits 這是一個模板類它是用來推斷類型的,例如判斷是否這個類型是指針,這個指針指向什麼變量等等。我們在這之使用了NonConstType TypeTraits<T>::NonConstType 是用來轉化掉const。
4 Conversion 在參考7 中有它的詳細說明。
這個類可以用來檢測是不是一個一類型可以隱轉化為另外的一個類型。Conversion是整個實現的基礎。
The MinMaxTraits Class Template
為了簡化類型的計算,我建立了一個簡單的線性types結構。首先我把所以types以固定的順序放入。同時,我假定返回值的類型將是list中靠近底部的類型。定義如下:(不考慮const)
namespace Private
本身,unsigned types 應該在signed types後面, float 應該int 後面。例如
{
typedef TYPELIST_14(
const bool,
const char,
const signed char,
const unsigned char,
const wchar_t,
const short int,
const unsigned short int,
const int,
const unsigned int,
const long int,
const unsigned long int,
const float,
const double,
const long double)
ArithTypes;
}
Min(long,double)那麼返回結果應該是double類型,因為在list中double在long後面(譯者:我們知道結果也應該是double)
現在,一般的算法就可以計算Min()的結果類型了。如果你的參數類型為不是引用的T,R具有如下條件:
1 假設 結果為R。
2 如果R可以隱轉化為L,那麼可以把結果轉化為L型。
3 如果在Private::ArithTypes中R在L的後面,那麼,轉化結果為R型,這一步要處理很多的數學轉化。
4 如果在不要引入臨時變量的情況下,L&可以自動的轉化為R&,那麼結果轉化為R&,這一步確保Min(frame,shape)將返回Shape&型。
5 如果在不要引入臨時變量的情況下,R&可以自動的轉化為L&,那麼結果轉化為L&,
這一步確保Min(shape,frame)返回Shape&.
在這中間罪艱難的一步是“不引入臨時變量的類型間的轉化”。本質如果not-const T的引用可以轉化為not-const U的引用的話,那麼,T可以在引入臨時變量的情況下,轉化為U。
The Min and Max Overloads
對應於const與not-const的參數的四個不同組合,這裡有四個Min(),Max()函數重載了。為了避免在Shape/Rectangle例子中的Slicing問題,Min()有個不同於a<b?a:b的實現。如下:
template <class L, class R>
這兩個返回語句確保正確的轉化而且沒有slicing現象。這四個重載函數可以處理如下的情況:Min(a +b, c + d) 、 Min(a +b, 5)。
typename MinMaxTraits<L, R>::Result
Min(L& lhs, R& rhs)
{ if (lhs < rhs) return lhs; return rhs; }
template <class L, class R>
typename MinMaxTraits<const L, R>::Result
Min(const L& lhs, R& rhs)
{ if (lhs < rhs) return lhs; return rhs; }
... two more overloads ...
.. similar Max implementation ...
分析:
讓我看看這個新的實現是怎樣滿足Scott Meyers''先生的要求的。他的要求如下:
1 函數在語意上要進行類型檢查。明顯Min()滿足。
2 支持const ,non-const 參數。由於重載了四個函數。Min()支持non-const const的參數的任何組合。
3 支持不同參數類型。Min()很顯然是支持不同的類型的,它做了大量的智能工作,這是macro和簡單模板所無法完成的。Min()消除了不同類型之間的差別。主動的進行類型轉化。這個轉化過程是掌握在library編寫人的手中。
4 不需要強制實例化,Min()不需要。
在不同的指針的情況下(Shape* ,Rectangle*)Min()也能表現的很好。
這是由於算法的第一步所實現的。
Min()的一個重要的特征是它可以一個算法來推斷結果類型,而這個算法是你可以編寫的。
如果你發現這個算法有什麼不滿意的地方。你可以自行修改。
很遺憾的是這個實現在一些編譯器上無法通過,我知道代碼是沒有問題的。如果你有什麼好的建議請聯系我。
Look Ahead in Anger
這些天我在看 The Age of Spiritual Machines by Ray Kurzweil [8]. Kurzweil 說充分證明,在2020左右你將用不到$1000買到一個有人腦一樣的機器。在那個時候人們可能再說“你看,在2001年,那些家伙在實現max()還那麼困難,一個家伙還特此寫了一篇論文,”
min()不重要嗎,我不這麼認為,最大化,最小化是數學上,現實生活中兩個簡單的概念。如果,程序語言不能表達這樣簡單的概念,那麼說明在語言的深處,還存在問題。
也許有小孩說“媽媽我不想學語法和字匯,我只想學寫詩”這樣能成功嗎?
如果你寫了一個a < b ? a : b,也許你還想它能夠處理表達式,它能實現嗎?
那麼,這裡存在一些錯誤?不只有C++這樣,在Java和C#中也如此,因為你知道min和max不是objects。
感謝:
感謝所有參與者,希望大家的支持。
參考文獻:
[1] http://aristeia.com/Papers/C++ReportColumns/jan95.pdf
[2] http://cuj.com/experts/1902/alexandr.html
[3] http://www.gotw.ca/gotw/077.htm
[4] A. Alexandrescu. "Traits: the else-if-then of Types," C++ Report, April 2000.
[5] A. Alexandrescu. Modern C++ Design (Addison-Wesley Longman, 2001).
[6] J. Vlissides and A. Alexandrescu. "To Code or Not to Code," C++ Report, March 2000.
[7] A. Alexandrescu. "Generic<Programming>: Mappings between Types and Values," C/C++ Users Journal Experts Forum, September 2000, http://www.cuj.com/experts/1810/alexandr.html.
[8] R. Kurzweil. The Age of Spiritual Machines: When Computers Exceed Human Intelligence (Penguin USA, 2000).
Andrei Alexandrescu is a Development Manager at RealNetworks Inc. (www.realnetworks.com), based in Seattle, WA, and author of the acclaimed book Modern C++ Design. He may be contacted at http://www.moderncppdesign.com/. Andrei is also one of the featured instructors of The C++ Seminar (www.gotw.ca/cpp_seminar).
譯者:感謝所以浏覽本文的人,希望大家批評指教。