为什么设计一种具有独特匿名类型的语言?


90

作为C ++ lambda表达式的功能,这一直困扰着我:C ++ lambda表达式的类型是唯一且匿名的,我根本无法写下来。即使我创建了两个在语法上完全相同的lambda,也将结果类型定义为不同的。结果是:a)lambda只可以传递到允许编译时传递的模板函数,不可言传的类型与对象一起传递,以及b)lambda仅在通过类型擦除它们之后才有用std::function<>

好的,但这只是C ++的工作方式,我准备将其编写为该语言的令人讨厌的功能。但是,我只是了解到Rust似乎也做同样的事情:每个Rust函数或lambda都有一个唯一的匿名类型。现在我在想:为什么?

因此,我的问题是:从语言设计者的角度来看,将唯一的匿名类型的概念引入语言的优势
什么?


6
与往常一样,更好的问题是为什么不这样做。
Stargateur

31
“ lambda仅在通过std :: function <>进行类型擦除时才有用。”-不,不带它们就直接有用std::function。可以直接调用已传递给模板函数的lambda,而无需使用std::function。然后,编译器可以将lambda内联到模板函数中,这将提高运行时效率。
Erlkoenig

1
我的猜测是,它使lambda的实现更加容易,并使语言更易于理解。如果确实允许将完全相同的lambda表达式折叠成相同的类型,那么您将需要特殊的规则来处理,{ int i = 42; auto foo = [&i](){ return i; }; } { int i = 13; auto foo = [&i](){ return i; }; }因为它所引用的变量是不同的,即使从文本上看它们是相同的。如果您只是说它们都是独一无二的,则不必担心试图弄清楚。
NathanOliver

5
但您也可以给lambdas类型命名,并使用相同的方法。lambdas_type = decltype( my_lambda);
idclev 463035818

3
但是通用lambda应该是什么类型[](auto) {}?首先应该具有类型吗?
EVG

Answers:


78

许多标准(尤其是C ++)都采取了使它们对编译器的需求最小化的方法。坦白说,他们已经要求足够了!如果他们不必指定某些东西使其起作用,则他们倾向于保留其实现定义。

如果lambda不是匿名的,我们必须对其进行定义。关于如何捕获变量,这将不得不说很多。考虑一个lambda的情况[=](){...}。该类型必须指定lambda实际捕获了哪些类型,这对于确定而言可能并非易事。另外,如果编译器成功优化了变量,该怎么办?考虑:

static const int i = 5;
auto f = [i]() { return i; }

一个优化的编译器可以轻松地识别出i可以捕获的唯一值是5,然后将其替换为auto f = []() { return 5; }。但是,如果类型不是匿名的,这可能会更改类型迫使编译器进行更少的优化,i即使实际上并不需要它也可以进行存储。这是一整袋复杂性和细微差别,而lambda打算要做的事情根本不需要。

而且,在实际上确实需要非匿名类型的情况下,您总是可以自己构造闭包类,并使用函子而不是lambda函数。因此,它们可以使lambda处理99%的情况,而让您自己编写1%的解决方案。


Deduplicator在评论中指出,我没有像匿名性那样强调唯一性。我不太清楚唯一性的好处,但是值得注意的是,如果类型是唯一的,则以下行为很清楚(动作将被实例化两次)。

int counter()
{
    static int count = 0;
    return count++;
}

template <typename FuncT>
void action(const FuncT& func)
{
    static int ct = counter();
    func(ct);
}

...
for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

for (int i = 0; i < 5; i++)
    action([](int j) { std::cout << j << std::endl; });

如果类型不是唯一的,则在这种情况下,我们必须指定应该发生的行为。那可能很棘手。在这种情况下,关于匿名性的一些问题也因其独特性而变得丑陋。


请注意,这实际上不是为编译器实现者节省工作,而是为标准维护者节省工作。编译器仍必须针对其特定的实现回答上述所有问题,但标准中未指定这些问题。
ComicSansMS

2
@ComicSansMS当您不必使实现与其他人的标准相适应时,在实现编译器时将这些事情放在一起要容易得多。从经验上来讲,标准维护者过分规范功能通常要比试图找到要指定的最小数量同时仍要从语言中获得所需功能容易得多。作为一个出色的案例研究,看看他们花了多少工作来避免过度指定memory_order_consume,同时仍然使它有用(在某些体系结构上)
Cort Ammon

1
像其他所有人一样,您也极力主张匿名。但是,强迫它也具有独特性真的是个好主意吗?
重复数据删除器

在这里,重要的不是编译器的复杂性,而是生成代码的复杂性。关键不是要使编译器更简单,而是要给它足够的摆动空间以优化所有情况并为目标平台生成自然代码。
Jan Hudec

您无法捕获静态变量。
拉斯兰

70

Lambda不仅是函数,它们是函数和状态。因此,C ++和Rust都使用调用运算符将​​它们实现为对象(operator()在C ++中,Fn*Rust中的3个特征)。

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

struct __SomeName {
    int a;

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

然后使用使用__SomeNamelambda的实例。

在Rust中时,|| a + 1Rust中的糖会变成类似

{
    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 }
}

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

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

  • 使用匿名类型,这是两种语言都实现的。这样做的另一个结果是,所有的lambda必须具有不同的类型。但是对于语言设计人员而言,这具有明显的优势:可以使用其他已经存在的简单语言部分来简单地描述Lambda。它们只是围绕该语言现有位的语法糖。
  • 使用一些特殊的语法来命名lambda类型:但是这不是必需的,因为lambdas已经可以与C ++中的模板或Fn*Rust中的泛型和特征一起使用。两种语言都不会强迫您键入lambda来使用它们(std::function在C ++或Box<Fn*>Rust中)。

另请注意,两种语言都同意可以将不捕获上下文的琐碎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>);
        }
    }
};

尽管它们对于人类而言似乎更为复杂,但对于语言设计者或编译器而言,两者都更为简单。


15
@ cmaster-reinstatemonica考虑传递一个lambda作为排序函数的比较器参数。您是否真的要在这里增加虚拟函数调用的开销?
Daniel Langr

5
@ cmaster-reinstatemonica,因为在C ++中默认情况下没有虚拟的东西
Caleth

4
@cmaster-您的意思是强迫所有lambda的用户为动态双拼支付,即使他们不需要时也要付费?
StoryTeller-Unslander Monica

4
@ cmaster-reinstatemonica您将获得的最好的选择是加入虚拟。你猜怎么着,std::function这是否
Caleth

9
@ cmaster-reinstatemonica任何可以重新指向要调用的函数的机制都将具有运行时开销的情况。那不是C ++的方式。您选择加入std::function
卡雷斯

13

(添加到Caleth的答案中,但是太长,无法在评论中显示。)

lambda表达式只是匿名结构(Voldemort类型,因为您不能说出它的名字)的语法糖。

您可以在以下代码片段中看到匿名结构与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仍然不满意,那么对于匿名结构也应该同样不满意。

某些语言允许使用某种鸭子输入方式,这种方式稍微灵活一些,尽管C ++的模板并不能真正帮助从具有成员字段的模板中创建对象,而该成员字段可以直接替换lambda而不是使用std::function包装纸。


3
谢谢您,这的确确实为在C ++中定义lambda的方式背后的原因提供了一点启发(我记得术语“ Voldemort type” :-))。但是,问题仍然存在:在语言设计师看来,这有什么优势
cmaster-恢复莫妮卡

1
您甚至可以添加int& operator()(){ return x; }这些结构
Caleth,

2
@ cmaster-reinstatemonica•以推测的方式,其余C ++的行为方式都是这样。为了使lambda使用某种“表面形状”的鸭子类型与其他语言完全不同。在lambda的语言中添加这种功能可能会被认为在整个语言中都是通用的,这将是一个潜在的巨大突破。仅针对lambda省略这种功能就适合其余C ++的强力打字。
Eljay

从技术上讲,Voldemort类型为auto foo(){ struct DarkLord {} tom_riddle; return tom_riddle; },因为foo什么也不能使用标识符DarkLord
Caleth

@ cmaster-reinstatemonica效率,替代方法是将每个lambda装箱并动态调度(将其分配在堆上并清除其精确类型)。现在,您已经注意到编译器可以对lambda的匿名类型进行重复数据删除,但是您仍然无法将它们写下来,并且它需要大量工作才能获得很少的收益,因此赔率并不十分理想。
Masklinn

10

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

因为在某些情况下,名称无关紧要,没有用,甚至适得其反。在这种情况下,抽象能力的存在很有用,因为它可以减少名称污染,并解决计算机科学中两个难题(如何命名事物)之一。出于相同的原因,临时对象很有用。

拉姆达

唯一性不是特殊的lambda事物,甚至不是匿名类型的特殊事物。它也适用于该语言中的命名类型。考虑以下内容:

struct A {
    void operator()(){};
};

struct B {
    void operator()(){};
};

void foo(A);

请注意,即使类相同,我也不能传递Bfoo。此相同属性适用于未命名的类型。

lambda只能传递给模板函数,这些模板函数允许将编译时无法说的类型与对象一起传递...通过std :: function <>擦除。

对于lambda的子集,还有第三个选择:非捕获的lambda可以转换为函数指针。


请注意,如果匿名类型的限制是用例的问题,那么解决方案很简单:可以使用命名类型代替。Lambda不执行命名类无法完成的任何工作。


10

Cort Ammon接受的答案很好,但是我认为关于可实现性还有一个更重要的观点。

假设我有两个不同的翻译单元“ one.cpp”和“ two.cpp”。

// one.cpp
struct A { int operator()(int x) const { return x+1; } };
auto b = [](int x) { return x+1; };
using A1 = A;
using B1 = decltype(b);

extern void foo(A1);
extern void foo(B1);

这两个重载foo使用相同的标识符(foo),但名称不同。(在POSIX-ish系统上使用的Itanium ABI中,错误的名称为_Z3foo1A,在这种情况下为_Z3fooN1bMUliE_E。)

// two.cpp
struct A { int operator()(int x) const { return x + 1; } };
auto b = [](int x) { return x + 1; };
using A2 = A;
using B2 = decltype(b);

void foo(A2) {}
void foo(B2) {}

C ++编译器必须确保void foo(A1)“ two.cpp”中的错误名称与“ one.cpp”中的错误名称相同extern void foo(A2),以便我们可以将两个目标文件链接在一起。这是“相同类型”这两种类型的物理含义:本质上是关于单独编译的目标文件之间的ABI兼容性。

C ++编译器是必需的,以确保B1B2属于“相同类型”。(实际上,需要确保它们是不同的类型;但是现在这并不重要。)


什么物理机制并编译使用,以确保A1A2“相同类型”?

它只是遍历typedef,然后查看该类型的完全限定名称。这是一个名为的类类型A。(好吧,::A因为它在全局名称空间中。)因此在两种情况下都是相同的类型。这很容易理解。更重要的是,它很容易实现。要查看两个类类型是否相同,请使用它们的名称并执行strcmp。要将类类型转换为函数的转换名称,请在其名称中写入字符数,然后输入这些字符。

因此,命名类型很容易处理。

什么物理机制可能编译器使用,以确保B1B2“相同类型”,在一个假想的世界里,C ++要求他们具有相同的类型?

那么,它不能使用类型的名称,因为该类型不具有名称。

