【问题标题】:What makes a template different from a generic?是什么让模板与泛型不同?
【发布时间】:2023-03-24 07:10:01
【问题描述】:

我了解 C++ 中的模板与 Java 和 C# 中的泛型不同的方面。 C# 是一种具体化,Java 使用类型擦除,C++ 使用鸭子类型,等等。C++ 模板可以做许多 Java 和 C# 泛型不能做的事情(例如模板特化)。但是Java 泛型可以做很多 C# 和 C++ 不能做的事情(例如,为像 class Foo<T extends Comparable<?>> 这样的泛型系列创建有界类型参数),以及很多事情 C# 泛型可以做到 Java 和 C++ 做不到的(例如运行时通用反射)。 [编辑:显然 Java 泛型比我想象的要弱得多。 (这是在说些什么。)无论如何,尽管它们无能,但它们仍然与 C# 的泛型一起被视为泛型。]

我不明白是什么在概念上使模板与泛型不同。 C++ 模板的哪些部分是不能在不是模板但是泛型的东西中完成的事情?例如,如果我要实现一种支持模板的语言,那么其中绝对需要什么?对于支持泛型的语言,我可以省略什么?

我的猜测是模板是泛型的超集,或者它们是实现泛型的一种方式,但我不太明白真正的模板与真正的泛型的区别。

【问题讨论】:

  • 泛型和模板都允许相同的源代码用于不同的类型。主要区别在于源代码编译成什么。简而言之:泛型 = 所有 类型(满足约束)的相同字节码;模板 = 实际使用的 每个 类型的不同机器代码。
  • 所以泛型和模板是解决同一个问题的两种方法(使用不同类型的相同源代码),但没有一个是另一个的超集。对于泛型,C# 和 Java 再次遵循不同的方法,C# 具有对泛型的运行时支持,而 Java 没有。
  • 你有一个事实不准确。 Java 中的有界类型参数近似于 C# 中的泛型类型约束。
  • @spender Java 所独有的并不是有界参数本身。 Java 允许您将它们与通配符结合使用,但 C# 没有通配符。通配符允许您指定一系列泛型。我在 C# 中的 Java 示例必须是 class Foo<T, U> where T : Comparable<U>
  • @dtb C# 为值类型的实例化生成单独的字节码。例如。 List 和 List 不共享字节码。

标签: c# java c++ templates generics


【解决方案1】:

嗯.. 如果您说您深入了解 C++ 模板并说您看不到/感觉泛型与它们之间的区别,那么您很可能是对的 :)

有许多差异将描述泛型如何/为什么比模板更好,列出大量差异等,但这与想法的核心几乎无关。

这个想法是为了更好地重用代码。模板/泛型为您提供了一种构建某种高阶类定义的方法,这些定义抽象了一些实际类型。

在这个术语中,它们之间没有区别,唯一的区别是由底层语言和运行时的特定功能和约束强制执行的那些。

有人可能会争辩说,泛型提供了一些额外的功能(通常在谈论对象的类树的动态自省时),但其中很少(如果有的话)不能在 C++ 的模板中手动实现.通过一些努力,它们中的大多数都可以实现或模拟,因此它们不能很好地区分“适当的泛型”和“真实模板”。

其他人会争辩说,由于 C++ 的复制粘贴行为,优化的巨大潜力是不同的。对不起,不是真的。 Java 和 C# 中的 JIT 也可以做到这一点,嗯,几乎,但做得很好。

然而,有一件事确实可以使 Java/C# 的泛型成为 C++ 模板功能的真正子集。而且你还提到过!

这是模板专业化

在 C++ 中,每个特化都表现为完全不同的定义。

在 C++ 中,专用于 T==int 的 template<typename T> Foo 可能如下所示:

class Foo<int> 
{
    void hug_me();

    int hugs_count() const;
}

虽然专用于 T==MyNumericType 的“相同”模板可能看起来像

class Foo<MyNumericType> 
{
    void hug_me();

    MyNumericType get_value() const;
    void  reset_value() const;
}

仅供参考:这只是伪代码,不会编译:)

Java 和 C# 的泛型都无法做到这一点,因为它们的定义声明所有泛型类型实现都将具有相同的“用户界面”。

更重要的是,C++ 使用 SFINAE 规则。模板可能存在许多“理论上冲突”的专业化定义。但是,在使用模板时,只会使用那些“实际上很好”的模板。

如果你使用与上例类似的类:

 Foo<double> foood;
 foood.reset_value();

只会使用第二个特化,因为第一个不会编译,因为 ... "reset_value" 缺失。

使用泛型,您无法做到这一点。您需要创建一个具有所有可能方法的泛型类,然后在运行时动态检查内部对象并为不可用的方法抛出一些“未实现”或“不支持”异常。这……太可怕了。这样的事情在编译时应该是可能的。

模板专业化SFINAE的实际威力、影响、问题和整体复杂性是泛型和模板的真正区别所在。简而言之,泛型以这样一种方式定义,即不可能进行专门化,因此 SFINAE 是不可能的,因此,自相矛盾的是,整个机制更容易/更简单。

