最近為了port一段小程式,竟然被幾個小問題給卡了大半天,有點尷尬的是這些都是 n 年前就 well known 的問題了。

第一個問題有點囧,那就是ISO C++當中竟然沒有 copy_if 和 copy_n,而且我最近才知道。(更精確的說,我一直都知道手邊有些STL實作沒有copy_if 和 copy_n,但最近才確認這是標準)

不要笑!當然我有一些藉口。因為我開始學STL的時間比較早,那時候參考的 SGI manual 明明就有。並且我常使用的 STL 實作之一確實就有 copy_if 和 copy_n,所以我很容易把其他我常用、但沒有此二者的STL實作當成是「不合標準」的實作。再說STL當中有很多類似的idiom,所以有這樣的想法應該是自然而然才對。

我必須說 copy_if 和 copy_n 雖然寫起來五、六行就可以搞定,不過如果程式庫裡面內建不是更方便嗎?這一定是ISO C++委員會的錯誤。

另一個小小的意外是,下面是某些教科書示範計算有多少個英文字母、大寫轉小寫的方法,在VC是沒問題的,但是gcc底下「很容易」編譯失敗

string s = "You're doing it WRONG!";
int n = count_if(s.begin(), s.end(), isalpha);
transform(s.begin(), s.end(), s.begin(), tolower);

很多人的第一個想法應該是古時候 isalpha 和 tolower 常常用 macro 實作,所以會面臨macro展開出怪東西的窘境。不過我知道這不是問題,因為只要確定引用<cctype>標頭檔,就一定會得到真正的函數,而非古老 <ctype.h> 所流傳下來的macro版。

真正的問題在於 std namespace 底下實際上有多個 tolower 函數的 overload,其他 <cctype> 底下的函數也都是。其中之一是位於<cctype>當中的int (int)版本,另外則是<locale>底下的function template。如果引用了<locale>,那麼 transform 就會不知道該使用<cctype>當中的 tolower 還是<locale>底下的 tolower。

雖然大部分的人根本就不知道<locale>到底能幹嘛,也不會想去引用,不過<iostream>會引用。若是沒有直接、間接含入<locale>,那麼上面的程式碼是不會有問題的。但<iostream>畢竟還是很常用的header,所以還是得有其他的解決方案才好。這是我實驗過在gcc下可用的方式:

1.利用namespace區隔。

transform(s.begin(), s.end(), s.begin(), ::tolower);

這裡用::運算符取得的最global的範圍,所以不會和 std 下的東西搞混。不過這個作法最大的缺點是,他抓到的是傳統<ctype.h>的版本,如前所述,在其他編譯器上很可能是macro而非函數,所以根本得不到函數,只會得到macro展開的怪東西。

 

2.指名型態

transform(s.begin(), s.end(), s.begin(), (int(*)(int)) std::tolower);

缺點是看起來很倒胃口。

 

3.自己寫函數包裝tolower。這可能是最完善的做法。

 


 

追根究柢,問題還是出在 gcc 的 overload 解析。下面的程式碼在VC編譯不會有任何問題,但是gcc不給過:

int F(int n) {return n;}
template <class T>
T F(T n) {return n;}
template <class Fn>
void Ftest(Fn f) {}
int main()
{
    Ftest(F);
}

我根本懶得去確定 ISO 如何規定這類解析規則,根據我的經驗,這種東西即使規定了也完全不能依賴,各編譯器的作法可能有所出入。如果一般人寫出這樣的程式碼,根本是和自己的睡眠時間以及休閒生活過不去。

不過我還是有點好奇,gcc和VC誰才是所謂「正確」的實作呢?我猜是VC啦。有沒有善心人士能證實一下

novus 發表在 痞客邦 PIXNET 留言(3) 人氣()


留言列表 (3)

發表留言
  • damody
  • 個人我覺得都算正確吧?像cmath中的三角函數,
    常常就會有輸入常數後,有多種配對的問題,
    然後在錯誤訊息問請用float好嗎?之類的。
    不過這個文章的問題我在mingw下遇不到耶= =
  • 我想你沒有真正了解這個問題的關鍵,也沒有實際執行上面的程式碼。

    這和cmath的情形有點不同
    cmath相對來說比較簡單,C++有一套計算隱式轉型"距離"的明確規則,所以有辦法決定距離最近的一個overload。
    (可參考我去年寫的 http://tw.knowledge.yahoo.com/question/question?qid=1608043005237)

    在這個例子裡,關鍵在於取overload function的指標。
    編譯器理論上沒辦法從呼叫的引數決定使用哪一個overload,當然這不是甚麼大問題,我們都知道明確指名型態是可行的方案。

    但VC竟然不需要明確指名型態,這就表示好像有某種推導規則可用

    總之如果有空的話去mingw或其他平台的g++下跑跑最下面那段程式,你就會明白了。

    novus 於 2009/11/10 13:07 回覆

  • damody
  • mingw 3.4.5 下我有試過了,0 errors, 1 warnings
    warning是因為n沒有用到。
    我的程式是:
    #include <iostream>
    #include <cctype>
    #include <locale>

    int main()
    {
    std::string s = "You're doing it WRONG!";
    int n = count_if(s.begin(), s.end(), isalpha);
    transform(s.begin(), s.end(), s.begin(), tolower);
    return 0;
    }
  • 這當然會過
    因為你用的是我文中提到的 解法1:利用namespace區隔

    只是我上面的例子是全檔預設在using namespace std;的情境下
    而你是預設在global space下
    不過記得 transform 和 count_if 前面要加 std::
    有些編譯器可能會有向後相容的顧慮讓你過,但有些則否

    你應該要試的是
    int F(int n) {return n;}

    template
    T F(T n) {return n;}

    template
    void Ftest(Fn f) {}

    int main()
    {
    Ftest(F);
    }

    後來我想想VC能編譯過還挺奇怪的

    novus 於 2009/11/10 21:43 回覆

  • damody
  • sorry誤解了,原來你說的是全域函數與std空間函數的衝突。
  • 比較精確的答案是
    std下有多個同名函數會互相衝突
    但 global space 只有一個,
    所以不會衝突

    novus 於 2009/11/11 11:50 回覆