65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Lambda 表达式 - C++ vs. C# vs. C++/CX vs. C++/CLI

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (72投票s)

2011年11月3日

CPOL

7分钟阅读

viewsIcon

347261

downloadIcon

2

比较 C++ 和 C# 中的 lambda 表达式,重点关注这些语言及其变体中 lambda 用法的异同。

引言

作为 C++ 开发者,曾有一段时间,你没有 lambda 表达式,而 C# 等其他语言已经拥有,这相当令人失望。当然,现在情况已经改变,C++ 不仅有了 lambda 表达式,而且 C++ lambda 表达式比 C# lambda 表达式具有更大的语法灵活性。本文将对这两种语言中 lambda 表达式的用法进行比较。

注意:本文无意全面涵盖 C# 或 C++ 中 lambda 表达式的语法。它假设你已经可以在一种或两种语言中使用 lambda 表达式。本文的重点是这些语言及其变体中 lambda 用法的异同。

C# 和 C++ 中的基本用法

这是一个非常简单的 C# 方法,我将用它来演示 C# lambda 表达式

static void RunFoo(Func<int, int> foo)
{
    Console.WriteLine("Result = " + foo(3) + "\r\n");
}

这是相应的 C++ 版本

void RunFoo(function<int(int)> foo)
{
    std::cout << "Result = " << foo(3) << "\r\n\r\n";
}

这两个版本都接受一个函数,该函数有一个 int 参数并返回一个 int。这是一个非常简单的 C# 示例,展示了如何使用 lambda 调用该方法。

RunFoo(x => x);

请注意参数类型和返回类型是如何由编译器推断的。现在是 C++ 版本。

RunFoo( [](int x) -> int { return x; });

参数类型和返回类型需要在 C++ 中指定。嗯,不完全是,下面这样也可以。

RunFoo( [](int x) { return x; });

请注意我是如何删除返回类型规范的。但是现在,如果我做了一个小改动,它将不再编译。

RunFoo( [](int x)  { x++; return x; });

你会得到以下错误。

// C3499: a lambda that has been specified to have a void return type cannot return a value	

智能感知工具提示解释了那里发生的事情。

// the body of a value-returning lambda with no explicit return type must be a single return statement	

这段代码将编译。

RunFoo( [](int x) -> int  { x++; return x; });

请注意,这可能会在未来的版本中改变,即使对于多语句 lambda 表达式,返回类型也会被推导出来。然而,C++ 不太可能支持 lambda 参数中的类型推导。

捕获变量 - C# vs C++

C# 和 C++ 都允许你捕获变量。C# 总是通过引用捕获变量。以下代码的输出将使其变得相当明显。

var foos = new List<Func<int, int>>();

for (int i = 0; i < 2; i++)
{
    foos.Add(x =>
    {
        Console.WriteLine(i);
        return i;
    });
}
foos.ForEach(foo => RunFoo(foo));
foos.Clear();

这将输出

2
Result = 2

2
Result = 2

我想你们大多数人都知道为什么会发生这种情况,但如果你不知道,原因如下。考虑这个非常基本的代码片段。

int i = 5;
RunFoo(x => i);

编译器生成一个看起来像下面这样的类(伪代码)

sealed class LambdaClass
{
    public int i;
    public LambdaClass(){}
    public int Foo(int x){ return i;}
}

RunFoo 的调用编译为(伪代码)

var local = new LambdaClass();
local.i = 5;
RunFoo(new Func<int, int>(local.Foo));

因此,在前面的示例中,在 for 循环内部的每次迭代中都重用了编译器生成的类的同一个实例。这解释了输出。C# 中的解决方法是通过引入局部变量,强制它每次都创建一个 lambda 类的新实例。

for (int i = 0; i < 2; i++)
{
    int local = i;

    foos.Add(x =>
    {
        Console.WriteLine(local);
        return local;
    });
}
foos.ForEach(foo => RunFoo(foo));

这迫使编译器创建 lambda 类的单独实例(只有一个生成的类,有多个实例)。现在看看类似的 C++ 代码。

std::vector<function<int(int)>> foos;

