【问题标题】:Compiling Java Generics with Wildcards to C++ Templates将带有通配符的 Java 泛型编译为 C++ 模板
【发布时间】:2012-06-06 10:53:28
【问题描述】:

我正在尝试构建一个 Java 到 C++ 的反编译器(即 Java 代码进入,语义上“等效”(或多或少)C++ 代码出来)。

不考虑垃圾回收,语言很熟悉,所以整个过程已经很好了。然而,一个问题是 C++ 中不存在的泛型。当然,最简单的方法是像 java 编译器那样执行擦除。但是,生成的 C++ 代码应该很好处理,所以如果我不会丢失泛型类型信息会很好,也就是说,如果 C++ 代码仍然可以使用List<X> 而不是List,那就太好了。否则,C++ 代码在使用此类泛型的任何地方都需要显式转换。这容易出错且不方便。

所以,我试图找到一种方法来以某种方式获得更好的泛型表示。当然,模板似乎是一个不错的选择。尽管它们是完全不同的东西(元编程与仅编译时类型增强),但它们仍然很有用。只要不使用通配符,只需将泛型类编译为模板就可以很好地工作。但是,一旦通配符开始发挥作用,事情就会变得非常混乱。

例如,考虑以下列表的 java 构造函数:

class List<T>{
List(Collection<? extends T> c){
    this.addAll(c);
}
}

//Usage
Collection<String> c = ...; 
List<Object> l = new List<Object>(c);

如何编译这个?我有在模板之间使用链锯重新解释演员表的想法。然后,上面的例子可以这样编译:

template<class T>
class List{
List(Collection<T*> c){
    this.addAll(c);
}
}

//Usage
Collection<String*> c = ...; 
List<Object*> l = new List<Object*>(reinterpret_cast<Collection<Object*>>(c));

但是,问题是这种重新解释的演员阵容是否会产生预期的行为。当然,它很脏。但它会起作用吗?通常,List&lt;Object*&gt;List&lt;String*&gt; 应该具有相同的内存布局,因为它们的模板参数只是一个指针。但这有保证吗?

我想到的另一个解决方案是将使用通配符的方法替换为实例化每个通配符参数的模板方法,即将构造函数编译为

template<class T>
class List{

template<class S>
List(Collection<S*> c){
    this.addAll(c);
}
}

当然,所有其他涉及通配符的方法,例如addAll,也需要模板参数。这种方法的另一个问题是处理类字段中的通配符。我不能在这里使用模板。

第三种方法是混合方法:将泛型类编译为模板类(称为T&lt;X&gt;)和擦除类(称为E)。模板类T&lt;X&gt; 继承自擦除类E,因此始终可以通过向上转换为E 来放弃泛型。然后,所有包含通配符的方法都将使用擦除类型编译,而其他方法可以保留完整的模板类型。

您如何看待这些方法?您在哪里看到它们的缺点/优点? 对于如何尽可能干净地实现通配符,同时在代码中保留尽可能多的通用信息,您还有其他想法吗?

【问题讨论】:

  • " 虽然它们是完全不同的东西(元编程与仅编译时类型增强)," C++ 模板两者都提供。
  • 是否有效取决于Collection的实现。如果它有任何虚函数,我怀疑它是从 Java 翻译过来的,它有未定义的行为(这不应该被认为是暗示如果没有虚函数就可以了)。正如我在另一个问题中提到的那样,reinterpret_cast 并不是非常有用。
  • 为什么?当然,它有虚方法。但是,如果它们共享相同的内存布局,则将 Collection&lt;Object*&gt; 的方法调用到实际上是 Collection&lt;String*&gt; 的对象上应该没问题。
  • 首先,因为reinterpret_cast 的结果是未指定的。其次,因为它会导致违反别名规则,导致未定义的行为。

标签: java c++ templates generics compiler-construction


【解决方案1】:

不考虑垃圾回收,语言很熟悉,所以整个过程已经很好了。

没有。虽然这两种语言实际上看起来相当相似,但它们在“如何完成”方面显着不同。您尝试的这种 1:1 反编译将导致糟糕的、性能不佳的并且很可能出现错误的 C++ 代码,尤其是如果您不是在查看独立的应用程序,而是在查看可能与“普通”、手动编写的 C++ 接口。

C++ 需要与 Java 完全不同的编程风格。这从 not 开始,所有类型都派生自 Object,涉及避免 new 除非绝对必要(然后尽可能将其限制在构造函数中,在析构函数中使用相应的 delete - 或者更好的是,遵循以下 Potatoswatter 的建议),并且不会以“模式”结束,例如使您的容器符合 STL 标准并将 begin- 和 end-iterators 传递给另一个容器的构造函数而不是整个容器。我也没有在您的代码中看到 const-correctness 或 pass-by-reference 语义。

请注意,有多少早期 Java“基准”声称 Java 比 C++ 更快,因为 Java 传播者采用 Java 代码并将其 1:1 转换为 C++,就像您计划做的那样。这样的转编译没有什么可取之处。