在编译器的内部实现更容易/更简单,并且非专业人士也能理解。

虽然我同意 Java/C# 中泛型的整体优势,但我真的很怀念专业化、接口灵活性和 SFINAE 规则。但是,如果我不提及与健全的 OO 设计相关的一件重要事情,我将不公平:如果您对类型 xxx 的模板专业化实际上更改了它的客户端 API,那么很可能它应该命名不同并且应该形成不同的模板.模板可以做的所有额外好处大部分都添加到了工具集中,因为......在 C++ 中没有反射,它必须以某种方式进行模拟。 SFINAE 是一种编译时反射。

因此,差异世界中最大的参与者被简化为一个奇怪的(有益的)副作用,即应用修补程序来掩盖运行时的缺陷,即几乎完全缺乏运行时内省:))

因此,我说除了语言强制执行的一些任意的,或者运行时平台强制执行的一些任意的之外,没有区别。

它们都只是高阶类或函数/方法的一种形式,我认为这是最重要的东西和特性。

【讨论】:

  • 与健全的 OO 设计相关的一件重要事情...模板是与 OO 不同的范式。如果您尝试使用 OO 眼镜查看模板,您将看不到重要的细节,或者您不会喜欢您所看到的。
  • 我完全同意!这就是我说“最有可能”和“客户端 API”的原因,因为如果你创建一个会发出嘎嘎声而不是持有对象的 vector 特化,那么它应该是一个“鸭子”,而不是“向量”:) 或“quacktor”至少..
  • 嗯...有趣的是你带来了这个特定的例子,因为在 quacks i> 喜欢矢量,但不存储像矢量一样的对象...
  • 我想你误解了我的意图.. vector 规范实际上存储值(不知何故)并提供API典型的向量。因此,即使就 OO 而言,它也是一个适当的向量。就 OO 而言,只要满足已发布的 API,实现在内部执行的操作是无关紧要的。我所说的“quack”是指“完全不同的东西”,比如删除 .size .at .push 方法并提供字面意义上的 .quack .move .fly - 模板专业化通常完全可以做到这一点:)
  • 我认为我们用不同的方式说同样的话。特化允许对仍然以某种方式属于同一协议的对象进行适当的修改(尽管不完全是,std::vector&lt;T&gt;::front() 为除bool 以外的任何类型返回T&amp;,但如果存储的类型为bool,则返回一个代理对象。这是对特化的有效使用。当然,如果您缩小派生类型的合同,您可以滥用它并以与普通继承相同的方式破坏它)
【解决方案2】:

首先,有趣的是,RTTI/内省是大多数答案的重要组成部分。好吧,这不是泛型与模板的区​​别,而是具有自省的语言与没有自省的语言的区别。否则,您也可以声称这是 C++ 类与 Java 类的区别,以及 C++ 函数与 Java 函数的区别......

如果您不考虑自省,主要区别在于模板定义了一种图灵完备的语言,在风格上具有功能性,尽管您可以在其上编程的语法很糟糕。我听说的第一个非常复杂的示例(我很想拥有代码,但我没有)是一个在编译时计算素数的程序。这确实带来了另一个区别:模板可以采用类型参数、模板参数或非类型参数(non-type 指的是任何不是类型或模板的东西,例如 int 值) .

这在其他答案中已经提到过,但只是说模板可以专门化并且有 SFINAE 并没有明确说明这两个功能足以生成图灵完整的语言。

【讨论】:

  • 做了一点狩猎。显然primes program 是第一个 TMP 程序。它打印素数作为编译时错误消息 O_o
【解决方案3】:

Java 泛型可以做很多 C# 和 C++ 不能做的事情(例如 泛型家族的有界类型参数,如class Foo&lt;T extends Comparable&lt;?&gt;&gt;)

这个例子并不完全正确:

template <typename Comparable>
struct Foo {
    static bool compare(const Comparable &lhs, const Comparable &rhs) {
        return lhs == rhs;
    }
};

只有当模板参数是等式可比类型时,此类模板才会成功实例化compare 函数。它不称为“有界类型参数”,但作用相同。

如果在 C++ 中您想将 Comparable 视为显式接口(即基类)而不是鸭子类型的概念,那么您可以 static_assert(is_base_of&lt;Comparable, T&gt;::value, "objects not Comparable"); 或其他。

【讨论】:

  • @Eva:当泛型被发明时,你可能没有关注 Java,但我的观点是,除了只能通过运行时自省来完成的事情之外,泛型的大多数或所有特性都被发明为在 Java 中实现在 C++ 中已经使用模板完成的事情的工具。当然,Java 已经从这个起点开发了自己独特的功能和习语,因为 Java 实现这些目标的方式是不同的。
【解决方案4】:

不,模板不是泛型的超集,对于 C++ 模板,您没有与 C# 泛型相同级别的运行时支持,这意味着 C++ 中的 RTTI 无法检测并为您提供模板的元数据,例如反射适用于 C# 中的泛型。

除此之外,我喜欢这个sn-p:

C++ 模板使用编译时模型。当模板用于 C++程序,效果就像一个复杂的宏处理器有 被使用了。

