87.特殊用途语言特性

发布时间 2023-05-08 20:58:33作者: CodeMagicianT

1.默认实参

  某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(default argument)调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
  例如,我们使用string对象表示窗口的内容。 一般情况下,我们希望该窗口的高、宽和背景字符都使用默认值。但是同时我们也应该允许用户为这几个参数自由指定与默认 值不同的数值。为了使得窗口函数既能接纳默认值,也能接受用户指定的值,我们把它定义成如下的形式:

typedef string::size_type sz;//关于typedef参见2.5.1节(第60页)
string screen(sz ht= 24, sz wid = 80, char backgrnd =''); 

  其中我们为每一个形参都提供了默认实参,默认实参作为形参的初始值出现在形参列表中。我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

1.1使用默认实参调用函数

  如果我们想使用默认实参,只要在调用函数的时候省略该实参就可以了。例如,screen函数为它的所有形参都提供了默认实参,所以我们可以使用0、1、2或3个实参调用该函数:

string window; 
window = screen();//等价于screen(24, 80,' ')
window = screen(66);//等价于screen(66,80,'') 
window = screen(66, 256);//screen(66,256,'') 
window = screen (66, 256,'#');//screen(66,256,'#') 

  函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧位 置)。例如,要想覆盖background的默认值,必须为ht和wid提供实参:

window = screen( , , '?');//错误:只能省略尾部的实参
window = screen('?');//调用screen('?', 80, '')

  需要注意,第二个调用传递一个字符值,是合法的调用。然而尽管如此,它的实际效果却与书写的意图不符。该调用之所以合法是因为'?'是个char,而函数最左侧形参的类型string: :size_type是一种无符号整数类型,所以char类型可以转换成(参见4.11节,第141页)函数最左侧形参的类型。当该调用发生时,char类型的实参隐式地转换 成string::size_type,然后作为height的值传递给函数。在我们的机器上,'?'对应的十六进制数是0x3F,也就是十进制数的63,所以该调用把值63传给了形参height。

  当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。

1.2默认实参声明

  对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。换句话说,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。假如给定

//表示高度和宽度的形参没有默认值
string screen(sz, sz, char = '');

我们不能修改一个已经存在的默认值:

string screen (sz, sz, char ='*');//错误:重复声明

但是可以按照如下形式添加默认实参:

string screen(sz = 24, sz = 80, char);//正确:添加默认实参

建议:

通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。

默认实参初始值
  局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:

//wd、def和ht的声明必须出现在函数之外
sz wd = 80; 
char def = ''; 
sz ht(); 
string screen(sz = ht(), sz = wd, char= def); 
string window = screen();//调用 screen(ht(), 80,' ') 

  用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时:

void f2()
{
    def = '*';//改变默认实参的值 
    sz wd = 100;//隐藏了外层定义的wd,但是没有改变默认值
    window = screen();//调用 screen(ht(), 80,'*') 
}

  我们在函数f2内部改变了def的值,所以对screen的调用将会传递这个更新过的值。另一方面,虽然我们的函数还声明了一个局部变屈用于隐藏外层的wd,但是该局部变品与传递给screen的默认实参没有任何关系。

2.内联函数和constexpr函数

  在6.3.2节(第201页)中我们编写了一个小函数,它的功能是比较两个string形参的长度并返回长度较小的string的引用。把这种规模较小的操作定义成函数有很多好处,主要包括:

●阅读和理解shorterS七ring函数的调用要比读懂等价的条件表达式容易得多。
●使用函数可以确保行为的统一 , 每次相关操作都能保证按照同样的方式进行。
●如果我们斋要修改计算过程,显然修改函数要比先找到等价表达式所有出现的地方 再逐一修改更容易。
●函数可以被其他应用觅复利用, 省去了程序员重新编写的代价 。

  然而,使用shorterString函数也存在一个潜在的缺点:调用函数一般比求等价表达式的值要慢一些。在大多数机器上,一次函数调用其实包含着一系列工作: 调用前要先保存寄存器,并在返回时恢复可能需要拷贝实参。程序转向一个新的位置继续执行。