【讨论】:

  • 我确信会发布这样的答案......我更熟悉 C++,因为代码可能看起来像。当然,我从生成运行代码而不是干净、优雅、高性能的代码开始。让程序写出好的 C++ 比自己写要难得多。我知道您发布的所有问题,我已经处理了它们,它们超出了这个问题的范围。当然,生成的代码看起来不像本地编写的 C++ 代码,也不会使用符合 STL 的容器。这不是重点。
  • @gexicide:这是问题的核心。如果你已经“处理”了“所有的问题”,你就不会拥有这个特定的问题。我不是在谈论“lcean、优雅、高性能”的代码。我说的是有意义的代码,它的目的不仅仅是“它可以完成,技术上”。如果您不同意,请忽略此答案。
  • +1,但new 比简单地将delete 放入析构函数更难使异常安全。最好对所有内容使用智能指针和容器类。
  • @gexicide:“C++ 中不存在泛型”?模板是“完全不同的东西(元编程与仅编译时类型增强)”,但“它们仍然有用”?
  • 不同并不差。并且(Java)泛型在 C++ 中不存在,这是事实。我将模板(即元编程)视为比(Java)泛型强大得多的功能,确实如此。 “仍然有用”的意思是“尽管它们有所不同,但对我的应用程序仍然有用”。我看到了 C++ 功能的强大,它们只是不适用于手头的问题。
【解决方案2】:

您尚未讨论的一种方法是使用包装类模板处理通用通配符。因此,当您看到Collection&lt;? extends T&gt; 时,您将其替换为模板的实例化,该实例化公开了一个只读[*] 接口,如Collection&lt;T&gt;,但包装了Collection&lt;?&gt; 的一个实例。然后你在这个包装器(以及其他类似的包装器)中进行类型擦除,这意味着生成的 C++ 相当好处理。

您的电锯reinterpret_cast保证可以正常工作。例如,如果在String 中存在多重继承,那么通常甚至不可能将String* 键入为Object*,因为从String*Object* 的转换可能涉及对地址(不仅如此,还有虚拟基类)[**]。我希望您将在 C++-from-Java 代码中对接口使用多重继承。好的,所以它们将没有数据成员,但它们将具有虚函数,并且 C++ 对您想要的东西没有特别的考虑。我认为使用标准布局类,您可能可以重新解释指针本身,但是(a)这对您来说太强了,并且(b)它仍然不意味着您可以重新解释集合。

[*] 或其他。我忘记了通配符在 Java 中如何工作的详细信息,但是当您尝试将 T 添加到 List&lt;? extends T&gt; 时会发生什么,而 T 原来不是 ? 的实例,请执行那 :-) 棘手的部分是为任何给定的泛型类或接口自动生成包装器。

[**] 而且因为严格的别名禁止它。

【讨论】:

    【解决方案3】:

    如果目标是在 C++ 中表示 Java 语义,那么请以最直接的方式进行。不要使用reinterpret_cast,因为它的目的是击败 C++ 的原生语义。 (在高级类型之间这样做几乎总是会导致程序崩溃。)

    您应该使用引用计数或类似的机制,例如自定义垃圾收集器(尽管在这种情况下这听起来不太可能)。所以这些对象无论如何都会进入堆。

    将通用List 对象放在堆上,并使用单独的类以List&lt;String&gt; 或其他方式访问它。通过这种方式,持久对象具有可以处理 Java 可以表达的任何格式错误的访问方法的泛型类型。访问器类只包含一个指针,您已经拥有用于引用计数的指针(即,它继承了“本机”引用,而不是堆的对象),并公开了适当的向下转换的接口。您甚至可以使用泛型源代码为访问器生成模板。如果你真的想试试。

    【讨论】:

    • 你可能是对的。我的回答试图挽救底层 C++ 集合知道类型,但我怀疑经过一定程度的开发后,我会发现一些“Java 可以表达的格式错误的访问”的边缘案例,放弃并切换到什么你说——只在包装器中强制类型,“真实”集合总是Object*
    • @SteveJessop:关于格式错误的访问...例如,您可以创建一个采用通用 List 的 Java 函数,向其传递一个 List,然后在函数内添加一个字符串到列表... ?:-)
    • @DevSolar:是的,那里会有一个dynamic_cast,用于正常使用。但是,如果 Java 为您提供了完全绕过检查的方法,并成功地将 String 添加到创建为 ArrayList&lt;int&gt; 的集合对象中,那么底层 C++ 容器当然必须是 Object,就像 Java 容器一样是。没有办法解决它。
    • 是的,这个答案似乎是合理的(连同史蒂夫的前一个)。我确实已经使用了引用计数,所以它很适合在那里。此外,创建泛型类的协变和逆变视图并仅将它们传递给带有 co 或逆变通配符的方法的想法听起来是个好主意。
    • 祝你好运……我不是 Java 专家,但从我最初对泛型的阅读来看,你可以做一些事情,比如在运行时获取一个完全动态的类型并将其绑定到未参数化的(术语?) 容器。这样的hackery 根本不适用于C++ 模板的任何使用……但您可能对支持all Java 代码不感兴趣。或者我不知道,也许实际使用这样一个容器的语义足够严格,以至于您可以在某个关键点测试typeid,然后抛出异常或继续使用静态类型而不会丢失功能。可能是。其他事情会变得很棘手。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多