C#泛型 vs C++模板 - 需要关于约束的澄清

11

重复

C#和Java中的泛型与C++中的模板有什么区别?


大家好,

我是一名经验丰富的C++程序员,但对C#还比较新。

那些约束和泛型是怎么回事?为什么它不像在C++中一样,约束是隐式的,并从您对模板类进行的实例化中派生出来的呢?

为什么微软没有使其与C++中的工作方式相同呢?


2
抱歉,我不理解你的评论。Dup是什么? - user88637
1
我认为这个问题确切地涉及到了约束模板。它看起来不像那个其他问题那样通用。这个问题可以使用概念和公理更深入地探讨细节,而不是其他问题的答案。因此,我不会投票关闭。它肯定不是一个完全重复的问题。 - Johannes Schaub - litb
1
我不认为这是一个重复的问题。如果你认为是,那么请阅读这个问题和“重复”的答案。这个问题关注约束条件,而另一个问题则更加广泛。 - John Saunders
1
绝对不是重复,因为C#的泛型与Java的泛型非常不同... - Thomas Levesque
5个回答

20
总的来说,C++模板和C#泛型是相似的——相比之下Java泛型则完全不同,但它们也有很大的区别。就像在C#中一样,通过使用反射进行运行时支持,获取描述用于实例化泛型的类型的对象。C++没有反射,所有与类型相关的操作都在编译时完成。
C#泛型和C++模板最大的区别确实是C#泛型更好的类型检查。它们始终受到约束,即它们不允许在定义泛型时未声明为有效操作的操作。C#的首席设计师认为这是增加复杂性的原因,因此没有隐含约束。我对C#不是很熟悉,所以在这里无法深入讨论。我将谈论C++的情况以及如何改进,以便人们不认为C++的东西全部都是错的。
在C++中,模板没有被限制。如果您执行一个操作,则在模板定义时间暗示该操作将在实例化时间成功。甚至不需要对C++编译器进行语法检查,以验证模板的有效性。如果包含语法错误,则必须在实例化时进行诊断。在那之前的任何诊断都是实现的纯粹的好处。
这些暗示的约束在短期内对模板设计师来说很容易,因为他们不必关心在其模板接口中声明有效操作。他们把负担放在他们模板的用户身上,因此用户必须确保满足所有这些要求。经常发生的情况是,用户尝试看似有效的操作但失败了,编译器给出了数百行错误消息,说明一些无效的语法或未找到的名称。因为编译器无法知道特定约束首先被违反了什么,所以它列出了与故障位置周围涉及的所有代码路径和所有甚至不重要的细节,用户将不得不浏览可怕的错误消息文本。
这是一个根本性的问题,可以通过在模板或泛型的接口中声明类型参数具有哪些属性来解决。就我所知,C#可以将参数限制为实现接口或继承基类。它在类型级别上解决了这个问题。
C++委员会长期以来已经看到需要解决这些问题,很快(明年,可能)C++也将有一种方法来明确规定这样的约束请参见下面的时光机注释),如下例所示。
template<typename T> requires VariableType<T>
T f(T a, T b) {
    return a + b; 
}

编译器在那一点发出错误信号,因为按照要求,该表达式的写法无效。这有助于模板设计者编写更加正确的代码,因为代码已经进行了某种程度的类型检查(在那里可以进行的范围内)。程序员现在可以陈述该要求:
template<typename T> requires VariableType<T> && HasPlus<T, T>
T f(T a, T b) {
    return a + b; 
}

现在,它将编译。编译器通过看到T作为返回类型的方式自动暗示T是可复制的,因为该使用出现在接口中,而不是模板主体中。其他要求是使用要求子句陈述的。现在,如果用户使用没有定义op+的类型,用户将获得适当的错误消息。
C++1x将要求与类型分离。以上内容适用于原始类型和类。在这种意义上,它们更加灵活,但相当复杂。规定何时以及何时满足要求的规则很长……使用新规则,您可以说以下内容:
template<typename T> requires MyCuteType<T>
void f(T t) { *t = 10; }

然后,使用一个 int 调用 f 函数!只需为 MyCuteType<int> 编写一个概念映射,教会编译器如何对 int 进行解引用即可。这在像下面这样的循环中非常方便:

for_each(0, 100, doSomething());

由于程序员可以告诉编译器如何使int满足“输入迭代器”的概念,因此如果只写适当的概念映射,您实际上可以在C++1x中编写这样的代码,这并不难。
好了,够了。我希望我能向您展示,有约束的模板并不是那么糟糕,而实际上更好,因为模板中类型和它们之间的操作关系现在已被编译器所知。我甚至还没有写过关于“公理”的内容,这是C++1x中概念的另一个好东西。请记住,“这是未来的事情”,它还没有推出,但大约在2010年左右就会推出。然后我们将不得不等待一些编译器来实现所有这些 :)

来自“未来”的更新

C++0x概念未被接受进入草案,但已在2009年末被投票淘汰。太遗憾了!但也许我们会在下一个C++版本中再次看到它?让我们都抱有希望!


概念(Concepts)是C++0x规范的一部分,但在此回答发布三个月后被删除,因此不会在2010年发布,可能要等到2015年。 - Simon D

4
C++模板: 编译器会检查参数是否满足代码设置的约束条件。例如:
template <typename T, unsigned int dim>
class math_vector
{
    T elements[dim];

    math_vector<T,dim> operator+ (const math_vector<T,dim>& other) const
    {
        math_vector<T,dim> result;
        for (unsigned int i = 0; i < dim; ++i)
            result.elements[i] = elements[i] + other.elements[i];
    }
}

struct employee
{
    char name[100];
    int age;
    float salary;
}

math_vector<int, 3> int_vec; //legal
math_vector<float, 5> float_vec; //legal
math_vector<employee, 10> employee_vec; //illegal, operator+ not defined for employee

在这个例子中,您可以创建一个类,为其定义operator+并将其用作math_vector的参数。因此,仅当类型满足模板代码定义的约束条件时,模板参数才有效。这很灵活,但会导致编译时间长(每次实例化模板时都必须检查类型是否满足模板的约束条件)。 C#泛型:与其检查每个特定实例的有效性(导致编译时间更长且容易出错),您明确声明泛型参数必须实现特定接口(一组方法、属性和运算符)。在泛型代码内部,您无法自由调用任何方法,而只能调用该接口支持的方法。每次实例化泛型时,运行时不必检查参数是否满足一长串约束条件,而只需检查它是否实现了指定的接口。当然,这样做的灵活性较小,但出错的可能性也较小。例如:
class SortedList<T> where T : IComparable<T>
{
    void Add(T i) { /* ... */ }
}

class A : IComparable<A> { /* ... */ }

class B
{
    int CompareTo(B b) { /* ... */ }
    bool Equals(B b) { /* ... */ }
}

SortedList<A> sortedA; // legal
SortedList<B> sortedB; // illegal
// B implements the methods and properties defined in IComparable,
// however, B doesn't explicitly implement IComparable<B>

3
很快你会得到更好的回答。到那时,我会删除这个回答。
C++中的模板类似于宏,不同之处在于模板是在实例化时编译的,如果隐含的约束条件被违反,则会显示编译错误。这就是如何进行模板特化 - 模板基本上已经被特化展开,因此使用的是特化模板。
.NET中的泛型(也适用于VB.NET)是一种运行时结构,是一种特殊类型。约束条件是必要的,以确保在最终使用类型时任何实际使用都是有效的。
实际上,您可以使用反射来查看泛型类型并找到用于实例化它的类型参数,或查看泛型定义并查看每个类型参数上的约束。在C++中,这些信息在运行时已经消失了。

C++模板不同于宏,它们是由编译器处理的,而不是宏处理器。无论模板是否被实例化,代码都会被编译器处理。 - anon
在C++中,您无法反射或获取有关实例化模板的元类型信息。但是,您可以部分特化模板并获取使用的模板参数,或者使用typedef导出参数(非常常见)。 - Johannes Schaub - litb
1
C++模板与宏毫不相似,这是完全不同的机制。 - Nemanja Trifunovic
4
请冷静。在这个语境中,它们类似于宏,因为它们像文本扩展一样被编译器编译。 .NET 泛型是“在运行时扩展”的(这就是它们不叫模板的原因,因为它们与 C++ 的模板不同)。 - Denis Troller

2

C# 泛型与 C++ 完全不同。

在 C# 中,编译器基本上为所有对象类型编译一个类定义,并为每个值类型编译一个类定义。

而在 C++ 中,每种类型都有自己的类定义。

约束只是为了让编译器从其他位置推断出一些信息。

我建议看一下 Action<T>Func<T, T>Predicate<T> 委托以及相关的 IEnumerable 扩展方法。使用 lambda 函数并结合这些委托,你就能看到约束所起的作用。


我认为这些约束确实是C++模板的一个重要区别。下一个C++版本将引入概念,这也将向C++引入有约束的模板。每个操作都必须在模板定义时通过需求条款声明有效,就像在C#中一样,但更加强大。 - Johannes Schaub - litb
我的观点是,这些约束条件并没有什么神奇的地方;它们只是用于编译器的类型检查而已。 - Michael Hedgpeth

0

模板和泛型确实是两个不同的概念。

泛型的一个目标是能够以跨库、跨语言的方式使用它们,这与 C++ 模板不同。它们是 CLR 的概念,而不是语言概念(尽管它们显然需要语言支持)。

在 C++ 中,模板可以被看作是“超级宏”,因为你可以将它们视为文本扩展,然后进行编译。这使得它们能够使用模板参数中定义的任何内容(主要是运算符),因为约束是由使用它们的代码所强制的。

在 .NET 中,由于泛型是在运行时解析(实例化)的,所以必须在定义级别上施加约束,以便编译器可以确保它们的使用是有效的(这意味着您不能在泛型参数上使用运算符,因为您无法指示存在运算符的约束)。

正如我所说,泛型的主要目的是能够创建通用的 DLL 供其他项目使用。这就是它们与其他概念的不同之处。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接