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


之前已經有在《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」。

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

關於 Heresy
https://kheresy.wordpress.com

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

  1. 引用通告: 微軟推出 Windows 8 RP 以及 Visual Studio 2012 RC | Heresy's Space

  2. zarda 說道:

    在VS2012上就沒這問題了
    不過
    如果搭配function pointer array是很好用的
    如:

    int main(int argc, char * argv[]) {

    typedef void(*func_ptr)();
    func_ptr func_arr[5];

    func_arr[0] = ( [](){ cout << "lambda0" << endl; } );
    func_arr[1] = ( [](){ cout << "lambda1" << endl; } );
    func_arr[2] = ( [](){ cout << "lambda2" << endl; } );

    for(int i=0; i<9; ++i)
    func_arr[i%3]();

    return true;
    }

    喜歡

    • Heresy 說道:

      對,這在 VC11 已經修正了。
      不過目前還有很多東西不支援 VC11 orz

      喜歡

      • littlewater 說道:

        最大的问题还是VS2012目前还没有办法支持XP以前的系统……为什么……虽然有绕圈子的做法但是还是期待官方最后兑现诺言支持XP- -

        喜歡

        • Heresy 說道:

          VC11 已經有支援了喔~
          只要安裝 Visual Studio 2012 Update 1,就會有「V110_xp」這個 toolset 可以用了
          http://blogs.msdn.com/b/vcblog/archive/2012/11/26/visual-studio-2012-update-1-now-available.aspx

          之前 CTP 的介紹
          https://kheresy.wordpress.com/2012/11/03/2-visual-studio-2012-ctp-update/

          喜歡

          • littlewater 說道:

            原来已经有了?!赶紧下去了!感谢提醒^^

            喜歡

          • littlewater 說道:

            感谢,已经安装好了^^,家里没有XP环境,等到上班的时候去试试了!

            喜歡

          • Heresy 說道:

            等你回報測試結果了 :)

            喜歡

          • littlewater 說道:

            已经测试成功了= =,就是需要在Toolset里面选择xp support,但是不太明白为什么这个项目不作为默认选项呢- -,也许XP支持就会降低运行性能了吧

            喜歡

        • Heresy 說道:

          沒有設定成為預設的原因,基本上應該是為了相容 Windows XP 這類的舊系統,就得犧牲一些新功能。
          這部分在之前微軟發布 CTP 的時候有提到,像是 C++AMP 就不能在 V110_xp 下使用。

          喜歡

          • littlewater 說道:

            早先也有折腾过AMP,感觉是一个很C++的GPU环境^^,不过好像很多资料里面都比较旧了,不知道有没有什么地方有系统介绍,例如 restrict(direct3d)第一次接触的时候已经无效,都改为restrict(amp)了要……

            喜歡

          • Heresy 說道:

            他的全名是 C++ AMP,不是 AMP;而當然,他就是針對 C++ 來做延伸的了。
            以微軟的東西來說,通常 MSDN 上的資料就相當充分了
            http://msdn.microsoft.com/en-us/library/hh265137.aspx

            喜歡

      • zarda 說道:

        但其實我現在又遇到另一個問題
        就是我上面提供的程式中
        如果用
        func_arr[0] = ( [](){ cout << "lambda0" << endl; } );
        如此指派一個新的function時 是無法傳值的
        當改成
        func_arr[0] = ( [](int i){ cout << i << endl; } );
        時是無法編譯過的會出現
        error C2440: '=' : cannot convert from 'main::’ to ‘func_ptr ‘
        這很有可能又是lambda指標無法轉型
        不知Heresy大 有何解

        喜歡

        • zarda 說道:

          找到解法了
          int main(int argc, char * argv[]) {

          typedef void(*func_ptr)(int n);
          func_ptr func_arr[5];

          for(int n, n<3, ++n)
          func_arr[n] = ( [=](int n){ cout << "lambda" << n << endl; } );

          for(int i=0; i<9; ++i)
          func_arr[i%3](i%3);

          return true;
          }
          自動產生functor
          再自動呼叫

          喜歡

          • zarda 說道:

            再改一下
            #include
            #include
            #include
            int main(int argc, char * argv[]) {

            array<function, 5> func_arr;

            for(int n, n<3, ++n)
            func_arr[n] = ( [=](int i){ cout << "func(" << n << ")= "<< i << endl; } );

            for(int i=0; i<9; ++i)
            func_arr[i%3](i);

            return true;
            }
            還是用安全多

            喜歡

          • littlewater 說道:

            其实也可以考虑使用标准库的std::function(在中),只需要将你的func类别传送给这个函数,就可以接管同类别的所有函数了。但是必须注意这里func不是func_ptr而必须是函数类型,即应该是typedef void func(int n);否则会意料外无法编译通过。

            喜歡

          • Heresy 說道:

            如果可以,個人也比較建議用 std::function 來取代 function pointer
            建議可以參考: https://kheresy.wordpress.com/2010/11/12/function_object_tr1/

            喜歡

  3. 引用通告: 微軟推出 Windows 8 RP 以及 Visual Studio 2012 RC « Heresy's Space

  4. littlewater 說道:

    思路和早先看到的差不多,不过这个方案比较适合于扩展,而且相较于将会被标准化转换,这个方法可以支持依赖项参数的传递,很不错=w=

    喜歡

    • Heresy 說道:

      可以試試看啦~
      不過實際上在使用上還是有一些限制。
      因為這東西的機制是在 compile time 的時候去做的,所以基本上大概沒辦法把他寫到函式庫裡面…

      喜歡

發表迴響

在下方填入你的資料或按右方圖示以社群網站登入:

WordPress.com Logo

你正使用 WordPress.com 帳號留言。 登出 / 變更 )

Twitter picture

你正使用 Twitter 帳號留言。 登出 / 變更 )

Facebook照片

你正使用 Facebook 帳號留言。 登出 / 變更 )

Google+ photo

你正使用 Google+ 帳號留言。 登出 / 變更 )

連結到 %s

%d 位部落客按了讚: