为什么不总是使用模板而不是实际类型
Why not always use templates instead of actual types?
为什么不直接使用模板而不是实际类型?我的意思是,那么你在任何时候都不必关心你正在处理什么类型,对吧?还是我错了,我们实际上有使用实际类型(如 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()
等等,我们也可以使用其他算术类型,如long
和double
并构建一个非常有用的模板。
再进一步怎么样?为什么不参数化len
和cap
成员?
template<typename VALUE,typename SIZE>
class Row {
SIZE len;
SIZE cap;
VALUE* values;
};
我们赢得了什么?看起来不是那么多。size_t
的目的是成为表示对象大小的合适类型。您可以使用int
或unsigned
或其他任何东西,但您不会获得灵活性(负长度没有意义),您要做的就是任意限制行的大小。
请记住,每次使用Row
都必须是模板并接受SIZE
的替代方案。这是我们Matrix
模板:
template<typename VALUE, typename ROW_SIZE, typename COL_SIZE>
class {
Row< Row<VALUE,ROW_SIZE> , COL_SIZE> rows;
};
好的,所以我们可以通过使ROW_SIZE
与COL_SIZE
相同的类型来简化,但最终我们通过选择size_t
作为大小的共同点来做到这一点。
我们可以得出合乎逻辑的结论,程序的入口点将变为:
int main() {
main<VALUE,SIZE,/*... many many types ...*/,INDEX_TYPE>();
return EXIT_SUCCESS;
}
其中每个类型决策都是一个参数,并通过所有函数和类串接到入口点。
这存在许多问题:
这是一场维护噩梦。如果不将埋藏类的类型决策线程化到入口点,则无法更改或添加到埋藏类。
这将是一场编译噩梦。C++编译速度不快,这将使它变得更糟。对于一个大型程序,我可以想象您甚至可能会耗尽内存,因为编译器解析所有模板的母模板。[更多关于大型应用程序的问题]
难以理解的错误消息。出于充分的理由,编译器努力在模板中提供易于跟踪的错误。将模板嵌套在模板中,谁知道这将是一个真正的问题。
您不会获得任何有用的灵活性。这些类型最终是相互关联的,许多杂项类型都有一个很好的答案,无论如何你都不想改变。
最后,如果您确实有一个您认为是应用程序参数的类型(例如某些数学包中的值类型),则参数化的最佳方法是使用typedef
。 实际上,typedef double real_type
使整个源代码成为一个模板,而没有商店里所有的模板。
您可以typedef float real_type
或typedef Rational real_type
(其中Rational
是一些想象的有理数实现),并真正创建一个灵活的参数化库。
但即便如此,您可能不会typedef size_t size_type
或其他任何东西,因为您不希望改变该类型。
因此,总而言之,您最终将做大量工作来提供灵活性,其中大部分您不会使用,并且具有诸如库级别typedef
之类的机制,允许您以不那么显眼和劳动密集型的方式参数化您的应用程序。
我会说模板的指南草案是"你需要其中两个吗?如果某些函数或类可能具有具有不同参数的实例,那么答案是模板。如果您认为您有一个针对应用程序的给定实例固定的类型(或值),则应使用编译时常量和库级别typedef
s。
有几个原因。我现在将列出其中的一些:
向后兼容性。某些代码库不使用模板,因此您可以只替换所有代码。
代码错误。有时你想确定你得到了一个浮点数/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
是模板化的,编译器必须生成一个foo
、do_thing_a
、do_thing_b
的实例,并且可能为每Int
short
或unsigned long long
生成从do_thing_a
和do_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
生成一个实例。
- 为什么"fun(i)"被推导出为"fun<int&>"而不是"fun<int>",因为"i"是"int"的类型而不是参考?
- 通过构造函数方法输出的类到类类型转换是 5500 为什么不是 5555
- 为什么不需要在 C++20 中的依赖类型之前指定"typename"?
- 为什么不调用预期的函数?我是否对类型特征的理解不正确?
- 为什么不能在C++中重新定义类中的类型名称?
- 为什么函数返回类型中不允许参数推导?
- 为什么不采用非类型部分专用元组?
- 为什么类型变量;不调用默认 CTR
- 为什么指定数据类型而不是构造功能参数?C
- 为什么 int 对象和函数类型之间不明确?
- 为什么不能在模板函数中向局部变量添加低级 const 类型
- 为什么不总是使用模板而不是实际类型
- 为什么不在这个具有自动返回类型的函数中省略复制?
- 为什么不能在类声明(不完整类型)中使用"is_base_of"?
- 为什么与非类型参数相反,为什么不可见/存在模板类型参数
- 为什么不允许 std::variant 与其替代类型之一相等?
- 为什么C++模板类型匹配不检索引用限定符"&"?
- 为什么模板<类型名...>不能通过模板<模板类型名>识别为可实例化<typename>?
- 为什么 std::bitset 只支持整型数据类型?为什么不支持浮点数?
- 为什么将指针铸成数字类型是不安全的