”更高级的宏“模板到底是怎么实例化的?

Posted C+G

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了”更高级的宏“模板到底是怎么实例化的?相关的知识,希望对你有一定的参考价值。

文章目录

本文默认你已经拥有基本的gcc编译选项知识,如果没有,可以看看这篇文章 程序的编译过程gcc版

从预编译的角度对比宏定义和模板

来测测宏定义

大家都知道,宏定义仅仅只作用于文本的替换,在预编译的时候,把用到宏定义的部分替换为真正的文本而已,缺点就是不会做类型检查,只要语法能过编译就行。

我们定义一个宏来求两者之间的最大值,为了防止预编译的代码太过冗长不易看懂,没有用 include 去包含其他函数库的声明。

代码如下:

#define MAX_VALUE(_1,_2) ((_1>_2)?_1:_2)

int main()
    MAX_VALUE(1,2);

经过 gcc -E 命令后得到预处理后的代码如下:

# 0 ".\\\\test_template.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 ".\\\\test_template.cpp"
# 10 ".\\\\test_template.cpp"
int main()
    ((1>2)?1:2);


观察以上代码,我们发现,确实是直接的文本替换,前面的宏定义代码都不见了,只有,传入的替换文本了。

宏定义的害处

如果我们把下面这段代码传入到宏定义中:

#define MAX_VALUE(_1,_2) ((_1>_2)?_1:_2)


int main()
    MAX_VALUE("abdcdf","fdf");

很明显,这个代码是可以过编译的,但我们肯定不想直接的如下的方式进行字符串的比较,这样的比较毫无意义,只不过是比较的地址而已。

# 0 ".\\\\test_template.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 ".\\\\test_template.cpp"
# 10 ".\\\\test_template.cpp"
int main()
    (("abdcdf">"fdf")?"abdcdf":"fdf");


故宏定义的最大危害,就是没法对类型进行检查!这就导致无法灵活的进行优化和调整一些直接文本替换带来的副作用。

模板是否会进行预处理操作?

源代码:

template<typename T>
T MAX_VALUE(T _1, T _2)
    if(_1<_2)
        return _2;
    return _1;


int main()
    MAX_VALUE("abdcdf","fdf");

预处理后:

# 0 ".\\\\test_template.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 ".\\\\test_template.cpp"


template<typename T>
T MAX_VALUE(T _1, T _2)
    if(_1<_2)
        return _2;
    return _1;


int main()
    MAX_VALUE("abdcdf","fdf");


我们发现模板并不会在预处理时被展开,它甚至不会和预处理的调用部分形成任何联系!

看来模板的展开处理过程是发生在后续的编译过程中,由于本人不清楚如何反汇编和反编译过程,故没有亲身实践。

但从我的日常使用体验和大佬们总结的经验看来,模板也和宏定义的展开类似,就是简单的文本替换,但可以利用编译时期的语法检查和类型判断(type_traits技术),所以能够对不同情况的展开进行不同的处理。

比如上面的代码利用偏特化进行优化:

#include <iostream>
#include <cstring>
template<typename T>
int MAX_VALUE(T _1, T _2)
    if(_1<_2)
        return _2;
    return _1;


using type = const char *;
template<>
int MAX_VALUE<type>(type _1, type _2)
        std::cout << "调用偏特化";
    return strcmp(_1, _2);


int main()
    MAX_VALUE("abdcdf","fdf");

善用编译期的模板

为什么大多数模板库声明和定义都放在一起?

声明和定义分开处理的原因

我们清楚,声明和定义中,声明可以有无数份,但定义在一个项目里只能存在一份!所以我们在多文件项目的过程中,需要 include<xxx.h> 得到对应的声明,最后再把对应的实现 xxx.c 编译完后,再通过链接过程让声明能够连接到对应的定义来进行使用。所以在通常情况下我们都是在 .h 文件里面进行声明,再写一个 .c 文件实现定义,把声明和定义进行分开,这样无论你在其他的 .c 文件里使用多少个 include 操作,整个项目都只存在一份定义而已。故这种情况下,用 include 把多份相同的声明放在一个main函数里运行也是没问题的。

话说回来,那为什么一般不会去直接把定义和声明写在一起呢?

很明显,定义和声明分开,无论我们怎么 include 都不会因为重复定义而产生错误,而如果我们写在一个被 include 的文件里面,则整个项目中,每次 include 都会多产生一次定义!

为什么模板要把声明和定义写在一起?

那既然如此,为什么写模板的时候声明和定义写在一个文件里面呢???

我们前面已经清楚了,预编译命令 include 就仅仅把里面的代码拷贝到使用它的文件里去,如果我们把模板的定义和声明分开,画风大概会像下面这样:

//test.h

#ifndef TEST_TEMPLATE_TEST_H
#define TEST_TEMPLATE_TEST_H

template<typename T>
void print(T val);

#endif //TEST_TEMPLATE_TEST_H

//test.cpp

#include "test.h"
#include <iostream>
template<typename T>
void print(T val) 
    std::cout<<val;


不出我所料,很快就报错,内容如下:

undefined reference to `void print<int>(int)'

就是链接时找不到对应的函数定义,原因是模板只会在调用的时候实例化展开代码,如果 .h 只存在模板的声明,那么被include后调用,只会被实例化为以下内容:

void print([实例化的类型名] val);

因为对应的.cpp文件里面,不会有任何的变更,也就是不会有对应的定义产生。

所以想要把声明和定义分文件且实现模板的实例化展开,几乎是不可能的(我目前不清楚有哪种方式可以(似乎在末尾再include定义的文件就可以但。。和直接include没区别))

所以在写模板的时候,我们需要让使用到它的人(include调用者)把声明和定义一块给它,而防止找不到实例化展开的定义。

如何防止模板类的重复定义?

以上描述了,为什么模板声明和定义需要写在同一个文件里,但问题又出现了,如何预防一个项目中产生多个定义呢?

简单预防:

通过预处理的宏定义控制导入导出的代码,但这样只能够保证一个 .c文件 中不会出现两次 include 相同代码段。而无法保证其他 .c文件 include 后再次产生定义!

如下代码,我写了一个ttt.h文件,导出一个 print 函数的声明和定义(直接定义就包含声明)。

//ttt.h

#ifndef TEST_TEMPLATE_TTT_H
#define TEST_TEMPLATE_TTT_H
#include <iostream>

void print()
    std::cout<<"hhhh";



#endif //TEST_TEMPLATE_TTT_H

//test.cpp

#include "ttt.h"
#include <iostream>
//main.cpp

#include "ttt.h"
#include <iostream>

int main() 
    print();
    return 0;

运行main函数时,很快就发生了重复定义的报错!

因为我们在test.cpp文件里面也 include“ttt.h" 这将导致重复定义跨文件的重复定义是简单的宏定义没法避免的!

但我们又发现我们导入多次 <iostream> 标准库却不会出现这个问题,这是为什么呢?

我试着点进 iostream 里面观察观察,发现写的全是声明。

再进入到更深层的include瞧一瞧看看:

好家伙,<bits/c++config.h> 是两千多行的宏定义!

<ostream><istream> 也都只是大量的类型定义和各种类的声明而已,还有我看不懂的各种模板语法的运用,追溯到 <ios_base.h> 才终于发现各种类和方法的实现,但看起来都很短,还有各种模板技术的运用,感觉还有很多实现没有放在这个库里面。最后总之就是虽然标准库用的模板,但它用各种技术避免了声明和定义放在一个文件里面。。。或者说让文件导入的时候不会重复的去导入了。

以我目前的水平,看标准库就等于是看天书。。。

大概这就是C++劝退的原因之一吧,模板库为了防止重复定义,使得可读性变得极差,反正我是完全看不懂。。。

相对而言,Java的源代码直接就能简单上手看懂,而且比读文档还清晰,只能说C++的痛。。。

一些简单且常用的模板技术

template + 函数声明将模板提前实例化一份

由于我们template定义后,只有调用它的时候,才会实例化展开一份代码,但我们也能让编译器先为我们实例化一份代码,这个我也是最近才清楚有这个用法,以前从碰到过。

template<typename T>
int MAX_VALUE(T _1, T _2)
    if(_1<_2)
        return _2;
    return _1;


using type = const char *;
template int MAX_VALUE(type _1, type _2);

type_traits(类型萃取技术)

type_traits是编译期就去确定具体的类型,而如何确定的呢,这个只需要用到模板的特化展开进行特定的标记即可。

比如写一个无符号整型的编译期类型判断工具可以像下面这样写:

原理就是利用的模板的实例化展开,再加上特定的偏特化,来对类内变量的值进行一个区分,便可实现类型的判断了。

type_traits + static_assert()的运用

我们都知道C语言有个assert()宏,用于DEBUG模式下的断言(不清楚的可以看看我的 assert源码解析),而C++也有static_assert()进行断言,这个断言比assert宏要强大很多。

assert和static_assert的对比

共同点:

都是用于断言,满足条件则正常运行,否则抛出错误信息。

不同点:

assert是在运行时且为DEBUG模式,出错后调用函数来进行报错退出处理。

static_assert()则是在程序编译时,在编译时进行判断,再抛出相应信息。

type_tarits的运用

如下图:

通过type_trait可以实现只接受float类型和int类型。

以上是关于”更高级的宏“模板到底是怎么实例化的?的主要内容,如果未能解决你的问题,请参考以下文章

模板是如何实例化的?

带有 lambda 作为每个实例化的唯一默认参数的模板

将 pimpl 与 Templated Class 和显式实例化的模板一起使用

函数申明对函数模板实例化的屏蔽

ueditor无法滚动到底部工具栏浮动bug怎么修复

带有 Clang 10 显式模板实例化的 ~queue 的未定义引用