2.1内联函数可避免函数调用的开销

  将函数指定为内联函数(inline),通常就是将它在每个调用点上 “ 内联地 ” 展开。 假设我们把shorterString函数定义成内联函数, 则如下调用

cout << shorterString(s1, s2) << endl; 

将在编译过程中展开成类似于下面的形式

cout << (s1.size() < s2.size() ? s1 : s2) << endl; 

从而消除了 shorterString函数的运行时开销。

在shorterString函数的返回类型前面加上关键字inline,这样就可以将它声明成内联函数了:

//内联版本:寻找两个string对象中较短的那个
inline const string &shorterString(const string &s1, const string &s2) 
{
    return s1.size() <= s2.size() ? s1 : s2; 
}

注意:

内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。

  一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且 个75行的函数也不大可能在调用点内联地展开。

2.2constexpr函数

  constexpr函数(constexpr function)是指能用于常量表达式(参见2.4.4节,第58页)的函数。 定义constexpr函数的方法与其他函数类似,不过要遵循几项约定:函数的返问类型及所有形参的 类型都得是字面值类型(参见2.4.4节,第59页),而且函数体中必须

constexpr int new_sz()(return 42;) 
constexpr int foo = new_sz();//正确:foo是一个常量表达式

  我们把new_sz 定义成无参数的constexpr函数。因为编译器能在程序编译时验证new_sz函数返回的是常量表达式,所以可以用new_sz函数初始化constexpr类型的变量foo。
  执行该初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr函数中可以有空语句、类型别名(参见2.5.1节,第60页)以及using声明。

  我们允许constexpr函数的返回值并非一个常量:

//如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt) { return new_sz() * cnt;}

当scale的实参是常量表达式时,它的返回值也是常被表达式;反之则不然:

int arr[scale(2)];//正确:scale(2)是常桽表达式 
int i = 2;//i不是常量表达式
int a2[scale(i)];//错误:scale(i)不是常量表达式 

如上例所示,当我们给scale函数传入一个形如字面值2的常量表达式时,它的返回类型也是常量表达式。此时,编译器用相应的结果值替换对scale函数的调用。

  如果我们用一个非常量表达式调用scale函数,比如int类型的对象i,则返回值是一个非常量表达式。当把scale函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求。如果结果恰好不是常量表达式,编译器将发出错误信息。

注意:

constexpr函数不一定返回常量表达式。

2.3把内联函数和constexpr函数放在头文件内

  和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr函数来说,它的多个定义必须完全一致。基于这个原因,内联函 数和constexpr函数通常定义在头文件中。

3.调试帮助

  C++程序员有时会用到一种类似于头文件保护(参见2.6.3节,第67页)的技术,以便有选择地执行调试代码。基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到两项预处理功能:assert和NDEBUG。

3.1assert预处理宏

  assert是一种预处理宏(preprocessor marco)。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:

assert(expr);

首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果 表达式为真(即非0),assert什么也不做。

  assert宏定义在cassert头文件中。如我们所知,预处理名字由预处理器而非编译器管理(参见2.3.2节,第49页),因此我们可以直接使用预处理名字而无须提供using 声明。也就是说,我们应该使用assert而不是std::assert,也不需要为assert提供using声明。
  和预处理变量一样,宏名字在程序内必须唯一。含有cassert头文件的程序不能再定义名为assert的变量、函数或者其他实体。在实际编程过程中,即使我们没有包含cassert头文件,也最好不要为了其他目的使用assert。很多头文件都包含了 cassert,这就意味着即使你没有直接包含cassert,它也很有可能通过其他途径包含在你的程序中。

  assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阙值。此时,程序可以包含条如下所示的语句:

assert(word.size() > threshold); 

3.2NDEBUG预处理变量

  assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。
  我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。同时,很多编译器都提供了一个命令行选项使我们可以定义预处理变篮;

$ CC -D NDEBUG main.C # use /D with the Microsoft compiler 

这条命令的作用等价于在main.c文件的一开始写#define NDEBUG。

  定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert应该仅用于验证那些确实不可能发生的事情。我们可以把assert 当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程 序本身应该包含的错误检查。
  除了用于assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef和#end辽之间的代码:如果定义了NDEBUG,这些代码将被忽略掉:

void print(const int ia[], size t size) 
{
    #ifndef NDEBUG 
    //_ _func_ _是编译器定义的一个局部静态变量,用于存放函数的名字
    cerr << _ _func_ _ << ": array size is "<< size << endl; 
    #endif
    //...
}

  在这段代码中,我们使用变量_ _func_ _输出当前调试的函数的名字。编译器为每个函数都定义了_ _func_ _,它是const char的一个静态数组,用于存放函数的名字。
  除了C++编译器定义的_ _func_ _之外,预处理器还定义了另外4个对于程序调试很有用的名字:

_ _FILE_ _   存放文件名的字符串字面值。
_ _LINE_ _   存放当前行号的整型字面值。
_ _TIME_ _  存放文件编译时间的字符串字面值。
_ _DATE_ _   存放文件编译日期的字符串字面值。

可以使用这些常量在错误消息中提供更多信息:

if(word.size() < threshold) 
    cerr << "Error: " << FILE 
         << " : in function " << _ _func_ _
         << " at line "<< _ _LINE_ _ << endl
         << "       Compiled on " << _ _DATE_ _
         << " at " << _ _TIME_ _ << endl
         << "        Word read was \"" << word 
         << "\": Length too short" << endl; 

如果我们给程序提供了一个长度小于threshold的string对象,将得到下面的错误消息:

Error:wdebug.cc : in function main at line27 
      compiled on Jul 11 2012 at 20:50:03 
      Word read was "foo": Length too short

4.函数匹配

 在大多数情况下,我们容易确定某次调用应该选用哪个重载函数。然而,当几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来时,这项工作就不那么 容易了。以下面这组函数及其调用为例:

void f(); 
void f(int); 
void f(int, int); 
void f(double, double = 3.14); 
f(5. 6);//调用void f(double, double) 

4.1确定候选函数和可行函数

  函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。在这个例子中,有4个名为f的候选函数。

  第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数(viable function)。可行函数也有两个特征:是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

  我们能根据实参的数量从候选函数中排除掉两个。不使用形参的函数和使用两个int形参的函数显然都不适合本次调用,这是因为我们的调用只提供了一个实参,而它们分别有0个和两个形参。

  使用一个int形参的函数和使用两个double形参的函数是可行的,它们都能用一个实参调用。其中最后那个函数本应该接受两个double值,但是因为它含有一个默认实参,所以只用个实参也能调用它。

注意:

如果函数含有默认实参(参见6.5.1节,第211页),则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。

  在使用实参数量初步判别了候选函数后,接下来考察实参的类型是否与形参匹配。和一般的函数调用类似,实参与形参匹配的含义可能是它们具有相同的类型,也可能是实参类型和形参类型满足转换规则。在上面的例子中,剩下的两个函数都是可行的:

●f(int)是可行的,因为实参类型double能转换成形参类型int。
●f(double, double)是可行的,因为它的第二个形参提供了默认值,而第一个形参的类型正好是double,与函数使用的实参类型完全一致。

注意:

如果没找到可行函数,编译器将报告无匹配函数的错误。

4.2寻找最佳匹配(如果有的话)

  函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。它的基本思想是,实参类型与形参类型越接近,它们匹配得越好。

  在我们的例子中,调用只提供了一个(显式的)实参,它的类型是double。如果调用f(int),实参将不得不从double转换成int。另一个可行函数f(double, double)则与实参精确匹配。精确匹配比需要类型转换的匹配更好,因此,编译器把f(5.6)解析成对含有两个double形参的函数的调用,并使用默认值填补我们未提供的第二个实参。

4.3含有多个形参的函数匹配

  当实参的数量有两个或更多时,函数匹配就比较复杂了。对千前面那些名为f的函数, 我们来分析如下的调用会发生什么情况:

