在 MSVC10 下,將 lambda expression 轉換成 C 的 function pointer

| | 0 Comments| 17:31
Categories:

之前已經有在《C 0x:Lambda expression》一文中,介紹過 C 11 / C 0x 這個算是滿好用的匿名函式、lambda expression 了~透過 lambda expression 可以很快地建立一個 function object,而不用另外宣告一個真正的函式,在很多地方,可以有效地簡化程式的寫法。

不過,在 Visual C 2010(VC10)的環境下,其實 lambda expression 有一些不符合標準的小問題…那就是他不能轉換成 C 的 function pointer(請參考《在 C 裡傳遞、儲存函式 Part 1:Function Pointer》)、拿來註冊成 callback function。

下面是一個簡單的範例:

#include <iostream>
using namespace std;

void CallFunctionPointer( void(*pFunc)() )
{
(*pFunc)();
}

void Do()
{
cout << "general function" << endl;
}

int main( int argc, char** argv )
{
CallFunctionPointer( Do );
CallFunctionPointer( [](){ cout << "lambda" << endl; } );
}

這個簡單的範例裡面,有一個名為 CallFunctionPointer() 的函式,他會接受一個 function pointer、並執行他。

main() 裡面的 CallFunctionPointer( Do ); 這行程式碼,就是把 Do 這個全域函式當作參數傳給 CallFunctionPointer()、讓他來執行;這樣形式的用法,其實在 C 的函式庫上相當常見,像是常常搭配 OpenGL 使用的 glut(參考、目前建議用 freeglut 就是了),就是透過 function pointer 的形式、來指定 callback function,藉此做到事件的觸發與處理。

而接下來的 CallFunctionPointer( [](){ cout << "lambda" << endl; } ); 則就是把一個 lambda expression 當作參數丟給 CallFunctionPointer() 來執行。

理論上這樣的程式碼是合法的(註 3),而在 gcc 或是還沒推出的 VC11 上也是可以正確編譯、執行的。不過在 VC10 的話,編譯這段程式碼,則是會出現 Error C2664 的的錯誤,他的描述如下:

cannot convert parameter 1 from '`anonymous-namespace'::<lambda0>' to 'void (__cdecl *)(void)

也就是說,VC10 沒辦法把 lambda expression 轉換成所需要的 function pointer 的形式…這個問題在 Microsoft Connect 上也已經有人回報了(連結),微軟的說法是會在下一個版本(Visual Studio 11)時修正。


解決方法的實作

那如果現在希望可以解決的話,要怎麼辦呢?在《Fixing Lambda expressions in Visual Studio 2010》這篇文章裡面,提供了一種透過 Template metaprogramming 的機制來做封包,藉此把 lambda expression 轉換為可以被 VC10 當作 function pointer 的一般 function。

他的方法的基本概念,就是透過根據傳入的 lambda expressiob 來產生一個特別的 struct 或 class,然後透過裡面的 static member function 和 static member data 來做操作。而接下來的程式碼,就是 Heresy 根據文章中的方法,針對 void func() 這種不需要參數、也沒有回傳值的函式,稍作修改後實作出來的結果。

首先,是最主要的 template class:LambdaWrapper

// the class to wrap a lambda expression
template<typename TLambda>
class LambdaWrapper
{
public:
static TLambda* pFuncPtr;

static void Exec()
{
(*pFuncPtr)();
}
};

// instantiate the static member data
template<typename TLambda>
TLambda* LambdaWrapper<TLambda>::pFuncPtr = NULL;

它的成員都是 static 的,只有兩個東西:

  • 用來記錄 lambda expression 的指標、也就是 static member data pFuncPtr
  • 拿來當 function pointer / callback function 用的 static member function Exec()
    他所做的事就是去執行 pFuncPtr 這個指標所指到的 lambda experession。

而由於有 static 的 member data pFuncPtr,所以也需要 global scope 產生他的實例、並進行初始化。

最後,則是 Convert() 這個直接輸入 lambda expression、取得 function pointer 的 template 函式了~

// define the type of function pointer
typedef void(*FunctionType)();

