默认实参
我在这里穿插一个额外的知识点:默认实参。
函数默认实参
假设我现在有一个函数
我们可以给它的参数提供一个默认值:
这时,我在调用函数时可以略去带有默认值的参数:
int main() {
greeting(); // 使用默认实参,相当于 greeting("world");
greeting("Alice"); // 不使用默认实参
}
默认实参就是允许在函数调用表达式中省去末尾若干个实参的语法。它的语法形式是在函数形参后接 = 默认实参值
。当调用表达式略去这个实参时,相当于将 默认实参值
当做实参传入了函数。
换句话说,你可以将默认实参理解成提供了若干个新的重载:
void greeting() {
const char* who = "world";
std::cout << "Hello, " << who << std::endl;
}
void greeting(const char* who = "world") {
std::cout << "Hello, " << who << std::endl;
}
函数的默认实参只能在末尾的几个参数提供。如:
void f(int a, int b = 42) { } // OK
void f(int a = 42, int b) { } // 编译错误,默认实参不在结尾
void f(int a = 42, int b = 56) { } // OK,默认实参都在结尾
在调用带有默认实参的函数时,也只能省略末尾的若干个参数:
void f(int a, int b = 42, int c = 56) { }
int main() {
f(1); // 相当于 f(1, 42, 56); 省略了 b 和 c
f(1, 2); // 相当于 f(1, 2, 56); 省略了 c
// f(1, , 3); // 错误:没有这种语法
}
默认实参的值可以是表达式,但它有一些限制(如不能(在求值语境下)使用局部变量、this
等)。表达式的求值是在函数调用期间完成的,求得的值会用来初始化函数形参。
如果函数被多次声明,则默认实参只能出现在其中一次声明的位置上。同一形参位置多次出现默认实参是编译错误:
void f(int a, int b = 42);
// void f(int a, int b = 42); // 错误:不能多次提供 b 的默认实参
void f(int a, int b); // OK
提供默认实参的形参可以省略名字。尽管这看上去没有什么用。
成员函数也可以使用默认实参,但非函数调用的运算符重载不能。
默认模板实参
类似地,模板参数也可以提供默认实参。
非类型模板默认实参的写法和函数默认实参非常像:
非类型默认模板实参同样也包括如下语法限制:
- 默认模板实参只能在模板形参列表结尾提供;
- 模板实参列表中也只能省略结尾的若干个模板实参;
- 如果函数模板多次被声明,则同一位置的模板默认实参只能出现在其中一个声明中。
但默认模板实参由于其编译期实例化的特点,要求非类型默认模板实参值必须是常量。这是它与函数默认实参最显著的不同点。
类型模板实参拥有类似的语法:
类型模板默认实参可以使用一个固定的类型,或者任意之前已经被声明的类型(包括前面的模板形参)组成的“表达式”。这个“表达式”并不是真正的表达式,是指由一些类型通过模板、嵌套类或别名声明等一系列语法组成的新类型。比如:
template<typename T>
struct Helper {
T help() { /* [...] */ }
};
// 依赖于 T 的类型“表达式”作为 U 的默认模板实参
template<typename T, typename U = Helper<T>>
T f() {
U helper;
return helper.help();
}
// 例:f<int>(); 实例化 f<int, Helper<int>>
当可以进行模板实参推导时,若某个模板形参没有推导出其值但提供了默认模板实参,则推导成功并采用默认模板实参。这个过程对于类模板、函数模板、非类型模板参数或类型模板参数都适用。
提供默认模板实参的模板形参可以省略其名字。与函数默认实参不同,这种写法是有一定用处的,我将在比较靠后的篇幅提到。