for (int i = 0; i < 2; i++)
{
  foos.push_back([i](int x) -> int
  {
    std::cout << i << std::endl;
    return i;
  });
}

for each(auto foo in foos)
{
  RunFoo(foo);
}
foos.clear();

在 C++ 中,你可以指定如何进行捕获,无论是按值还是按引用。在上面的代码片段中,捕获是按值进行的,因此代码按预期工作。为了获得与原始 C# 代码相同的输出,我们可以按引用捕获(如果出于某种原因需要这样做)。

for (int i = 0; i < 2; i++)
{
  foos.push_back([&i](int x) -> int
  {
    std::cout << i << std::endl;
    return i;
  });
}

在 C++ 中发生的事情与在 C# 中发生的事情非常相似。这是一些伪代码,展示了编译器可能实现这一点的一种合理方式(简化版)

class <lambda0> 
{ 
  int _i; 
  
public: 
  <lambda0>(int i) : _i(i) {} 
  
  int operator()(const int arg) 
  {
    std::cout << i << std::endl;
    return i;
  } 
}; 

如果你查看反汇编代码,你会看到对 () 运算符的调用,其中 lambda 被执行,如下所示

00CA20CB  call `anonymous namespace'::<lambda0>::operator() (0CA1415h)  

捕获变量的 const 性

C++ 默认将变量捕获为 const,而 C# 则不会。考虑以下 C# 代码。

int val = 10;
RunFoo(x =>
{
    val = 25;
    return x;
});

现在是语法等效的 C++ 代码。

int val = 10;
RunFoo([val](int x) -> int
{
  // val = 25; <-- will not compile
  return x;
});

要失去捕获变量的 const 性,你需要显式使用 mutable 规范。

RunFoo([val](int x) mutable -> int
{
  val = 25;  // compiles
  return x;
});

C# 没有语法方法使捕获的变量为 const。你需要捕获一个 const 变量。

局部赋值

在 C# 中,你不能将 lambda 表达式赋值给 var 变量。以下代码行将无法编译。

var f = x => x;

你将收到以下编译器错误。

// Cannot assign lambda expression to an implicitly-typed local variable	

VB.NET 显然支持它(通过 Dim,这是它们的 var 等效项)。所以 C# 决定不这样做有点奇怪。VB.NET 生成一个匿名委托,并对所有参数使用 Object(因为在编译时无法进行推导)。

考虑下面的 C++ 代码。

auto func =  [](int x) { return x; };

这里 func 现在是编译器生成的 lambda 类型。你也可以使用 function<> 类(尽管在这种情况下不需要)。

function<int(int)> copy = func;

当你直接调用 lambda 时,代码将类似于

0102220D  call `anonymous namespace'::<lambda0>::operator() (1021456h)  

当你通过 function<> 对象调用它时,未优化的代码将如下所示

0102222B  call  std::tr1::_Function_impl1<int,int>::operator() (102111Dh) 
 - - - >
        010284DD  call std::tr1::_Callable_obj<`anonymous namespace'::<lambda0>,0>::_ApplyX<int,int &> (10215FAh)  
         - - - >
                0102B73C  call `anonymous namespace'::<lambda0>::operator() (1021456h)  

当然,编译器会简单地对其进行优化,因此在两种情况下,发布模式的二进制代码将是相同的。

从 Lambda 表达式调用方法

以下示例展示了从 C# lambda 表达式调用方法。

void Do(int x) { }

void CallDo() 
{
    RunFoo(x =>
        {
            Do(x);
            return x;
        });
}

C# 编译器在这里做的是生成一个 private 实例方法,该方法调用 lambda 中定义的方法。

private int <LambdaMethod>(int x)
{
    this.Do(x);
    return x;
}

正是这个方法作为委托传递给 RunFoo。现在假设你除了调用成员方法之外,还在捕获一个变量。编译器现在生成一个类,该类捕获变量以及 this 引用。

private class <Lambda>
{
    public int t;
    public Program __this;
    
    public <Lambda>() {}
    
    public int <CallDo>(int x)
    {
        this.__this.Do(x + this.t);
        return x;
    }
}

这在 C++ 中更加明显,因为你必须显式捕获 this 指针才能从 lambda 调用成员函数。请看下面的示例。