也许可以以某种方式对lambda主体的文本进行编码。但这有点尴尬,因为实际上b“ one.cpp”中的in与“ two.cpp”中的有细微差别b:“ one.cpp”具有x+1和“ two.cpp”具有x + 1。因此,我们不得不拿出,说要么这个空白差异的规则没有的事,或者说,它不会(毕竟使它们不同类型的),或者也许它(也许该程序的有效性是指实现的,或者“格式错误,无需诊断”)。无论如何,A

摆脱困难的最简单方法就是说每个lambda表达式都产生唯一类型的值。那么,在不同翻译单元中定义的两个lambda类型肯定不是同一类型。在单个翻译单元中,我们可以通过仅从源代码的开头算起“命名” lambda类型:

auto a = [](){};  // a has type $_0
auto b = [](){};  // b has type $_1
auto f(int x) {
    return [x](int y) { return x+y; };  // f(1) and f(2) both have type $_2
} 
auto g(float x) {
    return [x](int y) { return x+y; };  // g(1) and g(2) both have type $_3
} 

当然,这些名称仅在该翻译单元内具有含义。这恩氏$_0总是从其他一些TU的不同类型$_0,即使该TU的struct A永远是同类型其他一些TU的struct A

顺便说一句,请注意我们的“编码lambda文本”的想法还有一个细微的问题:lambda$_2$_3由完全相同的文本组成,但显然不应将它们视为同一类型!


顺便说一句,C ++确实要求编译器知道如何处理任意C ++表达式的文本,例如

template<class T> void foo(decltype(T())) {}
template void foo<int>(int);  // _Z3fooIiEvDTcvT__EE, not _Z3fooIiEvT_

但是C ++并不需要编译器知道如何处理任意C ++语句decltype([](){ ...arbitrary statements... })即使在C ++ 20中仍然格式不正确。


还要注意,使用/可以为未命名的类型赋予本地别名很容易。我有一种感觉,您的问题可能是由于试图做一些可以解决的事情而引起的。typedefusing

auto f(int x) {
    return [x](int y) { return x+y; };
}

// Give the type an alias, so I can refer to it within this translation unit
using AdderLambda = decltype(f(0));

int of_one(AdderLambda g) { return g(1); }

int main() {
    auto f1 = f(1);
    assert(of_one(f1) == 2);
    auto f42 = f(42);
    assert(of_one(f42) == 43);
}

编辑添加:通过阅读您对其他答案的一些评论,听起来您想知道为什么

int add1(int x) { return x + 1; }
int add2(int x) { return x + 2; }
static_assert(std::is_same_v<decltype(add1), decltype(add2)>);
auto add3 = [](int x) { return x + 3; };
auto add4 = [](int x) { return x + 4; };
static_assert(not std::is_same_v<decltype(add3), decltype(add4)>);

这是因为不可捕获的lambda是默认可构造的。(仅从C ++ 20开始在C ++中使用,但是从概念上讲,这始终是正确的。)

template<class T>
int default_construct_and_call(int x) {
    T t;
    return t(x);
}

assert(default_construct_and_call<decltype(add3)>(42) == 45);
assert(default_construct_and_call<decltype(add4)>(42) == 46);

如果尝试使用default_construct_and_call<decltype(&add1)>t它将是默认初始化的函数指针,并且可能会出现段错误。那样就没用了。


实际上,需要确保它们是不同的类型;但是现在还不那么重要。 ”我想知道是否有充分的理由来强制唯一性(如果进行等效定义)。
Deduplicator

我个人认为完全定义的行为总是(几乎?)比未指定的行为更好。“这两个函数指针相等吗?好吧,仅当这两个模板实例化是相同的函数时,才是正确的,只有这两个lambda类型是相同的类型时才如此,仅当编译器决定合并它们时才是正确的。” ck!(但是请注意,我们有一个正好与字符串文本合并类似的情况,也没有人被扰动有关情况让我怀疑这会是灾难性的,允许编译器合并相同的类型。)
Quuxplusone

好吧,两个等效函数(如果不存在)是否相同也是一个很好的问题。对于自由和/或静态功能,标准中的语言不是很明显。但这超出了这里的范围。
重复数据删除器

碰巧的是,本月在LLVM邮件列表上已经进行了有关合并功能的讨论。Clang的代码生成将导致具有完全空体的函数几乎“偶然地”被合并:godbolt.org/z/obT55b从技术上讲这是不合格的,我认为他们可能会修补LLVM以停止这样做。但是,同意,合并功能地址也是一回事。
Quuxplusone

该示例还有其他问题,即缺少返回声明。他们难道不是一个人已经使代码不符合要求吗?另外,我将寻找讨论的内容,但是它们是否显示或假定合并等效功能不符合该标准,它们记录的行为,对gcc的影响,或者仅仅是某些依赖它的行为不发生?
重复数据删除器

9

C ++ lambda需要用于不同操作的不同类型,因为C ++是静态绑定的。它们仅是可复制/可移动构造的,因此大多数情况下,您无需命名它们的类型。但这只是实现细节。

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

C ++还具有匿名结构,其中每个定义都导致唯一的类型。在这里,这个名字不是很容易发音,就标准而言,它根本就不存在。

C#具有匿名数据类型,它小心地禁止从其定义的范围内转义。该实现也为这些名称提供了一个唯一的,不可发音的名称。

拥有匿名类型会向程序员发出信号,表示他们不应在实现过程中四处摸索。

在旁边:

可以为lambda类型指定名称。

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

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

void (*pfoo)() = foo;

1
第一个示例代码仍然不允许后续的Foo_t = []{};,仅此Foo_t = foo而已。
cmaster-恢复莫妮卡

1
@ cmaster-reinstatemonica,这是因为该类型不是默认可构造的,而不是因为匿名。我的猜测是,与任何技术原因一样,这与避免您不得不记住更多的死角案例有关。
卡雷斯

6

为什么要使用匿名类型?

对于由编译器自动生成的类型,选择是(1)满足用户对类型名称的请求,或者(2)让编译器自行选择一个。

  1. 在前一种情况下,每次出现这样的构造时,都希望用户显式提供一个名称(C ++ / Rust:每当定义lambda时; Rust:每当定义函数时)。这是用户每次都要提供的繁琐细节,并且在大多数情况下,永远不会再提及该名称。因此,让编译器自动为其命名是有意义的,并使用现有的功能(例如decltype或类型推断)在需要的几个地方引用该类型。

  2. 在后一种情况下,编译器需要为该类型选择一个唯一的名称,这可能是一个晦涩难懂的名称,例如__namespace1_module1_func1_AnonymousFunction042。语言设计人员可以精确地指定如何以精美而精致的细节来构造此名称,但这不必要地向用户暴露了实现细节,这是明智的用户所不能依靠的,因为即使面对很小的重构,该名称无疑也很脆弱。这也不必要地限制了语言的发展:将来添加功能可能会导致现有名称生成算法发生更改,从而导致向后兼容性问题。因此,简单地省略此细节并断言自动生成的类型是用户无法说出的是有意义的。

为什么要使用唯一(独特)类型?

如果值具有唯一类型,则优化编译器可以在保证其保真度的情况下跨其所有使用站点跟踪唯一类型。因此,用户可以确定编译器完全知道该特定值的来源的某些位置。

例如,编译器看到以下信息:

let f: __UniqueFunc042 = || { ... };  // definition of __UniqueFunc042 (assume it has a nontrivial closure)

/* ... intervening code */

let g: __UniqueFunc042 = /* some expression */;
g();

编译器具有完全的信心,g必须一定起源f,甚至都不知道g。这将使呼叫g虚拟化。用户也会知道这一点,因为用户非常小心地保留了f导致g

必然会限制用户使用该功能f。用户不能随意写:

let q = if some_condition { f } else { || {} };  // ERROR: type mismatch

因为这将导致两种不同类型的(非法)统一。

