Are C++ Templates just Macros in disguise?
我已经在C++中编程了几年,我使用STL相当多,并且已经创建了我自己的模板类几次来看看它是如何完成的。
现在我正试图将模板更深入地集成到我的OO设计中,一个令人不安的想法不断回到我的脑海:它们只是一个宏,真的……如果你真的想,你可以使用定义来实现(相当难看的)自动指针。
这种对模板的思考方式有助于我理解代码的实际工作方式,但我觉得我一定在某种程度上错过了要点。宏意味着邪恶的化身,然而"模板元编程"却风靡一时。
那么,真正的区别是什么?模板如何避免定义导致你陷入的危险,比如
- 中存在无法解释的编译器错误你不期待的地方?
- 代码膨胀?
- 难以跟踪代码?
- 设置调试器断点?
型
宏是文本替换机制。
模板是在编译时执行的功能图灵完整语言,并被集成到C++类型的系统中。您可以将它们视为语言的插件机制。
型
这里有很多注释试图区分宏和模板。
是的-它们都是一样的:代码生成工具。
宏是一种原始形式,没有太多的编译器强制(就像在C语言中执行对象一样——它可以完成,但并不漂亮)。模板更高级,具有更好的编译器类型检查、错误消息等。
然而,每一个都有另一个没有的优势。
模板只能生成动态类类型-宏可以生成几乎任何您想要的代码(除了另一个宏定义)。宏对于将结构化数据的静态表嵌入到代码中非常有用。
另一方面,模板可以完成一些宏所不可能完成的真正有趣的事情。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | template<int d,int t> class Unit { double value; public: Unit(double n) { value = n; } Unit<d,t> operator+(Unit<d,t> n) { return Unit<d,t>(value + n.value); } Unit<d,t> operator-(Unit<d,t> n) { return Unit<d,t>(value - n.value); } Unit<d,t> operator*(double n) { return Unit<d,t>(value * n); } Unit<d,t> operator/(double n) { return Unit<d,t>(value / n); } Unit<d+d2,t+t2> operator*(Unit<d2,t2> n) { return Unit<d+d2,t+t2>(value + n.value); } Unit<d-d2,t-t2> operator/(Unit<d2,t2> n) { return Unit<d-d2,t-t2>(value + n.value); } etc.... }; #define Distance Unit<1,0> #define Time Unit<0,1> #define Second Time(1.0) #define Meter Distance(1.0) void foo() { Distance moved1 = 5 * Meter; Distance moved2 = 10 * Meter; Time time1 = 10 * Second; Time time2 = 20 * Second; if ((moved1 / time1) == (moved2 / time2)) printf("Same speed!"); } |
模板允许编译器动态地创建和使用模板的类型安全实例。编译器实际上在编译时进行模板参数的数学运算,为每个唯一的结果在需要的地方创建单独的类。有一个隐含的Unit<1,-1>(Distance/Time=Velocity)类型,它是在条件内创建和比较的,但从未在代码中显式声明。
显然,大学里有人定义了一个包含40多个参数的模板(需要参考),每个参数代表不同的物理单元类型。想想那种类型的安全性,只为你的数字。
型
它们由编译器来解析,而不是由在编译器之前运行的预处理器来解析。
以下是msdn所说的:http://msdn.microsoft.com/en-us/library/aa903548(vs.71).aspx
Here are some problems with the macro:
- There is no way for the compiler to verify that the macro parameters are of compatible types.
- The macro is expanded without any special type checking.
- The i and j parameters are evaluated twice. For example, if either parameter has a postincremented variable, the increment is performed two times.
- Because macros are expanded by the preprocessor, compiler error messages will refer to the expanded macro, rather than the macro definition itself. Also, the macro will show up in expanded form during debugging.
号
如果这还不够,我不知道是什么。
答案太长了,我不能总结一切,除了:
- 例如,宏不能保证类型安全,而函数模板不能保证类型安全:编译器无法验证宏参数的类型是否兼容——同样,在函数模板实例化时,编译器知道
int 或float 是否定义operator + 。 - 模板为元编程打开了大门(简而言之,在编译时评估事物并做出决定):在编译时,可以知道类型是整型还是浮点型;它是指针还是常量限定型等等。在即将到来的C++ 0x中看到"类型特征"
- 类模板具有部分专用化
- 函数模板有显式的完全专用化,在您的示例中,
add 的实现方式可能与(5, 3); add 不同,后者在宏中不可能实现。(5, 3); - 宏没有任何作用域
#define min(i, j) (((i) < (j)) ? (i) : (j)) —对i 和j 参数进行两次评估。例如,如果其中一个参数有一个后递增变量,则执行两次递增。- 由于宏是由预处理器展开的,因此编译器错误消息将引用展开的宏,而不是宏定义本身。此外,在调试期间宏将以展开形式显示。
- 等。。。
注意:在一些罕见的情况下,我更喜欢依赖于变量宏,因为在C++ +0x成为主流之前,没有任何可变的模板。
参考文献:
- C++ FAQ Lite:(35)模板
- 模板的优点
- 模板与宏(C++)
在非常基本的层面上,模板只是宏替换。但是你这样想,却忽略了很多事情。
考虑模板专门化,据我所知,您不能用宏来模拟。这不仅允许某些类型的特殊实现,而且是模板元编程中的关键部分之一:
1 2 3 4 5 6 7 8 9 10 11 | template <typename T> struct is_void { static const bool value = false; } template <> struct is_void<void> { static const bool value = true; } |
它本身就是你能做的很多事情中的一个例子。模板本身是图灵完备的。
这忽略了非常基本的东西,比如范围、类型安全性,而且宏的更混乱。
不。一个简单的反例:模板遵守名称空间,宏忽略名称空间(因为它们是预处理器语句)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | namespace foo { template <class NumberType> NumberType add(NumberType a, NumberType b) { return a+b; } #define ADD(x, y) ((x)+(y)) } // namespace foo namespace logspace { // no problemo template <class NumberType> NumberType add(NumberType a, NumberType b) { return log(a)+log(b); } // redefintion: warning/error/bugs! #define ADD(x, y) (log(x)+log(y)) } // namespace logspace |
型
C++模板有点像Lisp宏(而不是C宏),因为它们对已解析的代码版本进行操作,并允许您在编译时生成任意代码。不幸的是,您正在编程类似于原始lambda微积分的东西,所以像循环这样的高级技术有点麻烦。有关所有血腥的细节,请参阅krysztof czarnecki和ulrich eisenecker编写的Generative编程。
型
如果你正在寻找一个更深入的处理这个问题,我可以把你变成每个人最喜欢的C++仇恨者。这个人知道和讨厌更多的C++,而不是我梦想的那样。同时,这使得FQA具有令人难以置信的煽动性和极好的资源。
- 模板是类型安全的。
- 模板化的对象/类型可以被命名,成为类的私有成员等。
- 模板化函数的参数不会在整个函数体中复制。
这真的是一件大事,可以防止大量的错误。
不,不可能。预处理器(勉强)足以处理一些诸如t容器之类的事情,但是对于模板可以做的其他一些事情来说,它仅仅是不够的。
对于一些真实的例子,通过Andre Alexandrescu,或Dave Abrahams和Aleksey Gurtovoy的C++元编程阅读现代C++编程。几乎任何一本书中的任何一件事都不能用预处理器以极小的程度进行模拟。
编辑:对于
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | struct X { int x; }; struct Y { typedef long x; }; template <class T> class Z { T::x; }; Z<X>; // T::x == the int variable named x Z<Y>; // T::x == a typedef for the type 'long' |
这个答案旨在阐明C预处理器以及它如何用于一般编程。
在某些方面,它们支持一些类似的语义。C预处理器已用于启用通用数据结构和算法(请参见令牌Concatition)。然而,在不考虑C++模板的任何其他特性的情况下,它使得整个泛型编程游戏更加清晰地阅读和实现。
如果有人想看到硬核C通用编程的实际应用,请阅读libevent源代码——这里也提到了这一点。实现了大量的容器/算法集合,并在单个头文件中完成(非常可读)。我非常钦佩这一点,C++模板代码(我更喜欢它的其他属性)是非常冗长的。
还没有提到的是模板函数可以推断参数类型。
1 2 3 4 | template <typename T> void func(T t) { T make_another = t; |
有人可能会说,即将到来的"typeof"操作符可以解决这个问题,但即使它也不能分解其他模板:
1 2 | template <typename T> void func(container<T> c) |
甚至:
1 2 | template <tempate <typename> class Container, typename T> void func(Container<T> ct) |
我也觉得专业化的主题还不够全面。下面是宏不能做的简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | template <typename T> T min(T a, T B) { return a < b ? a : b; } template <> char* min(char* a, char* b) { if (strcmp(a, b) < 0) return a; else return b; } |
空间太小,无法进行类型专门化,但就我而言,您可以用它做什么,这让人激动不已。
型
模板是类型安全的。使用defines,可以编译代码,但仍然不能正常工作。
宏在编译器到达代码之前展开。这意味着您将收到扩展代码的错误消息,而调试器只看到扩展版本。
对于宏,总是有可能对某些表达式进行两次计算。想象一下将类似于+X的东西作为参数传递。
型
模板可以放在命名空间中,也可以是类的成员。宏只是一个预处理步骤。基本上,模板是一个一流的语言成员,可以很好地使用(更好?)其他的一切。
型
模板所能做的比宏预处理器所能做的要多得多。
例如,有模板专门化:如果使用此类型或常量声明此模板,则不要使用默认实现,但此处使用此实现…
……模板可以强制某些参数是同一类型等。
以下是您可能希望查看的一些来源:
- 百万千克1VordelVoorde和JoSutTIS的C++模板。这是我所知道的关于模板的最好和最完整的书。百万千克1百万千克1Boost库几乎完全由模板定义组成。百万千克1
型
在我看来,宏是C的一个坏习惯。尽管它们对某些人有用,但当存在typedef和模板时,我看不到它们的真正需求。模板是面向对象编程的自然延续。你可以用模板做更多的事情…
考虑一下……
1 2 3 4 5 6 | int main() { SimpleList<short> lstA; //... SimpleList<int> lstB = lstA; //would normally give an error after trying to compile } |
为了进行转换,您可以使用一个称为转换构造函数和序列构造函数(请看末尾)的东西,以及一个相当完整的列表示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | #include template<class T> class SimpleList { public: typedef T value_type; typedef std::size_t size_type; private: struct Knot { value_type val_; Knot * next_; Knot(const value_type &val) :val_(val), next_(0) {} }; Knot * head_; size_type nelems_; public: //Default constructor SimpleList() throw() :head_(0), nelems_(0) {} bool empty() const throw() { return size() == 0; } size_type size() const throw() { return nelems_; } private: Knot * last() throw() //could be done better { if(empty()) return 0; Knot *p = head_; while (p->next_) p = p->next_; return p; } public: void push_back(const value_type & val) { Knot *p = last(); if(!p) head_ = new Knot(val); else p->next_ = new Knot(val); ++nelems_; } void clear() throw() { while(head_) { Knot *p = head_->next_; delete head_; head_ = p; } nelems_ = 0; } //Destructor: ~SimpleList() throw() { clear(); } //Iterators: class iterator { Knot * cur_; public: iterator(Knot *p) throw() :cur_(p) {} bool operator==(const iterator & iter)const throw() { return cur_ == iter.cur_; } bool operator!=(const iterator & iter)const throw() { return !(*this == iter); } iterator & operator++() { cur_ = cur_->next_; return *this; } iterator operator++(int) { iterator temp(*this); operator++(); return temp; } value_type & operator*()throw() { return cur_->val_; } value_type operator*() const { return cur_->val_; } value_type operator->() { return cur_->val_; } const value_type operator->() const { return cur_->val_; } }; iterator begin() throw() { return iterator(head_); } iterator begin() const throw() { return iterator(head_); } iterator end() throw() { return iterator(0); } iterator end() const throw() { return iterator(0); } //Copy constructor: SimpleList(const SimpleList & lst) :head_(0), nelems_(0) { for(iterator i = lst.begin(); i != lst.end(); ++i) push_back(*i); } void swap(SimpleList & lst) throw() { std::swap(head_, lst.head_); std::swap(nelems_, lst.nelems_); } SimpleList & operator=(const SimpleList & lst) { SimpleList(lst).swap(*this); return *this; } //Conversion constructor template<class U> SimpleList(const SimpleList<U> &lst) :head_(0), nelems_(0) { for(typename SimpleList<U>::iterator iter = lst.begin(); iter != lst.end(); ++iter) push_back(*iter); } template<class U> SimpleList & operator=(const SimpleList<U> &lst) { SimpleList(lst).swap(*this); return *this; } //Sequence constructor: template<class Iter> SimpleList(Iter first, Iter last) :head_(0), nelems_(0) { for(;first!=last; ++first) push_back(*first); } }; |
看看cplusplus.com上关于模板的信息!您可以使用模板来执行所谓的特征,使用的特征有一种类型等文档。你可以用模板做更多的事情,然后用宏做更多的事情!
typename关键字用于启用上下文无关的嵌套typdef。这些是允许将元数据添加到类型(尤其是内置类型,如指针)的特征技术所必需的,这是编写STL所必需的。否则,typename关键字与class关键字相同。
让我们试试原始的例子。考虑
1 | #define min(a,b) ((a)<(b))?(a):(b) |
调用为
1 | c = min(a++,++b); |
当然,真正的区别是更深层次的,但这应该足以消除宏的相似性。
编辑:不,不能用宏确保类型安全。对于定义小于比较(即
模板了解数据类型。宏不这样做。
这意味着你可以做如下的事情…
- 定义可以采用任何数据类型的操作(例如,一个用于包装数字),然后提供专门化,根据数据类型是整型还是浮点型来选择适当的算法。
- 在编译时确定数据类型的方面,允许数组大小的模板推断,微软用于C++的StrucPys及其ILK重载。
另外,因为模板是类型安全的,所以有许多模板编码技术可以用一些假设的高级预处理器来执行,但是最好是笨拙的和容易出错的(例如,模板模板参数、默认模板参数、在现代C++设计中讨论的策略模板)。
模板在其最基本的功能中仅与宏相似。毕竟,模板作为宏的"文明"替代品被引入到语言中。但即使涉及到最基本的功能,相似性也只是肤浅的。
然而,一旦我们了解到模板更高级的特性,比如专门化(部分或显式),任何与宏明显相似的地方都会完全消失。
尽管模板参数是类型检查的,而且模板比宏有许多优点,但是模板非常类似于宏,因为它们仍然基于文本替换。在给模板代码提供类型参数进行替换之前,编译器不会验证模板代码是否有任何意义。例如,Visual C++不抱怨这个函数,只要你不调用它:
1 2 3 4 5 | template<class T> void Garbage(int a, int b) { fdsa uiofew & (a9 s) fdsahj += *! wtf; } |
因此,一般来说,不可能知道模板代码对于模板要接受的类型参数的给定类别是否能够正确工作或编译成功。
这并不是一个答案,而是已经给出答案的结果。
与科学家、外科医生、图形艺术家和其他需要编程的人一起工作——但他们不是,也永远不会是专业的全职软件开发人员——我发现偶尔的程序员很容易理解宏,而模板似乎需要更高层次的抽象思维,只有具备更深入和持续的经验。在C++中进行编程。使用模板是有用概念的代码需要很多实例,这样概念就足够有意义了。尽管这可以说是任何语言特性,但是模板的经验量比专业的临时程序员从日常工作中可能获得的经验量要大得多。
一般的天文学家或电子工程师可能会对宏进行简单的修改,甚至可能理解为什么应该避免使用宏,但对模板的修改并不能满足日常使用的需要。在这种情况下,宏实际上更好。当然,也有很多例外;一些物理学家围绕专业软件工程师转来转去,但这并不典型。
宏有一些基本问题。
首先,他们不尊重范围或类型。如果我有
第二,宏不能改变。模板
第三,宏不能进行任何形式的类型推断。不能首先编写通用交换宏,因为它必须声明一个类型的变量,而且它不知道该类型可以是什么。模板可识别类型。
模板的强大功能的一个很好的例子是最初被称为标准模板库的东西,在标准中作为容器、算法和迭代器。看看它们是如何工作的,试着想想如何用宏替换它。Alexander Stepanov查阅了大量的语言来实现他的STL思想,并得出结论,C++是唯一的模板。
模板集成在语言中,并且是类型安全的。
告诉我如何处理宏。这是一个很重的模板元编程。
https://www.youtube.com/watch?V= 0A9Py8WeVK
我认为宏afaik不能像模板部分专用化那样计算类型。
型
模板提供一定程度的类型安全性。