提问者:小点点

为什么要设计一种具有唯一匿名类型的语言呢?


作为C++lambda表达式的一个特性,这一直困扰着我:C++lambda表达式的类型是唯一的和匿名的,我根本无法将其写下来。 即使我创建了两个语法上完全相同的lambda,生成的类型也被定义为不同的。 其结果是,a)lambda只能传递给允许编译时间,不可描述的类型与对象一起传递的模板函数,b)lambda只有在通过std::function<>进行类型擦除后才有用。

好的,但这只是C++的方式,我已经准备好把它作为那种语言的一个讨厌的特性来写了。 然而,我刚刚了解到Rust似乎也是这么做的:每个Rust函数或lambda都有一个唯一的匿名类型。 现在我在想:为什么?

所以,我的问题是:
从语言设计者的角度来看,在语言中引入唯一,匿名类型的概念有什么好处?


共3个答案

匿名用户

Lambdas不仅仅是功能,它是一种功能,也是一种状态。 因此,C++和Rust都将它们实现为一个带有调用运算符的对象(C++中的operator(),Rust中的3个fn*特征)。

基本上,在C++中,[a]{return a+1;}可以将类似于

struct __SomeName {
    int a;

    int operator()() {
        return a + 1;
    }
};

然后使用__somename的实例,其中使用lambda。

在锈蚀状态下,a+1在锈蚀状态下将会去糖,类似于

{
    struct __SomeName {
        a: i32,
    }

    impl FnOnce<()> for __SomeName {
        type Output = i32;
        
        extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
            self.a + 1
        }
    }

    // And FnMut and Fn when necessary

    __SomeName { a }
}

这意味着大多数lambdas必须具有不同的类型。

现在,我们有几种方法可以做到这一点:

  • 使用匿名类型,这是两种语言都实现的。 这样做的另一个后果是所有的lambda都必须具有不同的类型。 但是对于语言设计者来说,这有一个明显的好处:可以使用语言中其他已经存在的更简单的部分来简单地描述Lambdas。 它们只是围绕该语言已有部分的语法糖。
  • 使用一些特殊的语法来命名lambda类型:但是,这是不必要的,因为lambda已经可以与C++中的模板一起使用,或者与Rust中的泛型和fn*特征一起使用。 这两种语言都不会强制您键入删除lambdas来使用它们(在C++中使用std::function或在Rust中使用box)。

还要注意,两种语言都同意,不捕获上下文的琐碎lambda可以转换为函数指针。

使用简单的特性描述语言的复杂特性是非常常见的。 例如,C++和Rust都有range-for循环,它们都将它们描述为其他特性的语法糖。

C++定义

for (auto&& [first,second] : mymap) {
    // use first and second
}

相当于

{

    init-statement
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {

        range_declaration = *__begin;
        loop_statement

    }

} 

而Rust定义了

for <pat> in <head> { <body }

相当于

let result = match ::std::iter::IntoIterator::into_iter(<head>) {
    mut iter => {
        loop {
            let <pat> = match ::std::iter::Iterator::next(&mut iter) {
                ::std::option::Option::Some(val) => val,
                ::std::option::Option::None => break
            };
            SemiExpr(<body>);
        }
    }
};

虽然它们对人类来说似乎更复杂,但对语言设计者或编译器来说都更简单。

匿名用户

(补充了Caleth的回答,但太长了,不适合评论。)

lambda表达式只是匿名结构(一种伏地魔类型,因为你不能说出它的名字)的语法糖。

您可以在以下代码片段中看到匿名结构和lambda的匿名性之间的相似性:

#include <iostream>
#include <typeinfo>

using std::cout;

int main() {
    struct { int x; } foo{5};
    struct { int x; } bar{6};
    cout << foo.x << " " << bar.x << "\n";
    cout << typeid(foo).name() << "\n";
    cout << typeid(bar).name() << "\n";
    auto baz = [x = 7]() mutable -> int& { return x; };
    auto quux = [x = 8]() mutable -> int& { return x; };
    cout << baz() << " " << quux() << "\n";
    cout << typeid(baz).name() << "\n";
    cout << typeid(quux).name() << "\n";
}

如果这对于lambda仍然不满意,那么对于匿名结构也应该同样不满意。

有些语言允许一种更灵活的duck typing,尽管C++的模板并不能真正帮助从具有成员字段的模板生成对象,该成员字段可以直接替换lambda,而不是使用std::function包装器。

匿名用户

C++lambda需要唯一的类型,因为它们是对象,而不是函数。 它们只是可复制/移动构造的,所以大多数情况下不需要命名它们的类型。 但这都是一些实现细节。

我不确定C#lambda是否有一个类型,因为它们是“匿名函数表达式”,并且它们会立即转换为兼容的委托类型或表达式树类型。 如果是这样,那可能是一种难以发音的类型。

C++也有匿名结构,其中每个定义导致一个唯一的类型。 在这里,这个名字并不难听,它只是不存在,就标准而言。

C#具有匿名数据类型,它小心地禁止这些数据类型从定义的作用域中逃逸。 该实现还为这些对象提供了一个唯一的,难以发音的名称。

使用匿名类型向程序员发出信号,他们不应该在实现中四处查看。

旁白:

您可以为lambda的类型命名。

auto foo = []{}; 
using Foo_t = decltype(foo);

如果没有任何捕获,可以使用函数指针类型

void (*pfoo)() = foo;