使用通配符将Java泛型编译为C ++模板

时间:2012-06-06 10:53:28

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

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

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

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

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

List

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

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<Object*>应该具有相同的内存布局,因为它们的模板参数只是一个指针。但这有保证吗?

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

List<String*>

当然,涉及通配符的所有其他方法(如template<class T> class List{ template<class S> List(Collection<S*> c){ this.addAll(c); } } )也需要模板参数。这种方法的另一个问题是例如在类字段中处理通配符。我不能在这里使用模板。

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

您如何看待这些方法?你在哪里看到他们的优势? 您是否有任何其他想法可以尽可能清晰地实现通配符,同时尽可能在代码中保留通用信息?

3 个答案:

答案 0 :(得分:6)

  

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

没有。虽然这两种语言实际上看起来非常相似,但它们与“事情如何完成”的显着显着不同。你正在尝试的这种1:1反式编译将导致可怕的,表现不佳的,并且很可能是错误的C ++代码,如果你不是在寻找一个独立的应用程序,那么尤其是接口“正常”,手动编写的C ++。

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

请注意有多少早期Java“基准”声称Java比C ++更快,因为Java传播者使用Java代码并将其转换为C ++ 1:1,就像您计划要做的那样。这种反编译无法赢得任何东西。

答案 1 :(得分:5)

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

您的电锯reinterpret_cast 保证可以正常工作。例如,如果String中存在多重继承,则通常不可能将String*作为Object*进行输入,因为从String*转换为{{ 1}}可能涉及向地址应用偏移量(超过该值,使用虚拟基类)[**]。我希望你在C ++中使用多重继承 - 从Java代码到接口。好的,所以他们没有数据成员,但他们将拥有虚拟功能,而C ++没有特别允许你想要的东西。我认为对于标准布局类,你可能会重新解释指针本身,但是(a)这对你来说太强大了,而且(b)它仍然不意味着你可以重新解释这个集合。

[*]或者其他什么。我忘记了通配符在Java中如何工作的细节,但是当你尝试将Object*添加到T时应该发生什么,而List<? extends T>结果不是实例T,执行:-)棘手的部分是为任何给定的泛型类或接口自动生成包装器。

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

答案 2 :(得分:4)

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

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

将通用List对象放在堆上,并使用单独的类将其作为List<String>或其他类进行访问。这样,持久对象具有泛型类型,可以处理Java可以表达的任何不正确的访问方式。访问器类只包含一个指针,您已经拥有该指针用于引用计数(即它是“本机”引用的子类,而不是堆的Object),并公开适当的下载接口。您甚至可以使用泛型源代码为访问者生成模板。如果确实想要尝试。