【问题标题】:Why does C++ not have reflection?为什么 C++ 没有反射?
【发布时间】:2010-09-26 10:21:57
【问题描述】:

这是一个有点奇怪的问题。我的目标是了解语言设计决策并确定 C++ 中反射的可能性。

  1. 为什么 C++ 语言委员会没有着手在语言中实现反射?在一种不在虚拟机上运行的语言(如 java)中,反射是否太难了?

  2. 如果要为 C++ 实现反射,会有哪些挑战?

我猜反射的用途是众所周知的:编辑器可以更容易编写,程序代码会更小,可以为单元测试生成模拟等等。但是,如果您也可以评论反射的使用,那就太好了。

【问题讨论】:

    标签: c++ reflection


    【解决方案1】:

    C++ 中的反射存在几个问题。

    • 要添加的工作量很大,而且 C++ 委员会相当保守,除非他们确定会有回报,否则不要花时间在激进的新功能上。 (已经提出了添加类似于 .NET 程序集的模块系统的建议,虽然我认为人们普遍认为拥有它会很好,但这不是他们目前的首要任务,并且一直被推迟到很久以后C++0x。这个特性的动机是摆脱#include系统,但它也会启用至少一些元数据。

    • 你不会为你没有付出的代价 采用。这是必须的基本内容之一 C++ 的设计理念。 为什么我的代码要随身携带 如果我可能永远不需要元数据? 此外,添加元数据 可能会禁止编译器 优化。我为什么要付那个 如果我可能永远不需要,我的代码成本 那个元数据?

    • 这将我们引向另一个重点: C++非常很少保证 关于编译的代码。这 编译器可以做的漂亮 任何它喜欢的东西,只要 由此产生的功能是什么 是期待。例如,您的 类实际上不需要 在那里。编译器可以将它们优化掉,内联 他们所做的一切,而且 经常这样做,因为 即使是简单的模板代码也倾向于 创建不少模板 实例化。 C++ 标准 图书馆依靠这种激进 优化。函子只是 高性能,如果开销 实例化和销毁 对象可以被优化掉。 向量上的operator[] 仅与原始可比 性能中的数组索引 因为整个算子可以是 内联并因此完全删除 从编译的代码。 C# 和 Java 做出很多保证 编译器的输出。如果我定义 C# 中的一个类,那么该类 将 存在于生成的程序集中。 即使我从不使用它。即使所有 对其成员函数的调用可以 内联。班级必须是 在那里,以便反射可以找到 它。 C# 缓解了部分问题 编译为字节码,这意味着 JIT 编译器可以删除 类定义和内联 功能,如果它喜欢,即使 初始 C# 编译器不能。在 C++ 中, 你只有一个编译器,它 必须输出有效的代码。如果你 被允许检查元数据 一个 C++ 可执行文件,你会期望 查看它定义的每个类,其中 意味着编译器将有 保留所有定义的类, 即使它们不是必需的。

    • 还有模板。 C++ 中的模板一点也不像 其他语言的泛型。每一个 模板实例化创建一个 类型。 std::vector<int> 是一个完全独立的类 std::vector<float>。这加起来 一个整体有很多不同的类型 程序。我们应该反思什么 看? 模板 std::vector?但 怎么可能,因为那是 源代码构造,它没有 在运行时是什么意思?它必须看到 单独的类 std::vector<int>std::vector<float>。和 std::vector<int>::iteratorstd::vector<float>::iterator,一样 对于const_iterator 等等。和 一旦你进入模板 元编程,你很快就会结束 实例化数百个模板, 所有这些都被内联和删除 再次由编译器。他们没有 意思是,除了作为 a 的一部分 编译时元程序。都应该 这数百个类是可见的 反思?他们不得不, 因为否则我们的反思 如果它甚至不能保证我定义的类实际上会存在,那将毫无用处。一个附带的问题是模板类在实例化之前不存在。想象一个使用std::vector<int> 的程序。我们的反射系统是否应该能够看到std::vector<int>::iterator?一方面,你肯定会期望如此。这是一个重要的类,它是根据std::vector<int> 定义的,确实 存在于元数据中。另一方面,如果程序从未真正使用这个迭代器类模板,它的类型将永远不会被实例化,因此编译器一开始就不会生成该类。而且在运行时创建它为时已晚,因为它需要访问源代码。

    • 最后,反射并不完全 在 C++ 中与在 C# 中一样重要。这 原因又是模板 元编程。解决不了 一切,但在许多情况下 否则你会求助于 反思,可以写一个 做同样事情的元程序 编译时的事情。 boost::type_traits 是一个简单的 例子。你想知道类型 T?检查其type_traits。在 C# 中, 你必须在它之后钓鱼 使用反射键入。反射 对某些人仍然有用 东西(我能看到的主要用途, 哪个元编程不容易 替换,用于自动生成 序列化代码),但它会 承担一些重大成本 C++,而且它不像其他语言那样经常使用。

    编辑: 回应cmets:

    cdleary: 是的,调试符号做类似的事情,因为它们存储有关可执行文件中使用的类型的元数据。但他们也受到我所描述的问题的困扰。如果您曾经尝试过调试发布版本,您就会明白我的意思。您在源代码中创建的类存在很大的逻辑空白,该类已在最终代码中内联。如果您要将反射用于任何有用的事情,您需要它更加可靠和一致。事实上,几乎每次编译时类型都会消失和消失。您更改了一个微小的细节,编译器决定更改哪些类型被内联,哪些不被内联,作为响应。当您甚至不能保证最相关的类型将在您的元数据中表示时,您如何从中提取任何有用的东西?您正在寻找的类型可能在上一个版本中已经存在,但现在它已经消失了。明天,有人将检查一个小的无害更改到一个小的无害函数,这使得类型足够大以至于它不会被完全内联,所以它会再次回来。这对于调试符号仍然有用,但仅此而已。我讨厌尝试根据这些条款为类生成序列化代码。

    Evan Teran:当然,这些问题可以解决。但这又回到了我的第 1 点。这需要做很多工作,而且 C++ 委员会有很多他们认为更重要的事情。在 C++ 中获得一些有限的反射(并且它会受到限制)的好处真的足够大,足以证明以牺牲其他特性为代价来关注它吗?添加已经(大部分)可以通过库和预处理器(如 QT)完成的核心语言功能真的有巨大的好处吗?也许吧,但与不存在此类库的情况相比,这种需求的紧迫性要小得多。 不过,对于您的具体建议,我相信在模板上禁止它会使其完全无用。例如,您将无法在标准库上使用反射。什么样的反射不会让你看到std::vector?模板是 C++ 的一个巨大部分。一个在模板上不起作用的功能基本上是没用的。

    但你是对的,可以实现某种形式的反射。但这将是语言的重大变化。就像现在一样,类型完全是一种编译时构造。它们的存在是为了编译器的利益,仅此而已。编译代码后,没有类。如果您扩展自己,您可能会争辩说函数仍然存在,但实际上,只有一堆跳转汇编指令和大量堆栈推送/弹出。添加此类元数据时,无需进行太多操作。

    但是就像我说的,有一个建议是对编译模型进行更改,添加自包含模块,存储选择类型的元数据,允许其他模块引用它们而不必与#includes 混淆。这是一个好的开始,老实说,我很惊讶标准委员会并没有因为太大的变化而放弃提案。所以也许在5-10年内? :)

    【讨论】:

    • 这些问题中的大部分不是必须通过调试符号来解决吗?并不是说它会高效(因为您提到的内联和优化),但是您可以通过执行任何调试符号来允许反射的可能性
    • 关于您的第一点的另一件事:据我所知,没有人尝试将反射添加到 C++ 实现中。没有什么好的经验。委员会可能不愿意带头,尤其是在exportvector<bool> 之后。
    • 我同意 C++ 不应该有运行时反射。但是编译时反射几乎没有上述问题,如果他们愿意,可以用于某人在特定类上构建运行时反射。能够通过模板访问类的第 n 个方法和第 n 个父类的类型、名称和特性吗?并在编译时获得这样的数量?将使基于 CRTP 的自动反射变得可行,而没有人为他们不使用的东西付费。
    • 您的第三点在许多方面是最重要的:C++ 旨在适合在内存成本较高的平台上编写独立代码;如果删除一些未使用的代码将允许程序安装在成本为 2.00 美元的微控制器中,而不是一个成本为 2.50 美元的微控制器中,并且如果代码以 1,000,000 个单位运行,那么消除该代码可以节省 500,000 美元。如果没有反射,静态分析通常可以识别 90%+ 的无法访问的代码;如果允许反射,则必须假定可以通过反射到达的任何东西都可以到达,即使其中 90% 不是。
    • 委员会肯定有一些可以轻松改进的东西,最后要说的是,typeinfoname() 函数必须返回程序员输入的名称而不是未定义的东西。并给我们一个用于枚举器的字符串化器。这实际上对于序列化/反序列化、帮助制造工厂等至关重要。
    【解决方案2】:

    反射需要一些关于类型的元数据存储在可以查询的地方。由于 C++ 编译为本地机器代码并由于优化而发生大量更改,因此在编译过程中几乎丢失了应用程序的高级视图,因此无法在运行时查询它们。 Java 和 .NET 在虚拟机的二进制代码中使用了非常高级的表示,使得这种级别的反射成为可能。然而,在某些 C++ 实现中,有一种称为运行时类型信息 (RTTI) 的东西,可以将其视为反射的精简版本。

    【讨论】:

    • RTTI 符合 C++ 标准。
    • 但并非所有 C++ 实现都是标准的。我见过不支持 RTTI 的实现。
    • 而且大多数支持 RTTI 的实现也支持通过编译器选项关闭它。
    【解决方案3】:

    所有语言都不应该尝试融合所有其他语言的所有功能。

    C++ 本质上是一个非常非常复杂的宏汇编器。它不是(传统意义上的)C#、Java、Objective-C、Smalltalk 等高级语言。

    对于不同的工作有不同的工具是很好的。如果我们只有锤子,所有的东西都会看起来像钉子等等。拥有脚本语言对某些工作很有用,而反射型 OO 语言(Java、Obj-C、C#)对另一类工作很有用,而且超级- 接近机器的高效准系统语言对于另一类工作(C++、C、汇编程序)很有用。

    C++ 在将汇编程序技术扩展到令人难以置信的复杂性管理水平和抽象方面做得非常出色,从而使人类更可能进行更大、更复杂的编程任务。但它不一定是最适合那些从严格的高级角度(Lisp、Smalltalk、Java、C#)解决问题的人的语言。如果您需要一种具有这些功能的语言来最好地解决您的问题,那么感谢那些创建了这些语言供我们所有人使用的人!

    但 C++ 适合那些出于任何原因需要在其代码和底层机器操作之间建立强相关性的人。无论是它的效率,还是编程设备驱动程序,还是与较低级别的操作系统服务的交互,或者其他什么,C++ 都更适合这些任务。

    C#、Java、Objective-C 都需要更大、更丰富的运行时系统来支持它们的执行。该运行时必须交付给相关系统 - 预先安装以支持您的软件的操作。并且必须为各种目标系统维护该层,由其他语言定制以使其在该平台上工作。中间层——主机操作系统和代码之间的自适应层——运行时,几乎总是用 C 或 C++ 之类的语言编写,效率是第一,可以很好地理解软件和硬件之间的确切交互理解并操纵到最大收益。

    我喜欢 Smalltalk、Objective-C,以及拥有丰富的运行时系统,包括反射、元数据、垃圾收集等。可以编写惊人的代码来利用这些设施!但这只是堆栈上的较高层,必须位于较低层上的层,它们本身最终必须位于操作系统和硬件之上。我们将始终需要一种最适合构建该层的语言:C++/C/Assembler。

    附录:C++11/14 将继续扩展 C++ 能力以支持更高级别的抽象和系统。线程、同步、精确的内存模型、更精确的抽象机器定义使 C++ 开发人员能够实现许多高级抽象,而这些高级语言中的一些曾经拥有专有域,同时继续提供接近金属性能和出色的可预测性(即最小的运行时子系统)。也许反射工具将在未来的 C++ 版本中选择性地启用,对于那些需要它的人 - 或者也许一个库将提供这样的运行时服务(也许现在有一个,或者一个在 boost 中的开始?)。

    【讨论】:

    • 您关于语言的运行时必须用另一种语言编译的观点在 Objective-C 的情况下是不正确的,因为它的运行时是用 C 编写的(Objective-C 是)。
    • 这是没有区别的区别。最终,Objective-C 使用的运行时子系统实际上不是用 Objective-C 编写的,而是用 C 编写的,这有什么区别?
    • 对不起;但只要你正确链接它,你就可以用C编译一个objective-c程序,事实上我是在这里完成的:stackoverflow.com/a/10290255/427309。你上面的整个陈述都是错误的。运行时可通过 C 完全访问,这是使其成为如此强大的动态语言的原因之一。
    • “C 运行时”只是一个包含 C 标准库代码的动态库。 “C++ 运行时”也是如此。它与像 Objective-C 这样的运行时系统完全不同。另外......虽然我想你可以在技术上使用 C 中的 Objective-C 运行时,但这仍然只是一个使用 Objective-C 运行时的 C 程序——你不能在 C 中编译一个实际的 Objective-C 程序。
    • C++11 具有内存模型 + 原子使其更像 像一个可移植的汇编程序。这些不是高级的东西,它们是 C++ 以前缺乏可移植支持的 级的东西。但是,如果你做错任何事情,C++ 中的 UB 数量使它非常不同于 Java 等基于 VM 的语言,也不同于任何特定的汇编语言。例如签名溢出在 C++ 源代码中完全是 UB,编译器可以基于该事实进行优化,即使编译为 x86,但在几乎所有平台上的 asm 中它只会环绕。现代 C++ 与可移植的汇编语言相去甚远。
    【解决方案4】:

    如果您真的想了解有关 C++ 的设计决策,请查找 Ellis 和 Stroustrup 的 The Annotated C++ Reference Manual 的副本。它不是最新的标准,但它贯穿了原始标准,并解释了事情是如何工作的,并且经常解释他们是如何做到这一点的。

    【讨论】:

    • Stroustrup 的 C++ 的设计和演变
    【解决方案5】:

    对于具有反射功能的语言来说,反射是关于编译器愿意在您的目标代码中保留多少源代码以启用反射,以及有多少分析机制可用于解释该反射信息。除非编译器保留所有源代码,否则反射将限制其分析有关源代码的可用事实的能力。

    C++ 编译器不保留任何内容(好吧,忽略 RTTI),因此您不会在语言中获得反射。 (Java 和 C# 编译器只保留类、方法名称和返回类型,因此您可以获得一些反射数据,但您无法检查表达式或程序结构,这意味着即使在那些“启用反射”的语言中您可以获得的信息非常稀少,因此您确实无法进行太多分析)。

    但您可以超越语言并获得完整的反射能力。 reflection in C 上另一个堆栈溢出讨论的答案讨论了这个。

    【讨论】:

      【解决方案6】:

      Reflection can be and has been implemented in c++ before.

      它不是原生 c++ 功能,因为它的成本很高(内存和速度),不应该由语言默认设置 - 该语言是面向“默认情况下的最大性能”的。

      由于您不应该为不需要的东西付费,而且正如您自己所说,在编辑器中比在其他应用程序中更需要它,所以它应该只在您需要的地方实现,而不是“强迫”所有人代码(您不需要对将在编辑器或其他类似应用程序中使用的所有数据进行反思)。

      【讨论】:

      • 并且您不提供符号,因为它会让您的客户/竞争对手查看您的代码......这通常被认为是一件坏事。
      • 你说得对,我什至没有考虑代码公开问题:)
      【解决方案7】:

      C++ 没有反射的原因是,这将需要编译器将符号信息添加到目标文件中,例如类类型具有哪些成员、有关成员的信息、有关函数的信息以及所有内容。这实质上会使包含文件变得无用,因为声明传递的信息将从这些目标文件(然后是模块)中读取。在 C++ 中,通过包含相应的头文件,类型定义可以在程序中多次出现(前提是所有这些定义都相同),因此必须决定将有关该类型的信息放在哪里,就像命名一个一样这里的并发症。 C++ 编译器进行的积极优化,可以优化出几十个类模板实例,是另一个强项。这是可能的,但由于 C++ 与 C 兼容,这将成为一个尴尬的组合。

      【讨论】:

      • 我不明白编译器的积极优化如何成为强项。你能详细说明吗?如果链接器可以删除重复的内联函数定义,那么重复的反射信息有什么问题?对于调试器,符号信息是否仍然添加到目标文件中?
      • 问题是你的反射信息可能无效。如果编译器消除了 80% 的类定义,你的反射元数据会说什么?在 C# 和 Java 中,语言保证如果您定义一个类,它就会保持定义。 C++ 让编译器对其进行优化。
      • @Rob,优化是另一点,与多类复杂性无关。请参阅@jalf 的评论(和他的回答)了解我的意思。
      • 如果我实例化了 reflect,那么不要丢弃任何 T 的信息。这似乎不是一个无法解决的问题。
      【解决方案8】:

      在 C++ 中使用反射的情况有很多,使用模板元编程等编译时构造无法充分解决。

      N3340 提出富指针作为在 C++ 中引入反射的一种方式。除其他事项外,它还解决了除非您使用某项功能才为该功能付费的问题。

      【讨论】:

        【解决方案9】:

        据 Alistair Cockburn 称,subtyping can't be guaranteed in a reflective environment

        反射与潜在类型系统更相关。在 C++ 中,您知道自己拥有什么类型,并且知道可以用它做什么。

        【讨论】:

        • 更一般地说,在不引入未定义行为的情况下检查不存在的特征的能力使得将该特征添加到类的更高版本将改变明确定义的行为成为可能预先存在的程序,因此无法保证添加该功能不会“破坏”某些东西。
        【解决方案10】:

        这基本上是因为它是“可选的额外”。许多人选择 C++ 而不是 Java 和 C# 等语言,以便他们可以更好地控制编译器输出,例如更小和/或更快的程序。

        如果您选择添加反射,则有various solutions available

        【讨论】:

          【解决方案11】:

          如果 C++ 可以:

          • 变量名、变量类型和const修饰符的类成员数据
          • 函数参数迭代器(仅位置而不是名称)
          • 函数名称、返回类型和const 修饰符的类成员数据
          • 父类列表(与定义的顺序相同)
          • 模板成员和父类的数据;扩展的模板(意味着反射 API 可以使用实际类型,而不是“如何到达那里的模板信息”)

          这足以在当今 Web 和数据库应用程序中普遍存在的无类型数据处理的关键中创建非常易于使用的库 (所有的 orms、消息传递机制、xml/json 解析器、数据序列化等)。

          例如Q_PROPERTY宏(Qt Framework的一部分)支持的基本信息 http://qt.nokia.com/doc/4.5/properties.html 扩展到涵盖类方法和 e) - 将对 C++ 和整个软件社区非常有益。

          当然,我所指的反思不会涵盖语义或更复杂的问题(如 cmets 源代码行号、数据流分析等)——但我也不认为这些都需要成为语言标准的一部分.

          【讨论】:

          • @Vlad:是的,如果添加支持反射的特性到语言中,你就会在语言中得到反射。这只有在语言委员会下令时才有可能发生,我认为他们在 2011 年还没有,而且我怀疑在公元 2020 年之前还会有另一个 C++ 标准。所以,很好的想法。与此同时,如果你想取得进步,你可能不得不走出 C++ 之外。
          【解决方案12】:

          在过去的 10 年中,一直在尝试向 C++ 添加反射。最新的提案是针对,可能会也可能不会。

          与大多数语言中的反射不同, 反射的计划是编译时反射。所以在编译时,你可以对结构成员、函数和方法的参数和属性、枚举值和名称等进行反射。

          然后您可以进行有限的具体化,注入有关您反映的内容的信息以生成其他类型和代码。

          虽然这有点奇怪,但这意味着不使用反射的程序不会为此支付运行时间成本。它也非常强大。

          最简单的例子就是你可以用它来实现运行时反射。

          struct Member {
            std::string_view name;
            std::any_ref value;
          };
          
          struct Reflectable {
            virtual std::span<Member> GetMembers() const = 0;
            virtual std::span<Member> GetMembers() = 0;
          };
          
          template<class D>
          struct ImplReflectable:Reflectable {
            std::span<Member> GetMembers() const final;
            std::span<Member> GetMembers() final;
          };
          template<class D>
          std::span<Member> ImplReflectable<D>::GetMembers() const {
            // compile time reflection code on D here
          }
          template<class D>
          std::span<Member> ImplReflectable<D>::GetMembers() {
            // compile time reflection code on D here
          }
          

          上面你写了一次,突然你想要反射的任何类型,你可以这样做:

          struct Point : ImplReflectable<Point> {
            int x, y;
          };
          

          一个反射系统附加到Point

          实现此运行时反射的库可以随心所欲地复杂和强大。每种类型都必须做一些工作(如上述)才能选择加入,但对于 UI 库(例如)这样做并不是一个严重的问题。不选择加入的类型继续 C++ 假设“如果你不使用它,就不要付费”。

          但这仅仅是开始。一个提案,元类,许可:

          interface Reflectable {
            std::span<Member> GetMembers() const;
            std::span<Member> GetMembers();
          };
          

          你可以有元类,或者接受类型并返回它们的函数。这允许您定义类的元类,例如用语言编写的“接口”。现在,interface 有点像玩具,但您可以编写 QObjectReflectablePolymorphicValueTypeNetworkProtocol 元类来修改您的类定义的含义。

          这可能会或可能不会进入。它继续变得更好,但它也继续被推迟。对于大多数主要的 C++ 编译器,您可以尝试多种编译时反射实现。语法不断变化,因为有基于符号运算符的反射库、基于reflexpr 的运算符反射库,其中一些反射数据是类型,另一些是constexpr 对象和consteval 函数。

          【讨论】:

            【解决方案13】:

            C++ 中的反射,我认为如果 C++ 被用作数据库访问、Web 会话处理/http 和 GUI 开发的语言,这至关重要。缺少反射会阻止 ORM(如 Hibernate 或 LINQ)、实例化类的 XML 和 JSON 解析器、数据序列化和许多其他事情(最初必须使用无类型数据来创建类的实例)。

            可以使用在构建过程中可供软件开发人员使用的编译时间开关 消除这种“你为所用的东西付费”的担忧。

            我是固件开发者不需要反射来从串行端口读取数据——那么就可以不用开关了。但是,作为一名想要继续使用 C++ 的数据库开发人员,我不断地使用一种可怕的、难以维护的代码来映​​射数据成员和数据库结构之间的数据。

            Boost 序列化和其他机制都没有真正解决反射问题——它必须由编译器完成——一旦完成,C++ 将再次在学校中使用并用于处理数据处理的软件中

            对我来说,这个问题 #1(原生线程原语是问题 #2)。

            【讨论】:

            • 谁说 C++ 被用作数据库访问、Web 会话处理或 gui 开发的语言?有很多更好的语言可用于此类内容。编译时切换不会解决问题。通常,启用或禁用反射的决定不会基于每个文件。如果可以在单个类型上启用它,它就可以工作。如果程序员在定义类型时可以用属性或类似的方式指定,是否应该为其生成反射元数据。但是全球开关?你会削弱 90% 的语言,只是为了简化 10% 的语言。
            • 那么,如果我想要一个跨平台并且可以访问 gui 的程序,我应该使用什么?不灵活的java swing? windows只有C#?但说实话,事实上有很多程序是编译成可执行代码,并提供 gui 接口和访问数据库的,所以他们必须使用一些数据库和 gui 支持...... t 使用 QT。 (它应该被命名为MT(怪物工具包))
            • @Coyote21:C# 多年来一直不是仅限 Windows 的。 (虽然我不是 Mono 的粉丝,但它适用于大多数东西。)而且 Swing 并不是唯一的 Java 图形用户界面工具包。说实话,如果您想要跨平台,任何一个都是更好的选择。如果您正在做任何不平凡的事情,C++ 几乎总是会在这里或那里拥有特定于平台的部分。
            • 没有理由需要对 ORM 进行反射。您可以使用模板实现所有这些。有很多框架为 C++ 提供 ORM。
            • 这实际上是如何回答这个问题的?
            【解决方案14】:

            反射可以是可选的,例如预处理器指令。类似的东西

            #pragma enable reflection

            这样我们就可以两全其美,如果没有这个编译指示库,就可以在没有反射的情况下创建(没有讨论过的任何开销),那么个人开发人员是否想要速度或易用性将取决于他们。

            【讨论】:

            • 这如何回答实际提出的问题?
            【解决方案15】:

            C++ 是一种不需要反射的语言,因为 C++ 是一种您可以用来编写具有反射的语言的语言。

            【讨论】:

            • C++ 可以像任何其他语言一样从反射中受益。正是这些好处直接违背了它的最高性能和资源效率目标。
            猜你喜欢
            • 2018-05-14
            • 2016-10-12
            • 2010-09-07
            • 2016-03-03
            • 1970-01-01
            • 2021-05-13
            • 1970-01-01
            相关资源
            最近更新 更多