int Do(int h)
{
  return h * 2;
}

void Test()
{
  auto func =  [this](int x) -> int
  { 
    int r = Do(1);
    return x + r; 
  };

  func(10);
}

请注意 this 是如何被捕获的。现在当调用编译器生成的 lambda-class() operator 时,调用方法仅仅是调用该函数并传递捕获的 this 指针的问题。

call  T::Do (1021069h)

C++/CLI 中的 Lambda 表达式

对于 C++/CLI 开发者(所有 7 位)来说,一个巨大的失望是 C++/CLI 不支持托管 lambda 表达式。你可以在 C++/CLI 中使用 lambda 表达式,但它们将是本地的,因此你无法轻松地与期望例如 Func<> 参数的托管代码进行互操作。你必须编写管道类将本地 lambda 表达式转换为托管委托。下面是一个示例。

class LambdaRunner
{
  function<int(int)> _lambda;

public:
  LambdaRunner(function<int(int)> lambda) : _lambda(lambda)
  {
  } 

  int Run(int n)
  {
      return _lambda(n);
  }
};

上述类是 lambda 运行器的本地实现。以下类是它的托管包装器。

ref class RefLambdaRunner
{
  LambdaRunner* pLambdaRunner;

  int Run(int n)
  {
    return pLambdaRunner->Run(n);
  }

public:
  RefLambdaRunner(function<int(int)> lambda)
  {
    pLambdaRunner = new LambdaRunner(lambda);
  }

  Func<int, int>^ ToDelegate()
  {
    return gcnew Func<int, int>(this, &RefLambdaRunner::Run);
  }

  void Close()
  {
    delete pLambdaRunner;
  }
};

使用它看起来像下面这样。

auto lambda = [](int x) -> int { return x * 2; };
auto lambdaRunner = gcnew RefLambdaRunner(lambda);
int result  = lambdaRunner->ToDelegate()(10);
lambdaRunner->Close();

嗯,要让它顺利运行需要做很多工作。通过巧妙地使用宏和模板元编程,你可以简化生成本地和托管运行器类的代码。但它仍然是一个蹩脚的解决方案。所以对任何计划这样做的人的友好建议是——不要。省去自己的痛苦吧。

WinRT 和 C++/CX 中的 Lambda 表达式

你可以在 C++/CX 中将 lambda 表达式与 WinRT 类型一起使用。

auto r  = ref new R();
r->Go();

auto lambda = [r]()
{
};

// or

auto lambda = [&r]()
{
}

开发者预览版可能有一个或两个细微的错误,但预期行为是,当你通过复制捕获时,你会产生 AddRefRelease,而当你通过引用捕获时,则不会。编译器会尝试在复制捕获场景中为你优化掉这一点,因为它认为这样做是安全的。这可能是开发者预览版中一个错误的原因,其中一个 Release 被优化掉了,但 AddRef 没有,导致潜在的内存泄漏。但可以肯定的是,这些都将在 Beta 版之前修复,所以我不会太担心。

性能担忧

性能一直是 C++ 开发者(以及一些 C# 和 VB 开发者)的痴迷。因此,你经常会发现人们在论坛中询问使用 lambda 表达式是否会降低他们的代码速度。好吧,如果没有优化,是的,会。会对 () 运算符进行重复调用。但任何最近的编译器都会内联它,因此当你使用 lambda 表达式时,根本不会有任何性能下降。在 C# 中,你不会有编译时优化,但 CLR JIT 编译器应该能够在大多数情况下优化掉额外的间接调用。一个副作用是你的二进制文件会因为所有编译器生成的类而稍微臃肿一些,但对于 lambda 表达式在 C++ 或 C# 中提供的强大语法和语义价值来说,这是一个非常小的代价。

结论

请通过下面的文章论坛提交反馈和批评。所有反馈和批评都将仔细阅读,并将对更令人讨厌的回复进行悲剧性的沉默。*咧嘴笑*

历史

  • 2011 年 11 月 3 日 - 文章首次发布。
  • 2011 年 11 月 4 日
    • 次要拼写错误和格式修复
    • 添加了关于从 lambda 表达式调用方法的章节
© . All rights reserved.