(42, 2.56); 

  选择可行函数的方法和只有一个实参时样,编译器选择那些形参数量满足要求且实参类 型和形参类型能够匹配的函数。此例中,可行函数包括f(int, int)和f(double, double)。接下来,编译器依次检查每个实参以确定哪个函数是最佳匹配。如果有且只有一个函数满足下列条件。则匹配成功:

●该函数每个实参的匹配都不劣于其他可行函数需要的匹配。

●至少有一个实参的匹配优于其他可行函数提供的匹配。

如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。

  在上面的调用中,只考虑第一个实参时我们发现函数f(int, int)能精确匹配;要想匹配第二个函数,int类型的实参必须转换成double类型。显然需要内置类型转换的匹配劣于精确匹配,因此仅就第一个实参来说,f(int, int)比f(double, double)更好。

  接着考虑第二个实参2.56,此时f(double, double)是精确匹配;要想调用f(int, int)必须将2.56从double类型转换成int类型。因此仪就第二个实参来说,f(double, double)更好。

  编译器最终将因为这个调用具有二义性而拒绝其请求:因为每个可行函数各自在一个实参上实现了更好的匹配,从整体上无法判断孰优孰劣。看起来我们似乎可以通过强制类型转换(参见4.11.3节, 第144页)其中的一个实参来实现函数的匹配,但是在设计良好的系统中,不应该对实参进行强制类型转换。

建议:

调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。

5.实参类型转换

  为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:

1.精确匹配,包括以下情况:
●实参类型和形参类型相同。
●实参从数组类型或函数类型转换成对应的指针类型(参见6.7节,第221页,将介绍函数指针)。
●向实参添加顶层const或者从实参中删除顶层const。

2.通过const转换实现的匹配(参见4.11.2节,第143页)。

3.通过类型提升实现的匹配(参见4.11.1节,第142页)。

4.通过算术类型转换(参见4.11.1节,第142页)或指针转换(参见4.11.2节,第143 页)实现的匹配。

5.通过类类型转换实现的匹配(参见14.9节,第514页,将详细介绍这种转换)。

5.1需要类型提升和算术类型转换的匹配

警告:

内置类型的提升和转换可能在函数匹配时产生意想不到的结果,但幸运的是,在设计良好的系统中函数很少会含有与下面例子类似的形参。

  分析函数调用前,我们应该知道小整型一般都会提升到int类型或更大的整数类型。假设有两个函数,一个接受int、另一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数。有时候,即使实参是一个很小的整数值,也会直接将它提升成int类型;此时使用short版本反而会导致类型转换:

void ff(int); 
void ff(short); 
ff('a');//char提升成 int; 调用ff(int) 

  所有算术类型转换的级别都一样。例如,从int向unsigned int的转换并不比从int向double的转换级别高。举个具体点的例子,考虑

void manip(long);
void manip(float); 
manip(3.14);//错误:二义性调用

  字面值3.14的类型是double,它既能转换成long也能转换成float。因为存在两种可能的算数类型转换,所以该调用具有二义性。

5.2函数匹配和const实参

  如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时编译器通过实参参是否是常从来决定选择哪个函数:

Record lookup(Account&);//函数的参数是Account的引用
Record lookup(const Account&);//函数的参数是一个常量引用
const Account a; 
Account b;

lookup(a);//调用lookup(const Account&) 
lookup(b);//调用lookup(Account &) 

  在第一个调用中,我们传入的是const对象a。因为不能把普通引用绑定到const对象上,所以此例中唯一可行的函数是以常量引用作为形参的那个函数,并且调用该函数与实参a精确匹配。
  在第二个调用中,我们传入的是非常量对象b。对于这个调用来说,两个函数都是可行的,因为我们既可以使用b初始化常量引用也可以用它初始化非常量引用。然而,用非常量对象初始化常量引用需要类型转换,接受非常量形参的版本则与b精确匹配。因此,应该选用非常量版本的函数。
  指针类型的形参也类似。如果两个函数的唯一区别是它的指针形参指向常最或非常量,则编译器能通过实参是否是常见决定选用哪个函数:如果实参是指向常量的指针,调用形参是const*的函数:如果实参是指向非常量的指针,调用形参是普通指针的函数。

参考资料:

C++ Primer