【问题标题】:What if the lambda expression of C++11 supports default arguments?如果 C++11 的 lambda 表达式支持默认参数怎么办?
【发布时间】:2012-12-20 19:21:31
【问题描述】:

我觉得下面的代码非常好用,没有害处:

auto fn = [](bool b = false) -> int // NOT legal in C++11
{
    return b ? 1 : 0;
}; 

为什么 C++11 明确禁止 lambda 表达式的默认参数?

我只是想知道背后的基本原理和考虑因素。

我想知道 C++11 标准所说的“WHY”而不是“WHAT”。

【问题讨论】:

  • 因为从技术上讲,它是一个重载(我不确定这个词在这里是否正确,但你明白了):你可以用 bool 来调用它,也可以不用 bool。在这种情况下,lambda 函数的 type 应该是什么?在设计 lambda 时,他们不想引入可以具有可变数量参数(在您的情况下为 0 或 1)的 lambda 类型
  • “为什么”该代码非常方便且无害? (因为我们在这里问为什么)。
  • 不管怎样,lambdas 实际上只是匿名函子,函子可以有默认参数。有趣的是 lambdas 不会。
  • @DietrichEpp C++03 不允许将本地类型用作模板类型参数,但我相信 C++11 消除了该限制。
  • @Dietrich :这是一个有争议的问题; aschepler 是对的——在 C++11 中,模板参数不再需要链接。

标签: c++ c++11 lambda default-arguments


【解决方案1】:

没有真正的理由 lambda 不能有默认参数。但是,使用 lambda 有两种主要方式,其中只有一种允许默认参数而不更改类型系统。

  1. 您可以直接调用 lambda,也可以通过模板调用。在这种情况下,默认参数可以正常工作。

  2. 您可以通过 std::function 调用 lambda。如果不更改类型系统,默认参数将不起作用。

我的猜测是人们用 C++11 编写的新函数通常会采用std::function 参数,因为这样一来,函数就不会模板,否则不必为传递给它的每个 lambda 和仿函数实例化它。

为什么std::function(或函数指针)不能采用默认值?

这种函数的类型并不明显。

  • 如果是std::function<int(bool)>,那么如何使用默认值调用它? (你不能。)

  • 如果是std::function<int(bool=false)>,那么它兼容哪些类型,如何转换?你能把它转换成std::function<int()>吗? std::function<int(bool=true)>呢?

  • 如果是像std::function<int(bool=default)> 这样的新东西,那么它兼容什么类型,转换是如何工作的?

基本上,这不仅仅是一个您可以在标准中翻转并使函数指针/std::function 处理默认参数的开关。普通函数中的默认参数是使用函数声明中的信息处理的,这些信息在 lambda 或函数指针的调用站点不可用。因此,您必须将有关默认值的信息编码到函数类型中,然后为转换和兼容性制定所有非显而易见的规则。

因此,您必须提出一个令人信服的理由,说明为什么会添加这样的功能,并说服委员会。

那么,为什么 lambda 不能采用默认值?

我还没有回答这个问题。但我认为添加它不会是一个非常有用的功能。如果可以的话,我会删除这个答案,但它已被接受。如果可以的话,我会投反对票,但这是我的。生活吧。

【讨论】:

  • 为什么调用站点上没有来自 lambda 声明的信息?
  • @SethCarnegie:因为 lambda 函数没有函数声明,所以它们只有类型。这也适用于通过指针调用的任何函数。
  • @SethCarnegie:但它可以从完全不同的翻译单元调用。类型信息不会跨越翻译单元边界。
  • @JohannesSchaub-litb:不要纠结于函数指针与 lambda 的区别。关键是 lambda 是一个值,所以它没有声明。是的,当函数转换为值(具有函数指针类型)时,函数也是如此。由于类型系统不编码默认参数,因此很自然地得出结论,将默认参数添加到 lambda 需要扩展类型系统以编码默认参数。
  • 我很抱歉,但这根本没有意义。如果您想知道为什么它没有意义并希望我撤消我的反对票,您可以提出一个新的 SO 问题(看来我在评论中的解释不够详细)。一些人指出 lambda “只是”一个可以具有默认参数的类类型函数对象。
