Android C++系列:C++11函数特殊特性

317 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第28天,点击查看活动详情

1. 背景

在Python中函数有默认参数等,在C++11中我们发现C++也支持了默认参数;还有C++特有的内联函数、constexpr函数等知识都有不少细节,本文对这些知识做详细介绍。

2. 默认参数

我们在Java中重载函数时经常有这样的形式:

public CustomView(Context context) {
    super(context);
  }

  public CustomView(Context context,
      @Nullable AttributeSet attrs) {
    super(context, attrs);
    init(context, attrs);
  }

  public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context, attrs);
  }

同一个函数因为参数个数不同最后定义了好几种形式,有些场景我们可以这样理解,参数较少的函数只是参数较多的函数的某个参数是个特定值,比如上面第二个构造函数可以理解成第一个构造函数的defStyleAttr为0,我们不去使用这个参数。这样就面临一个问题,明明一个函数可以解决的事情,最后搞出好几个函数来,有没有一种简便方法呢?在Python中我们用了默认参数实现,C++11也提供了这种特性。

某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参(default argument)。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

实际的代码工程中经常看到这种(随便从工程代码截取一段):

std::future<bool> notifyOfWakeWord(
        capabilityAgents::aip::AudioProvider wakeWordAudioProvider,
        avsCommon::avs::AudioInputStream::Index beginIndex,
        avsCommon::avs::AudioInputStream::Index endIndex,
        std::string keyword,
        std::chrono::steady_clock::time_point startOfSpeechTimestamp,
        std::shared_ptr<const std::vector<char>> KWDMetadata = nullptr) override;

    std::future<bool> notifyOfTapToTalk(
        capabilityAgents::aip::AudioProvider tapToTalkAudioProvider,
        avsCommon::avs::AudioInputStream::Index beginIndex = capabilityAgents::aip::AudioInputProcessor::INVALID_INDEX,
        std::chrono::steady_clock::time_point startOfSpeechTimestamp = std::chrono::steady_clock::now(),
        capabilityAgents::aip::Initiator initiator = capabilityAgents::aip::Initiator::TAP,
        std::string keyword = "",
        float wakeUpConfidence = 0.f) override;

再举个例子说明:

void setPosition(int x, int y, int z = 0);

我们设置坐标值,如果是二维空间z坐标都为0,这时候我们只需要这样调用:

setPosition(1,1);

如果是三维需要设置z坐标:

setPosition(1,1,1);

我们可以定义一个或者多个形参的默认值,但是要注意:

  • 一旦一个形参被赋予了默认值,它后面的所有形参都必须有默认值,换句话说就是默认参数必须放在参数列表的最后面。
  • 调用函数时,其中一个参数使用了默认值,这个参数对应形参里面后面的所有参数都必须使用默认值。

**最佳实现:**设计含有默认实参的函数时,尽量让不怎么用到默认值的形参出现在前面,让经常使用默认值的形参出现在后面。

对于函数的声明来说,我们的习惯是将其放在头文件中,并且一个函数只声明一次,但是C++中多次声明同一个函数也是合法的。我们在给定的作用域中一个形参只能被赋予一次默认实参。意味着,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值

通常我们使用的默认实参都是字面值,其实只要表达式的类型能转换成形参所需的类型,这个表达式就能作为默认实参。注意:局部变量不能作为默认实参。

int dz= 100;
void setPosition(int x, int y, int z = dz);

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

int dz = 100;
void setPosition(int x,int y int z = dz);
void fun(){
	dz = 200;
	setPosition(100,100);
}

我们在fun内部改变了dz值,所以调用setPosition时传递的是更新过的值。

3. 内联函数

我们一般的函数调用会有以下动作:

  1. 调用前先保存寄存器;
  2. 在返回时恢复;
  3. 如果需要会拷贝实参
  4. 程序转向一个新的位置继续执行。

总结下来就是函数调用是有不小开销的。有没有办法优化这块呢?答案是内联函数

内联函数实际就是在每个调用点上”内联的“展开。

比如我们定义了一个内联函数:

inline int min(int x , int y){
	return x > y ? y : x;
}

调用函数:

std::cout << min(1,2) << std::end;

再编译过程会展开成下面形式:

std::cout << (x > y ? y :x) << std::endl;

上面也看到了,内联函数的定义是在函数返回类型前面加上关键字inline

内联关键字是想编译器发出请求,编译器具体实现时也可以忽略这个请求。

内联机制用于优化规模较小,流程直接,频繁调用的函数。对于大部分编译器,不支持内联递归函数,对于75行的函数也大概不允许在调用节点内联展开。

4. constexpr函数

constexpr函数是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循以下几项规定:

  1. 函数的返回类型及所有形参的类型都得是字面值类型
  2. 函数体中必须有且只有一条return语句。
constexpr int getX(){
	return 20;
}
constexpr int x = getX();

执行初始化任务时,编译器把对constexpr函数的调用替换成其结果值。和inline一样都是在编译时,所以为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。

constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,constexpr函数中可以有空语句、类型别名以及using声明。

最佳实践:

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

5. 总结

本文介绍了C++函数的特殊语法:默认参数、内联函数、constexpr函数。其中默认参数是C++11新特性,constexpr默认也是内联的,内联是在调用节点展开,有点类似宏展开。