// get function pointer from lambda expression
template<typename TLambda>
FunctionType Convert( TLambda rFunc )
{
static TLambda lf( rFunc );
LambdaWrapper<TLambda>::pFuncPtr = &lf;
return &LambdaWrapper<TLambda>::Exec;
}

首先是先透過 typedef 定義 FunctionType、也就是要回傳的 function pointer 的型別。而在 Convert() 裡面的第一個動作,就是先建立一個 static 變數 lf、將傳進來的 lambda expression rFunc 複製一份;而由於 lf 是 static 的,所以會一直存在、不會在離開 Convert() 這個函式時被釋放掉。(註 1)

而接下來,則是去設定 LambdaWrapper<TLambda>::pFuncPtr 的值、讓他指到剛剛建立出來的 lf;然後則是把 LambdaWrapper<TLambda>::Exec() 這個 static member function 傳回來,當作最後的 Function pointer 來用。

如此一來,要使用的話就非常簡單了~只要下面這樣呼叫就可以了!

CallFunctionPointer( Convert( [](){ cout << "lambda" << endl; } ) );

這樣的寫法,CallFunctionPointer 這個函式就會去執行傳進來的 function pointer、也就是 LambdaWrapper<TLambda>::Exec() 這個函式,然後執行我們所指定的 lambda expression 了~

所以這樣在把上面的程式寫好後,在使用上是非常方便的~像是 glut 這類本來需要 global function 或 static member function 的介面,都可以透過 lambda expression 來作封包,可以寫得更物件導向了~

不過,由於 LambdaWrapper 這樣的 template class 還是只能針對特定形式的 function pointer 來做轉換,像這邊的例子就只能針對 void() 的形式,不能用在其他形式的 function pointer;所以如果要對應到不同類型的 function pointer,也就需要寫不同的類別出來了…這點也算是比較討厭的地方。


稍微詳細一點的解釋

這個方法主要是透過 class 的 static member data 來紀錄要執行的 function、然後透過 static member function 來當作呼叫的介面;但是 class 裡面的 static member data 基本上是共用的,這邊這樣設計,重複使用的時候難道不會在後面呼叫 Convert() 時被覆蓋掉嗎?(註 2)

實際上,這個方法之所以可以這樣用,主要是因為編譯器在處理的時候,每一個 lambda expression 的型別都是不同的!下面就是一個簡單的測試例子:

#include <iostream>
#include <typeinfo>
using namespace std;

int main( int argc, char** argv )
{
auto f1 = [](){};
auto f2 = [](){};
cout << "f1 is [" << typeid( f1 ).name() << "]" << endl;
cout << "f2 is [" << typeid( f2 ).name() << "]" << endl;
}

以上面的程式碼來說,f1f2 這兩個 lambda expression 雖然是完全一樣的,但是實際上在執行的時候,所輸出的結果會是:

f1 is [class `anonymous namespace'::<lambda0>]
f2 is [class `anonymous namespace'::<lambda1>]

可以發現,兩者其實是不同的(雖然只差在編號)。

而也由於每一個 lambda expression 都是不同的,所以透過 lambda expression 來產生出來的 template class:LambdaWrapper<TLambda>,實際上也都會是不同型別的!所以針對不同的 lambda expression、實際上並非使用同一個 pFuncPtr、而是各自擁有一份。

而同樣的,Convert() 這個 template 函式,也是針對不同的 lambda expression、會在編譯階段、產生不同的 function 實體,而裡面用來複製、保存 lambda expression 的 static 變數 lf,也都是不一樣的~

而如果這邊改用 std::function 這種 function object 的話,就會因為型別相同、而出現問題了~像下面的例子,就是同時展示使用 lambda expression 和 function object 的使用:

auto fa = Convert( [](){ cout << "lambda a" << endl; } );
auto fb = Convert( [](){ cout << "lambda b" << endl; } );

function<void()> fo1 = [](){ cout << "function object 1" << endl; };
function<void()> fo2 = [](){ cout << "function object 2" << endl; };
auto f1 = Convert( fo1 );
auto f2 = Convert( fo2 );

(*fa)();
(*fb)();
(*f1)();
(*f2)();

在上面的程式碼中,是先透過 Convert() 把兩個 lambda expression 個別產生對應的 function pointer fafb;而接下來則是先把 lambda expression 轉型成 STL 的 function<void()> 形式的 function object fo1fo2,然後再透過 Convert()、產生對應的 function pointer f1f2

最後都準備就緒後,則是依序執行這些被產生出來的 function pointer。而執行的結果,會是:

lambda a
lambda b
function object 1
function object 1

可以發現,直接使用 lambda expression 的話,結果是正確、沒有問題的~但是如果是使用 function object 的物件的話,Convert()LambdaWrapper 都會因為丟進來的參數型別是相同的(都是 function<void()>),而產使用同一個函式/類別,導致 static 的變數實際上是用到同一份、而因此有錯誤的結果。

如果在偵錯模式下實際下去看的話,也會發現其實 f1f2 這兩個指標,實際上都是指到同一個位址,也就是 LambdaWrapper<function<void()>::Exec() 這個函式;而此時,LambdaWrapper<function<void()> 裡的 pFuncPtr,則是指到第一次傳進 Convert< function<void()> >() 這個函式的 function object、fo1 的複本、也就是 Convert< function<void()> >() 裡的 static 變數 lf

所以理所當然的,f1f2 的執行結果會是相同的,都是去呼叫到 fo1 這個 function object 的副本、輸出「function object 1」的字樣。


附註:

  1. 在《Fixing Lambda expressions in Visual Studio 2010》這篇文章裡面的作法,是沒有去建立傳入的 lambdaexpression、也就是 rFunc 的副本,而是讓 pointer 直接去指到這個在外部的 lambda expression。
    Heresy 沒有這樣做、而是另外去建立一份 lambda expression 的副本的原因,是怕外部的 lambda expression 會隨著生命週期到了而消失,這時候可能在執行時會有問題。

    FunctionType Gen( int i )
    {
    auto f = [i](){ cout << "lambda " << i << endl; };
    return Convert( f );
    }

    int main( int argc, char** argv )
    {
    auto fp = Gen1( 10 );
    (*fp)();
    }

    像上面的程式碼在 Heresy 給的程式裡,是可以正確執行的,但是如果把 Convert() 這個函式改成不透過 static 變數來複製傳進來的 lambda expression 的話:

    template<typename TLambda>
    FunctionType Convert( TLambda& rFunc )
    {
    LambdaWrapper<TLambda>::pFuncPtr = &rFunc;
    return &LambdaWrapper<TLambda>::Exec;
    }

    執行的時候所呼叫的 pFunctPtr 會是指到一個已經被釋放掉的記憶體空間;而 Heresy 測試時雖然還是可以跑,但是 lambda expression 裡面的 i 的數值已經亂掉了。

  2. 不過實際上,LambdaWrapper<TLambda>::pFuncPtr 是指到 Convert() 這個 template 函式裡面的 static 變數 lf,而 lf 只有在第一次執行時會被建立、用來複製第一次傳進來的 lambda expression,所以其實不會有覆蓋的問題,反而是會變成是被第一次執行時的參數給獨佔。

  3. Lambda Expression 只有在沒有 capture 任何變數的時候可以直接轉換成 function pointer,實際上這個時候它會被當成是類似 global function 的形式;如果有做變數的 capture 的話,就變變成類似 class 的 member fucntion 的形式、而無法轉換成上述的 function pointer。例如下面的例子,在 gcc 和 vc11 上都會出現編譯錯誤:

    #include <iostream>
    using namespace std;

    void CallFunctionPointer( void(*pFunc)() )
    {
    (*pFunc)();
    }

    int main( int argc, char** argv )
    {
    int x;
    CallFunctionPointer( [x](){ cout << "lambda" << endl; } );
    }

    這邊可以參考《Lambda Functions in C 11 – the Definitive Guide》的「A Note About Function Pointers」。

    而如果是遇到這個問題的話,也可以用這篇文章的方法來繞過去。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *