为什么不总是使用模板而不是实际类型

Why not always use templates instead of actual types?

本文关键字:类型 为什么不      更新时间:2023-10-16

为什么不直接使用模板而不是实际类型?我的意思是,那么你在任何时候都不必关心你正在处理什么类型,对吧?还是我错了,我们实际上有使用实际类型(如 int 和 char)的原因吗?

谢谢!

我认为这是一个过度复杂的问题,永远不会带来好处。

考虑一个简单的类:

class Row {
size_t len;
size_t cap;
int* values;
};

注意:你真的会实例化std::vector<int>但让我们把它作为一个熟悉的例子来看......

因此,以这种方式看,我们当然可以通过将其作为values类型的模板来获得好处。

template<typename VALUE>
class Row {
size_t len;
size_t cap;
VALUE* values;
};

这是一场巨大的胜利!我们可以编写一个通用的 Row 类,即使这是(比如)数学包的一部分,这是一个向量空间元组,成员如sum()max()等等,我们也可以使用其他算术类型,如longdouble并构建一个非常有用的模板。

再进一步怎么样?为什么不参数化lencap成员?

template<typename VALUE,typename SIZE>
class Row {
SIZE len;
SIZE cap;
VALUE* values;
};

我们赢得了什么?看起来不是那么多。size_t的目的是成为表示对象大小的合适类型。您可以使用intunsigned或其他任何东西,但您不会获得灵活性(负长度没有意义),您要做的就是任意限制行的大小。

请记住,每次使用Row都必须是模板并接受SIZE的替代方案。这是我们Matrix模板:

template<typename VALUE, typename ROW_SIZE, typename COL_SIZE>
class {
Row< Row<VALUE,ROW_SIZE> , COL_SIZE> rows;
}; 

好的,所以我们可以通过使ROW_SIZECOL_SIZE相同的类型来简化,但最终我们通过选择size_t作为大小的共同点来做到这一点。

我们可以得出合乎逻辑的结论,程序的入口点将变为:

int main() {
main<VALUE,SIZE,/*... many many types ...*/,INDEX_TYPE>();
return EXIT_SUCCESS;
}

其中每个类型决策都是一个参数,并通过所有函数和类串接到入口点。

这存在许多问题:

  1. 这是一场维护噩梦。如果不将埋藏类的类型决策线程化到入口点,则无法更改或添加到埋藏类。

  2. 这将是一场编译噩梦。C++编译速度不快,这将使它变得更糟。对于一个大型程序,我可以想象您甚至可能会耗尽内存,因为编译器解析所有模板的母模板。[更多关于大型应用程序的问题]

  3. 难以理解的错误消息。出于充分的理由,编译器努力在模板中提供易于跟踪的错误。将模板嵌套在模板中,谁知道这将是一个真正的问题。

  4. 您不会获得任何有用的灵活性。这些类型最终是相互关联的,许多杂项类型都有一个很好的答案,无论如何你都不想改变。

最后,如果您确实有一个您认为是应用程序参数的类型(例如某些数学包中的值类型),则参数化的最佳方法是使用typedef。 实际上,typedef double real_type使整个源代码成为一个模板,而没有商店里所有的模板。

您可以typedef float real_typetypedef Rational real_type(其中Rational是一些想象的有理数实现),并真正创建一个灵活的参数化库。

但即便如此,您可能不会typedef size_t size_type或其他任何东西,因为您不希望改变该类型。

因此,总而言之,您最终将做大量工作来提供灵活性,其中大部分您不会使用,并且具有诸如库级别typedef之类的机制,允许您以不那么显眼和劳动密集型的方式参数化您的应用程序。

我会说模板的指南草案是"你需要其中两个吗?如果某些函数或类可能具有具有不同参数的实例,那么答案是模板。如果您认为您有一个针对应用程序的给定实例固定的类型(或值),则应使用编译时常量和库级别typedefs。

有几个原因。我现在将列出其中的一些:

向后兼容性。某些代码库不使用模板,因此您可以只替换所有代码。

代码错误。有时你想确定你得到了一个浮点数/int/char 或者你有什么,以便你的代码运行没有错误。现在,使用模板然后将类型转换回您需要的类型是一个公平的假设,但tat并不总是有效。例如:

#include <iostream>
#include <string>
using namespace std;
void hello(string msg){
msg += "!!!";
std::cout << msg << 'n';
}
int main(){
hello("Hi there"); // prints "Hi there!!!"
}

这行得通。但是用这个函数替换上面的函数是行不通的:

template<typename T>
void hello(T msg){
msg += "!!!";
std::cout << msg << 'n';
}

(注意:一些编译器实际上可能会运行上面的代码,但通常你应该在计算"运算符+=(const char*,char [4])"时出错)

现在有办法解决此类错误,但有时您只需要一个简单的工作解决方案。

一个原因是需要为每个具体类型实例化模板,所以,假设你有这样的函数:

void f(SomeObject object, Int x){
object.do_thing_a(x);
object.do_thing_b(x);
}

Int是模板化的,编译器必须生成一个foodo_thing_ado_thing_b的实例,并且可能为每Intshortunsigned long long生成从do_thing_ado_thing_b调用更多函数。有时这甚至会导致实例的组合爆炸。

此外,出于显而易见的原因,您无法制作虚拟成员函数模板。现在有一种方法编译器可以在编译整个程序之前知道它应该放入vtable中的实例。

顺便说一下,具有类型推断的函数式语言一直在这样做。当你写的时候

f x y = x + y 

在哈斯克尔,你实际上得到了(非常松散地说)接近C++

template<class Num, class A>
A f(A x, A y){
return Num::Add(x, y);
}

然而,在Haskell中,编译器没有义务为每个具体的A生成一个实例。