要解决此问题,用户可以将__UniqueFunc042转换为非唯一类型&dyn Fn()

let f2 = &f as &dyn Fn();  // upcast
let q2 = if some_condition { f2 } else { &|| {} };  // OK

这种类型的擦除所要权衡的是使用&dyn Fn()复杂的编译器推理。鉴于:

let g2: &dyn Fn() = /*expression */;

编译器必须经过艰苦的检查,/*expression */以确定是g2源于f某个函数还是其他函数,以及该出处的条件。在许多情况下,编译器可能会放弃:也许人类可以说出它g2确实是f在所有情况下产生的,但是从f到的路径g2太复杂,编译器无法解密,从而导致虚拟调用g2具有悲观的性能。

当此类对象交付给通用(模板)功能时,这一点变得更加明显:

fn h<F: Fn()>(f: F);

如果调用h(f)where f: __UniqueFunc042,则h专用于唯一实例:

h::<__UniqueFunc042>(f);

这样一来,编译器便可以h针对,的特定参数量身定制针对的专用代码f,并将其分配给f如果不进行内联,则对to很可能是静态的。

在相反的情况下,h(f)用调用f2: &Fn()h实例化为

h::<&Fn()>(f);

在type的所有函数之间共享&Fn()。从内部看h,编译器对类型的不透明函数了解甚少&Fn(),因此只能保守地f使用虚拟调度进行调用。要静态分派,编译器将必须h::<&Fn()>(f)在其调用站点内联调用,如果h过于复杂则无法保证。


关于选择名称的第一部分没有讲到重点:像这样的类型void(*)(int, double)可能没有名称,但是我可以写下来。我将其称为无名类型,而不是匿名类型。我会称呼诸如__namespace1_module1_func1_AnonymousFunction042名称修饰之类的神秘事物,这绝对不在此问题的范围之内。这个问题是关于标准所保证的无法写下的类型,而不是引入可以以有用的方式表达这些类型的类型语法。
cmaster-恢复莫妮卡

3

首先,没有捕获的lambda可转换为函数指针。因此,它们提供了某种形式的通用性。

现在为什么带捕获的lambda无法转换为指针?因为函数必须访问lambda的状态,所以此状态将需要作为函数参数出现。


好吧,捕获应该成为lambda本身的一部分,不是吗?就像它们被封装在std::function<>
cmaster-恢复莫妮卡

3

为了避免名称与用户代码冲突。

甚至具有相同实现的两个Lambda也会具有不同的类型。没关系,因为即使它们的内存布局相等,我也可以为对象设置不同的类型。


int (*)(Foo*, int, double)这样的类型不会与用户代码发生名称冲突。
cmaster-恢复莫妮卡

您的示例不能很好地概括。虽然lambda表达式只是语法,但它会求值到某种结构,尤其是使用capture子句时。明确命名该名称可能会导致现有结构的名称冲突。

同样,这个问题是关于语言设计的,而不是关于C ++的。我可以肯定地定义一种语言,其中lambda的类型更类似于函数指针类型,而不是数据结构类型。C ++中的函数指针语法和C中的动态数组类型语法证明了这是可能的。这就引出了一个问题,为什么lambda不使用类似的方法?
cmaster-恢复莫妮卡

1
不,您不能,因为可变的curring(捕获)。您需要功能和数据才能使其正常工作。
布林迪

@Blindy哦,是的,我可以。我可以将lambda定义为包含两个指针的对象,一个用于捕获对象,一个用于代码。这样的lambda对象将很容易按值传递。或者,我可以在捕获对象的开头使用一段代码存根,在跳转到实际的lambda代码之前先获取自己的地址,以达到目的。那会将lambda指针变成一个地址。但这是不必要的,因为PPC平台已证明:在PPC上,函数指针实际上是一对指针。这就是为什么你不能投void(*)(void)void*和回标准的C / C ++。
cmaster-恢复莫妮卡
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.