C++模板别名的理解

简介: 故事背景: 最近在看邓俊辉老师的书《数据结构(C++语言版》。不得不说这本书写的太好了,强烈推荐大家看看。 我以前也学过C++,基础的语法还是知道的,也知道C++里模板的用法。所以我满以为凭这点底子看这本书的示例代码应该是没问题的。

故事背景:

最近在看邓俊辉老师的书《数据结构(C++语言版》。不得不说这本书写的太好了,强烈推荐大家看看。

我以前也学过C++,基础的语法还是知道的,也知道C++里模板的用法。所以我满以为凭这点底子看这本书的示例代码应该是没问题的。我还特地找了一个C++在线编译器wandbox。这个在线编译器支持多种版本的C++语法,还支持多文件。

在看到第三章列表节点模板类的示例代码时,我看不懂了。代码是这样的:

typedef int Rank; //秩
#define ListNodePosi(T) ListNode<T>* //列表节点位置

template <typename T> struct ListNode { //列表节点模板类(以双向链表形式实现)
// 成员
T data; ListNodePosi(T) pred; ListNodePosi(T) succ; //数值、前驱、后继
// 极造函数
ListNode() {} //针对header和trailer的构造
ListNode( T e, ListNodePosi(T) p = NULL, ListNodePosi(T) s = NULL)
 : data(e), pred(p), succ(s) {} //默认构造器
// 操作接口
ListNodePosi(T) insertAsPred(T const& e); //紧靠当前节点之前插入新节点
ListNodePosi(T) insertAsSucc(T const& e); //紧随当前节点之后插入新节点
};

看不懂的有两句(凭什么可以这样写?为什么我写不出来?):

#define ListNodePosi(T) ListNode<T>*

ListNodePosi(T) pred;

先来看看微软msdn对#define的语法解释:

#define Directive (C/C++)

#define identifier token-stringopt
#define identifier ( identifieropt,...,identifieropt)token-stringopt

The #define directive causes the compiler to substitute token-string for each occurrence of identifier in the source file. The identifier is replaced only when it forms a token. That is, identifier is not replaced if it appears in a comment, in a string, or as part of a longer identifier. For more information, see Tokens.

define指令使得编译器把源文件每个标识符都替换成token-string。只有当标识符构成一个token的时候才会被替换。那就是,标识符出现在注释,字符串,或者是更长标识符的一部分时不会被替换。

什么是token?(这个词很常见哟,尤其是涉及到分词的时候经常用到这个词)

A token is the smallest element of a C++ program that is meaningful to the compiler. The C++ parser recognizes these kinds of tokens: identifiers, keywords, literals, operators, punctuators, and other separators. A stream of these tokens makes up a translation unit.

一个token就是C++编程中对编译器有意义的最小元素。C++解析器会识别这些token:标识符,关键字,字面量,操作符,标点符号和其它分隔符。一连串这样的token构成了一个翻译单元。

比如定义一个变量, int a; 这就是一个token。因为它是有意义的,编译器能够编译它。
字面量就是“硬编码”。比如String str = "abcd"。这个"abcd"就是一个字面量。

现在明白了,define会把能构成token的identifier替换成token-stringopt。但不是简单粗暴的全部替换(比如注释里的就不会替换)

小结一下#define就两种用法:

  • #define 标识符 token字符串选项
  • #define 标识符 (标识符选项,...,标识符选项) token字符串选项

第二种用法省略号表示可以传入多个标识符选项。标识符就是变量的意思。

opt就是选项的缩写,表示这个token字符串是可选的(optional)。可选的意思就是说可以有,也可以没有。
identifer翻译过来是标识符的意思,变量名,函数名,类名等都是一个标识符。

啰嗦了这么久,再来看看#define ListNodePosi(T) ListNode<T>*这一句。这里identifier的地方有一个圆括号,显然是第二种用法。我们再来仔细看看第二种用法的语法:

The second syntax form defines a function-like macro with parameters. This form accepts an optional list of parameters that must appear in parentheses. After the macro is defined, each subsequent occurrence of identifier( identifieropt, ..., identifieropt ) is replaced with a version of the token-string argument that has actual arguments substituted for formal parameters.

第二种语法形式定义了一个类似函数的带参数的宏。这种形式接受一个可选的必须出现在圆括号里的参数列表。宏被定义之后,接下来出现的每个标识符(标识符选项,...标识符选项)都会被替换成拥有正式参数(实参)的token-string。 (言外之意如果opt-string里 有参数的话,参数会被替换成标识符里参数的值)

上面这句话看起来很容易,也很容易懂,但是我还是写不出这样的语句:

#define ListNodePosi(T) ListNode<T>*

ListNode<T>*:这的确是一个正确的token。这是一个指针类型,就像 int *一样。只不过这个类型带有泛型的参数。单独写我没意见。
ListNodePosi(T):一个标识符后面跟上参数,根据第二种写法,这样是合法的。

根据定义,凡是代码里出现ListNodePosi(T)的地方都要替换成ListNode<T>*。 并且会用标识符参数T替换 ListNode<T>里的参数T

我真正不理解的地方就在这里:ListNode<T>里的T参数吗?它跟标识符里参数T是同一个概念吗?能替换成功吗?

好吧,来看看C++模板的定义。

A template is a construct that generates an ordinary type or function at compile time based on arguments the user supplies for the template parameters.

模板是根据用户为模板参数提供的参数,在编译时生成普通类型或函数的句法结构。

这里明确说明,模板里的T是一个参数。跟普通函数里的参数是一样的。如果不一样文档里不会继续说成是parameter。不一样的话它一定会用另一个(新)词来代替。

所以真相大白了,模板里的参数是参数,跟标识符里的参数是一个概念,那么用ListNodePosi(T)里的T替换ListNode<T>*里的T没毛病,可以替换成功。


啰嗦了这么久,其实就是为了搞清楚一个问题。
宏定义里标识符的参数能替换右边token里模板的参数吗?

我的理解,文档里都把它们叫成parameter,所以是可以替换的。从编译结果来看也确实如此。而且这里的叫成parameter是非常准确的,parameter指的是形参。真正替换的时候其实是用标识符里的实参(argument)替换token里模板的实参(argument)。看上面define第二种语法的定义,人家说的就是argument

parameterargument的区别就是:前者多指形参,后者多指实参。

宏定义和模板里所谓的参数可以这么理解:

宏定义和模板里的参数T里理解成一个变量。编译器在编译的时候会给这个变量赋上具体的值。给模板的参数赋值,就实现了所谓的模板实例化。

#define ListNodePosi(T) ListNode<T>*这一句终于搞懂了,我可真能钻牛角尖。

那么ListNodePosi(T) pred;这一句呢。一般我们在使用(调用)宏的时候都要传入实参的对不对?这里为什么还可以继续写成形参T呢?

写个demo试一下:

#include <iostream>
#include <cstdlib>
#define sum(a, b) a+b
int abc(int a, int b){
    return sum(a, b);
}
int main()
{
    int c = abc(1, 2);
    std::cout<<c<<std::endl; // 输出3
}

额,我的理解是:

①目前只是在函数abc的定义里调用了宏,只是定义,程序并没有跑起来,所以可以继续使用形参。

②也可以理解为我们已经传入了“实参”,把函数abc的参数传给了宏。程序跑起来的时候在函数sum里,a和b已然是实际的值了。

好像第二种说法更有说服力一点。

总结:

以上是C++.0x标准(或者称C++11或者C++0B)出来以前的写法,有点难以理解,不过感觉很高大上啊。

从C++0x标准开始,模板别名(template alias)有了新的写法:

template <typename T> typedef ListNode<T>* ListNodePosi;

显然这种写法更简洁,更直观。因为ListNodePosi(T)感觉很别扭,总感觉是伪代码。

目录
相关文章
|
3月前
|
存储 算法 C++
C++ STL 初探:打开标准模板库的大门
C++ STL 初探:打开标准模板库的大门
126 10
|
5月前
|
编译器 C++
【C++】——初识模板
【C++】——初识模板
【C++】——初识模板
|
2月前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
64 4
|
6月前
|
程序员 C++
C++模板元编程入门
【7月更文挑战第9天】C++模板元编程是一项强大而复杂的技术,它允许程序员在编译时进行复杂的计算和操作,从而提高了程序的性能和灵活性。然而,模板元编程的复杂性和抽象性也使其难以掌握和应用。通过本文的介绍,希望能够帮助你初步了解C++模板元编程的基本概念和技术要点,为进一步深入学习和应用打下坚实的基础。在实际开发中,合理运用模板元编程技术,可以极大地提升程序的性能和可维护性。
|
2月前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
37 3
|
2月前
|
编译器 C++
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
34 0
|
3月前
|
编译器 程序员 C++
【C++打怪之路Lv7】-- 模板初阶
【C++打怪之路Lv7】-- 模板初阶
24 1
|
3月前
|
存储 编译器 C++
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
【C++篇】引领C++模板初体验:泛型编程的力量与妙用
55 9
|
3月前
|
编译器 C语言 C++
C++入门6——模板(泛型编程、函数模板、类模板)
C++入门6——模板(泛型编程、函数模板、类模板)
74 0
C++入门6——模板(泛型编程、函数模板、类模板)
|
3月前
|
算法 编译器 C++
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
【C++篇】领略模板编程的进阶之美:参数巧思与编译的智慧
99 2