【解决方案2】:

我同意没有真正的“技术”限制本身允许 lambdas 中的默认参数在某些情况下工作。它不会破坏您的指针和auto,因为函数的类型不受默认参数的影响。但这也是为什么这不是非常实用的原因。

为什么?

因为默认参数虽然是函数签名的一部分,但不是函数类型的一部分:

[C++11: 1.3.17]:
签名
名称、参数类型列表 (8.3.5) 和封闭命名空间(如果有)
[ 注意: 签名用作名称修改和链接的基础。 ——尾注 ]

[C++11: 8.3.5/6]: [..] 返回类型、parameter-type-listref-qualifier和cv-qualifier-seq,但不是默认参数 (8.3.6) 或异常规范 (15.4),是函数类型的一部分。 [ 注意: 在函数指针、函数引用和成员函数指针的赋值和初始化过程中会检查函数类型。 ——尾注 ]

它们本质上是一种语法糖,由编译器“激活”,能够看到您使用的函数的声明,并在函数调用时注入:

#include <iostream>

void foo(int x = 5)
{
   std::cout << x << '\n';
}

int main()
{
   foo();
}
  • 输出:5

默认参数是“可见”。

但是,当您将函数“隐藏”在指针后面时:

int main()
{
    void (*bar)(int) = &foo;
    bar();
}
  • 错误:too few arguments to function

bar 的类型是正确的,并且编译器知道 foo 有一个默认值,但是根本没有 direct 语法可以在调用 @987654330 时通知编译器@ 那bar 也是foo。当然,在这种微不足道的场景中,它可以通过观察分配来解决问题,但这很难成为更广泛论点的理由。

出于同样的原因,仅在调用站点不可见的定义中声明的默认参数几乎是无用的:

// a.h
void foo(int);

// a.cpp
#include "a.h"
#include <iostream>

void foo(int x = 5)
{
   std::cout << x << '\n';
}

// main.cpp
#include "a.h"

int main()
{
    foo();
}
  • main.cpp 中的错误:too few arguments to function

我想这就是原因:

[C++11: 8.3.6/4]: [..] 不同范围内的声明具有完全不同的默认参数集。 [..]

我们被允许为类非模板成员函数定义“堆积”默认参数 ([C++11 8.3.6/6]);该示例表明此默认值仍将仅适用于同一个 TU,这遵循我们在上面的第二个代码 sn-p 中看到的行为。

因此,如果默认参数不是函数类型的一部分,并且必须对调用站点明确可见,那么只有少数人为设计的极端情况对 lambdas 有用,即当它们在与创建它们的范围相同的范围内被调用,因此编译器可以轻松地弄清楚如何“填充”调用 lambda 的默认参数,那么,那有什么意义呢?不多,我告诉你。

【讨论】:

  • 只是一个想法。可以考虑使用void (*bar)(int) = &amp;foo; 定义的bar 将要求使用单个int 参数调用它,从而使调用bar(); 非法。此外,void (*bar2)() = &amp;foo; 可以生成一个中间函数,该函数会自动为您添加默认参数。这会使您的语句 "但是在调用 bar 时根本不存在直接的语法来通知编译器 bar 也是 foo。" 不正确,因为会有直接的语法可用。
  • 另外,lambdas 是简化的仿函数,你可以在其中使用默认参数。类型是具体的,但默认参数不是。直到你调用或查询一个带有默认参数的函数,它才需要放弃一个实际的函数类型。
【解决方案3】:

正如问题的 cmets 所暗示的那样,可能没有技术 理由不存在默认参数。但另一个问题是“默认参数是否有实际原因?”我认为这个问题的答案是“不”,这就是原因。

为了调用 lambda,您可以做的一件事是立即调用它

[] { printf("foo"); }();

如果有的话,我确信它的用途有限,所以让我们继续吧。调用 lambda 的唯一另一种方法是先将其绑定到变量,这里有几个选项。

  1. 使用自动。所以我们得到auto foo = [] { printf("foo"); }; foo();
  2. 将其绑定到函数指针:void (*foo)() = [] { printf("foo"); }; foo()。当然,这仅在 lambda 未捕获时才有效。
  3. 通过将 lambda 传递给函数或函数模板来执行 1 或 2 的等效操作。