C#泛型不仅仅是编译器的一个特性,也是一个特性 的运行时。像 List 这样的泛型类型维护它的 编译后的通用性(genericity)。或者,看 换句话说,C++ 编译器在编译时所做的替换 在 C# 泛型世界中,时间是在 JIT 时间完成的。

查看全文:How do C# generics compare to C++ templates?

【讨论】:

  • Java 泛型不是运行时特性,但仍被视为泛型。
  • Java 泛型与 C# 泛型不同,它们只是共享相同的名称。不要以为因为它们叫同一个东西,实际上它们是同一个东西。
  • @JackAidley 它们并不完全相同,但它们确实有很多共同点,因此将它们称为同一事物是明智的。我想知道它们的共性在哪里与模板有很大的不同,以至于它们被称为不同的东西。
  • @Angew 但 C# 的东西也称为“泛型”,在 C# 中 Foo&lt;int&gt;Foo&lt;double&gt; 也有自己的静态成员。
  • RTTI 不提供此类信息这一事实并没有太大区别,因为通常 C++ 中的 RTTI 几乎不会告诉您有关被检查对象的结构的任何信息。这是一个重要的观点,但完全错过了。它与反射/内省功能和缺陷有关,与模板/泛型无关。此外,您引用的 sn-p 有点遗漏,尽管它完全正确。它被遗漏了,因为它再次比较的不是模板/泛型,而是编译器运行时组合提供的运行时自省功能。
【解决方案5】:

这是一个老帖子,我的代表太低了,无法评论accepted answer,但我想补充一下:

除了显式特化之外,C++ 模板和 C# 泛型的另一个关键区别是 C++ 中使用的非类型模板参数:

template<int bar> class Foo {};

Foo<1> a;
Foo<2> b;

a = b; //error, different types. 

非类型模板参数可以是任何整数类型、枚举以及可以在编译时确定的指针(静态存储变量和函数指针)。在 C++20 中,它们也可以是类类型,但有一定的限制。

C# 和 Java 泛型都不能做到这一点。

您也可以明确地专注于非类型参数。

作为旁注,D 编程语言使用术语“模板”作为泛型编程的命名法,至少在我看来,它的特性在精神上与 C++ 一致,而不是 C#/Java。

我不知道为什么非类型参数被排除在 C# 之外的技术原因,但是作为我现在使用的语言比其他语言更多,我偶尔会错过这个功能。

【讨论】:

    【解决方案6】:

    我的答案仅限于 C++ 模板与 Java 泛型。

    1. C++ 模板(类模板和函数模板)是用于 实现编译时多态性,但 AFAIK Java 泛型是运行时的 机制。
    2. 使用 C++ 模板,你可以进行泛型编程,它实际上是 是完全独立的编程风格/范式,但 Java 泛型是 OO 风格本身。见下文:
    3. C++ 模板基于 Duck 类型,但 Java 泛型基于 键入擦除。在 C++ 中,vector&lt;int&gt;vector&lt;Shape *&gt;vector&lt;double&gt;vector&lt;vector&lt;Matrix&gt;&gt; 是 4 种不同的类型,但在 Java 中 Cell&lt;int&gt;Cell&lt;Integer&gt; Cell&lt;double&gt;Cell&lt;Matrix&gt; 是同一类型。更准确地说,在代码生成期间 编译器首先擦除类型。 您可以通过以下论文中的以下代码进行检查: 弗拉基米尔·巴托夫。 Java 泛型和 C++ 模板。 C/C++ 用户杂志,2004 年 7 月。

      public class Cell<E>
      {
         private E elem;
         public Cell(E elem)
         { 
            this.elem = elem;
         }
         public E GetElement()
         { 
            return elem;
         }
         public void SetElement(E elem)
         { 
            this.elem = elem;
         } 
      }
      
      boolean test()
      { 
        Cell<Integer> IntCell(10); 
        Cell<String> StrCell(“Hello”);
        return IntCell.getClass() ==
               StrCell.getClass(); // returns true
      }
      

    简而言之,Java 假装是通用的,而 C++ 实际上是。

    【讨论】:

    • 问题是关于泛型与模板的含义,而不是单一语言的特定方面。
    • @Eva:好吧,也许您应该定义“泛型”的含义,因为您将不同语言中的事物混为一谈,以至于将它们放在一起讨论毫无意义
    • @newacct 我在询问泛型的定义。 C# 泛型和 Java 泛型的术语是泛型,但 C++ 模板提供了许多相同的功能,但没有相同的术语。我在问那些使 C# 和 Java 泛型的共同点,以及它们与模板的区​​别,足以使模板不被称为泛型。已经为我提供了集总。我只是问为什么会这样。
    • @Eva:名字随意。如果 C++ 模板以这种方式命名,则可以将其称为 C++ 泛型。这个名字没有任何意义。 C# 和 Java 泛型非常不同。它们只是被称为相同的。
    • @newacct accepted answerthis comment 解释说差异不是任意的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-06
    • 2011-09-09
    • 2015-04-05
    • 2011-10-12
    • 2012-12-28
    相关资源
    最近更新 更多