constexpr函数------c++ primer
constexpr函数是指能用于常量表达式的函数。定义constexpr函数的方法有其他函数类似,不过要遵循几项约定:函数的返回值类型及所以形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句。为了能在编译过程中随时展开,constexpr函数被隐式指定地指定为内联函数。
constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr函数中可以有空语句、类型别名以及using声明。
允许constexpr函数的返回值并非一个常量:
constexpr int scale(int cnt){return 5*cnt;}//如果arg是常量表达式,则scale(arg)也是常量表达式
当scale的实参是常量表达式时,它的返回值也是常量表达式;反之则不然。如果我们用一个非常量表达式调用scale函数,比如int类型的对象i,则返回值是一个非常量表达式。当把scale函数用在需要常量表达式的上下文时,由编译器负责检查函数的结果是否符合要求。如果结果恰好不是常量表达式,编译器将发出错误信息。constexpr函数不一定返回常量表达式。
把内联函数和constexpr函数放在头文件内
和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致,基于这个原因,内联函数和constexpr函数通常定义在头文件中。
https://www.cufe-ifc.org/question/153643.html
当你下决心在.c文件中定义函数体的时候,自然不需要inline关键字了。而这时候也必须link相应的.o来确保找得到定义。
------------------------------ 话痨版 ------------------------------
首先来几个前置知识:
1) C和C++都有translation unit(或compilation unit)的概念:基本上编译器会一次编译一个文件,然后忘记一切,然后再编译下一个文件。哪怕你写gcc -c a.c b.c,其实和gcc -c a.c && gcc -c b.c大体上是没区别的。在最后,所有的.o文件都被linker汇总link成一个大的可执行文件。
2) static function。你可以把一个函数标记为static(也称为internal linkage),这样该函数的symbol就会被隐藏,从而该函数只存在在当前translation unit。换了下一个translation unit之后,该函数被忘得一干二净。linker也从来不知道这函数存在过。这时候你就算再定义一次上次translation已经定义过的static函数,linker也不会报redefinition错误。当然,这样代码在binary中就出现了多次。
3) 当然你也肯定知道C和C++的include的意思:在A中#include <B>就是把B的内容复制粘贴到A中#include对应的位置。
4) 编译器的内联优化就是看到你在Bar里调用Foo的时候,帮你复制一遍Foo的函数体,内嵌到Bar里去,同时消除栈操作的开销(因为代码已经被复制到“本地”了嘛,不需要跳来跳去了)。内联优化有个缺陷,就是在同一个translation unit里一定要看到函数体,所以光看到declaration是没用的。
现在考虑这么个问题:传统的在头文件中声明,在一个文件(.c)中实现函数体的方式有时执行太慢了。为什么慢呢,假设我这个函数就一行,但是函数调用的压栈传参数弹栈跳转等指令占了大部分开销,真是太不合算了。
这时候在传统C里面有两个解决方案:
1) “宏函数”。就是把本来藏在.c文件里的函数体放到一个宏里面去,当然宏也在头文件里。然后大家include头文件的时候就把宏也include走了,使用宏的时候就把这段代码一遍遍展开到所有使用的地方,消除了函数调用的开销。
2) 在编译器支持内联优化的情况下,在头文件里定义static function。任何别的.c文件,只要include了你的头文件,都对你的头文件做了一次复制粘贴,自动获得了该static function的函数体。所以在不同的translation unit里面,这些函数并不冲突,因为它们是static的。值得一提的是,这些函数体不一定一模一样。举例来说:
// a.h
#define FOO 3
static int Foo() { return FOO; }
// a.c
#include "a.h"
// b.c
#undef FOO
#define FOO 2
#include "a.h"
在不同的translation unit里面一个Foo返回3一个返回2。
1) 的坏处很明显,宏不能解决类型检查的问题,宏是dynamic scope(变量检查环境都是调用端而非定义端的)的,宏是textual substitution,搞不好有迷之编译不通过,宏很丑,定义不带语法高亮(雾),等等。
2) 看上去很好诶,写的是真正的函数,编译器还有能力内联。其缺陷是在编译器决定不内联的时候(通常这时候函数很大),每个translation unit中都定义了一个很大的函数,造成了不必要的binary size bloat。
这时候C++之父Bjarne Stroustrup站出来了,说我们在C++里搞个inline关键字吧!这个关键字不仅编译器认识,而且编译器在没有真正内联该函数时,会通过某种方式提示linker说这个函数被标记为“可重复定义”耶 - 根据我用gcc的实验,生成的是一个weak symbol。当linker看到一个weak symbol,会把函数名写在一个小本本上。在linker最后把所有文件link到一起的时候,它会把小本本扫一遍,对于同名函数只留一个,别的函数连带函数体统统删掉。这样就解决了binary size bloat的问题。当然这只是一种典型实现方式,并非一定如此。
另外,在编译器真正内联了该函数的时候,效果就和static一样了,这也是为什么你的代码里找不到定义 - 因为linker根本看不到static函数。话虽这么说,但是他们不管这个叫internal linkage(inline specifier),因为此时linkage是个implementation detail。语言只是强调:
The definition of an inline function must be present in the translation unit where it is called (not necessarily before the point of call)
在前面提到,用include static functions的方式中include进来的函数体可能不完全一样。inline此处也提到,你要是同名函数的函数体长得不一样,我才不告诉你我要留哪一份删哪几份呢。你要是敢这么做,我不保证我的输出有意义。这个在C++里叫做ODR violation (Definitions and ODR)。编译器一次只看一个translation unit,所以通常是没法检测ODR violation的(不排除LTO还是能查的),而linker也不查,我并不清楚为什么,大概是太昂贵吧。
另外,可以感受下当年inline关键字的marketing口号:
“An Inline Function is As Fast As a Macro”(Inline - Using the GNU Compiler Collection (GCC))
顺带建议一下刷ACM-ICPC的各种坑爹oj的同学,想要速度就用宏,因为oj可能不开内联优化。。
我来仔细的一步一步的分析这里面产生的缘由,我使用gcc作为演示,因为Linux有一系列的工具可以方便查看,但是这和Visual C++的原理是一致的。
回到你的例子,我们使用g++ -c http://main.cc -o main.o,然后使用nm main.o | c++filt 来查看生成的符号,如下图所示:
我们可以很清晰的看到A::max()的符号是U,即undefined,所以,它希望链接器去其它的地方找到函数的定义。若是一切正常的话,那么我们还有一个A.cpp,然后生成一个a.o,链接器去a.o的地方就可以找到这个函数定义了。
然而我们去编译其实现文件查看其符号表:
我们可以发现,是没有这个符号表的,因为如前文所述,inline函数并不会生成函数调用,会就在本源文件中展开,于是也就没有了函数的符号。
那么正是如此,后面链接器,把产生的.o合并到一起的时候,A::max()依然是未定义的,如下图所示:
那么,若是非内联的情况呢?
我们可以发现,a.o就会产生A::max()的函数符号,这样的话,若链接器去链接main.o a.o :
我们就会发现main.o 带U标记的undefined的A::max()被寻找到了,并在最后的.o中进行了填充。
那么,为什么inline的函数定义放在头文件就可以呢?因为我们使用的时候,会#include "a.h",那么这个时候编译器首先会预处理展开,这样也就包括了A::max()的定义,使用g++ -E http://a.cc > a.i; vim a.i 后如下图所示:
那么这个时候,http://main.cc在本源文件就可以找到A::max()的定义了,然后让我们看最后的符号表:
也的确如我们意想的不是U的undefined。
那么,这里多提一句的是,在现代的C++编译器中,我们几乎都做一个优化叫做内联函数优化,因为我们在优化的时候,若不是IPA这样的都是以一个函数来进行,那么我们内联函数优化后就会把相关的定义展开在一个函数中,这样就可以进行更好的优化。而这个内联函数的优化级别,在每一个编译器中是不同的,但是在我们实际C++编译器的实现中,其实已经完全不是很多教科书提到的几行了,我们往往是100行,乃至更多行的函数都会内联展开,就是为了更好的优化。而我们内联优化的话,其实也就是根据你的函数是多少行来进行决定的,于是在现代C++编译器中,如果你是为了inline优化而加上inline的话,我认为inline的意义在这里已经完全不需要了,现代的C++编译器优化已经非常强悍了。所以,若将来inline被C++废弃,也是完全可以理解的。
constexpr函数------c++ primer的更多相关文章
- 特殊用途语言特性(默认实参/内联函数/constexpr函数/assert预处理宏/NDEBUG预处理变量)
默认实参: 某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参.调用含有默认实参的函数时,可以包含该实参,也可以省略该实参. 需要特别 ...
- C++之内联函数与constexpr
inline 函数 规模小,流程直接且频繁调用 cout<<shortString(s1,s2)<<endl; = cout<<(s1.size()<s2.s ...
- C++ Primer 5th 第6章 函数
正如第一章所说:C++的函数是一个能够完成一个功能的模块或者说是一段命名了的代码块. 如下图所示,函数可以重载,是一段实现某些功能命名了的代码. 一个完整的函数的构成有四部分: 1.返回类型 2.函数 ...
- C++Primer学习——函数
编译器能以任意顺序对形参进行求值 函数的返回类型不能是数组类型和函数类型. 函数开始时为形参分配内存,一旦函数结束,形参也就被销毁了. 如果弄成静态局部变量,那么回到程序终止结束时才被销毁. void ...
- C++ Primer 笔记——函数
1.函数内的局部静态对象在程序的执行路径第一次经过对象定义语句的时候初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响. size_t get_count() { ; ...
- 【C++ Primer | 06】 函数
contexpr函数 const用于运行期常量,constexpr用于编译期常量 • [test1.cpp] #include <iostream> using namespace std ...
- [C++ Primer] 第6章: 函数
参数传递 const形参和实参: 顶层const作用于对象本身, 和其他初始化过程一样, 当用实参初始化形参时会忽略掉顶层const, 换句话说, 形参顶层const被忽略掉了, 当形参有顶层cons ...
- const限定符、constexpr和常量表达式------c++ primer
编译器将在编译过程中把用到const变量的地方都替换成对应的值,为了执行这种替换,编译器必须知道变量的初始值.如果程序包含多个文件,则那个用了const对象的文件都必须能访问到它的初始值才行.要做到这 ...
- <<C++ Primer>> 第 6 章 函数
术语表 第 6 章 函数 二义性调用(ambiguous call): 是一种编译时发生的错误,造成二义性调用的原因时在函数匹配时两个或多个函数提供的匹配一样好,编译器找不到唯一的最佳匹配. 实 ...
随机推荐
- Vue.js:条件与循环
ylbtech-Vue.js:条件与循环 1.返回顶部 1. Vue.js 条件与循环 条件判断 v-if 条件判断使用 v-if 指令: v-if 指令 在元素 和 template 中使用 v-i ...
- 查询mysql 哪些表正在被锁状态
查询mysql 哪些表正在被锁状态 show OPEN TABLES where In_use > 0; 参考链接:http://zhidao.baidu.com/link?url=tCQ70t ...
- 登陆验证系统实例-三种(cookie,session,auth)
登陆验证 因为http协议是无状态协议,但是我们有时候需要这个状态,这个状态就是标识 前端提交from表单,后端获取对应输入值,与数据库对比,由此对象设置一个标识,该对象 在别的视图的时候,有此标识, ...
- 【转】Context.getExternalFilesDir()和Context.getExternalCacheDir()方法
应用程序在运行的过程中如果需要向手机上保存数据,一般是把数据保存在SDcard中的.大部分应用是直接在SDCard的根目录下创建一个文件夹,然后把数据保存在该文件夹中.这样当该应用被卸载后,这些数据还 ...
- PHP函数(一)-变量
1.全局变量 <?php $a = 1; $b = 2; function test(){ echo $a + $b."<br>"; //运行结果为0 } tes ...
- web界面上的字体兼容方案
原贴地址:http://www.baidufe.com/item/60cd11d3bfdee5c51369.html 做前端的,对web界面基本都抠的很仔细,尤其精确到1px! 类似边距.宽度.高度等 ...
- 迷你MVVM框架 avalonjs 0.99发布
在本版本主要是性能优化,添加一些有用的功能(如回调什么的),离成品阶段不远了. 修正 updateViewModel bug 修正监控数组的set方法 bug 添加data-each-rendered ...
- ZooKeeper集群搭建过程
ZooKeeper集群搭建过程 提纲 1.ZooKeeper简介 2.ZooKeeper的下载和安装 3.部署3个节点的ZK伪分布式集群 3.1.解压ZooKeeper安装包 3.2.为每个节点建立d ...
- 安装express.js(NODEJS框架)
express.js是nodejs的一个MVC开发框架,并且支持jade等多种模板.下面简单来说说express的安装和app.js文件的配置,然后在今后的教程中一步一步使用express.js搭建个 ...
- LoadRunner Controller
1.Controller的引入 1)需要Controller的原因?需要多个用户来模拟并发的时候. 2)一种强大的.成熟的工具的体现. 2. Controller的启动方式 1)LoadRunner ...