现在让我们来看看默认参数在每种情况下的用处

  1. 在这种情况下,我们直接调用 lambda,因此代码可能足够紧密,我们不会使用各种数量的参数调用 lambda。如果我们是,lambda 可以(应该?)可能被重构为更通用的组件。我没有看到任何实际的好处。
  2. (见 1)
  3. 我们将 lambda 传递给另一个函数。我也没有看到这里默认参数的实际好处。回想一下好的旧仿函数(可以有默认参数)——我不能说我知道有太多函数依赖于默认参数,即使这些函数也存在。由于 lambdas 实际上只是函子,因此这种观察没有理由突然改变。

我认为这些点足以说明 lambda 的默认参数真的没那么有用。另外,我看到有些人在谈论 lambda 类型的问题,如果它有默认参数,但这是一个非 IMO 问题。您总是可以编写自己的仿函数来做同样的事情,并且 有默认参数。另外,关于降级为函数指针,也没什么好说的。取一个正常的函数

void func(int i = 0)
{
}

并获取它的地址。你得到了什么?一个void (*)(int)。 lambda 没有理由遵循不同的规则。

【讨论】:

  • 对不起,我回答了一半 - 我现在完成了!
  • 如果您担心这类事情,通常会删除答案并在完成后取消删除。
  • 请避开西部最快的枪。在写完之前,您无需点击“发布您的答案”。
  • 我们还在等待... + 一个技术原因:它的类型也不会像 cmets 中所讨论的那样被明确定义。
  • @LightnessRacesinOrbit:那不是我的本意。我在笔记本电脑上打字,然后“用手掌点击”了发布按钮。
【解决方案4】:

我已经阅读了其中的一些回复,并且每个人(根据我阅读的内容 tl;dr)都提到了类型系统中的限制。这真的和那个没有任何关系。虽然一个函数的类型包括所有参数,无论是否默认,但一个有默认参数的函子可以被认为是一个模糊函数,直到你看到它的类型才会出现。

由于 lambda 是简化函子使用的语法糖,并带有一些很酷的类型推导。当它被编译器归结并转换时,当它有捕获时是一个函子(如果没有捕获则为一个函数)。所以真的没有理由不能在 lambda 中使用它。观察:

#include<functional>
#include<iostream>

int main()
{
    // my lambda as written as a functor.  Not pretty eh?  But it works. o.O
    struct {
    void operator()(char const* x = "default") {
         std::cout << x << std::endl;
    } } fn;

    fn("Hi");
    fn();
    std::function<void(char const*)> fn_copy_1 = fn;
    std::function<void()>            fn_copy_0 = fn;
    fn_copy_1("there");
    fn_copy_0();
    return 0;
}

结果 (running example here):

Hi
default
there
default

所有类型系统都没有任何更改。

那么,为什么会有这样的限制呢?我的猜测,没有人认为它会有用,如果不实施,测试的东西就会更少。用户称他们需要功能 X 时,会将事物添加到语言中。如果没有人要求它并且没有足够的价值与之相关联,那么它就不会被添加。这并不是说以后不能加,而是有需求的时候才加。

【讨论】:

  • 您也可以使用auto fn_copy_1 = [&amp;](char const* x){fn(x);}; auto fn_copy_0 = [&amp;](){fn("default");};,它使用默认参数创建一个 lambda 函数。每个默认参数只需要另一个 lambda 函数。
  • @QuentinUK,是的,但这不是带有默认参数的 lambda,即有两个不同命名的 lambda 调用第一个,这意味着您必须调用不同命名的 lambda 才能拥有你想要的默认值。但是,如果您想让同名的 lambda 具有默认值,您可以按照您的建议加上 Overload a lambda function 中显示的内容来重载 lambda 名称,使其实际上允许使用默认参数。再说一遍,这可以实现,但事实并非如此。
  • 虽然我在写原始答案时确实知道 lambda 重载,但我选择不包含它,因为我觉得它并没有真正为论点添加任何东西,它只是另一种方式表明没有真正的理由不允许使用 lambda 默认参数,除了它使编译器更容易实现之外。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-24
相关资源
最近更新 更多