第2章 函数参数的占位符类型
# 第2章 函数参数的占位符类型
泛型编程是C++的一个关键范式。因此,C++20也为泛型编程提供了一些新特性。然而,有一个基本的扩展在本书中或多或少会经常用到:现在你可以使用auto
和其他占位符类型来声明普通函数的参数。
后面的章节将介绍更多泛型扩展:
- 非类型模板参数的扩展
- Lambda模板
# 2.1 普通函数参数的auto
自C++14起,Lambda表达式可以使用诸如auto
这样的占位符来声明/定义其参数:
auto printColl = [] (const auto& coll) { // 泛型Lambda表达式
for (const auto& elem : coll) {
std::cout << elem << "\n";
}
};
2
3
4
5
这些占位符允许我们传递任何类型的参数,前提是Lambda表达式内部的操作支持该类型:
std::vector coll{1, 2, 4, 5};
...
printColl(coll); // 为vector<int>编译Lambda表达式
printColl(std::string{ "hello "}); // 为std::string编译Lambda表达式
2
3
4
自C++20起,你可以在所有函数(包括成员函数和运算符)中使用诸如auto
这样的占位符:
void printColl(const auto& coll) // 泛型函数
{
for (const auto& elem : coll) {
std::cout << elem << "\n";
}
}
2
3
4
5
6
这样的声明只是声明如下模板的一种快捷方式:
template<typename T>
void printColl(const T& coll) // 等效的泛型函数
{
for (const auto& elem : coll) {
std::cout << elem << "\n";
}
}
2
3
4
5
6
7
唯一的区别在于,使用auto
时,你不再有模板参数T
的名称。因此,这个特性也被称为缩写函数模板语法(abbreviated function template syntax)。
因为使用auto
的函数是函数模板,所以使用函数模板的所有规则都适用。这尤其意味着,你不能在一个翻译单元(CPP文件)中实现带有auto
参数的函数,而在另一个翻译单元中调用它。对于带有auto
参数的函数,整个实现都应该放在头文件中,以便在多个CPP文件中使用(否则,你必须在一个翻译单元中显式实例化该函数)。另一方面,它们不需要声明为inline
,因为函数模板总是内联的。
此外,你可以显式指定模板参数:
void print(auto val) {
std::cout << val << "\n";
}
print(64); // val的类型为int
print<char>(64); // val的类型为char
2
3
4
5
6
# 2.1.1 成员函数参数的auto
你也可以使用这个特性来定义成员函数:
class MyType {
...
void assign(const auto& newVal);
};
2
3
4
这个声明等同于(区别在于没有定义类型T
):
class MyType {
...
template<typename T>
void assign(const T& newVal);
};
2
3
4
5
注意,模板不能在函数内部声明。使用带有auto
参数的成员函数时,你不能再在函数内部局部定义类或数据结构:
void foo() {
struct Data {
void mem(auto); // 错误:不能在函数内部声明模板
};
}
2
3
4
5
有关使用auto
的成员operator ==
的示例,请查看sentinel1.cpp
。
# 2.2 在实践中对参数使用auto
对参数使用auto
有一些好处,同时也会带来一些影响。
# 2.2.1 auto
实现的延迟类型检查
使用auto
参数能显著降低实现存在循环依赖代码的难度。
例如,假设有两个类,它们相互使用对方类的对象。要使用另一个类的对象,就需要知道其类型定义;仅仅进行前向声明是不够的(仅声明引用或指针的情况除外):
class C2; // 前向声明
class C1 {
public:
void foo(const C2& c2) const { // 正确
c2.print(); // 错误:C2是不完整类型
}
void print() const;
};
class C2 {
public:
void foo(const C1& c1) const {
c1.print(); // 正确
}
void print() const;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
虽然可以在类定义内部实现C2::foo()
,但却无法实现C1::foo()
,因为为了检查c2.print()
调用是否有效,编译器需要C2
类的定义。
因此,必须在声明完两个类的结构之后再实现C2::foo()
:
class C2;
class C1 {
public:
void foo(const C2& c2) const; // 前向声明
void print() const;
};
class C2 {
public:
void foo(const C1& c1) const {
c1.print(); // 正确
}
void print() const;
};
inline void C1::foo(const C2& c2) const { // 实现(如果在头文件中则为内联)
c2.print(); // 正确
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
由于泛型函数在调用时才会检查泛型参数的成员,因此通过使用auto
,可以这样实现:
class C1 {
public:
void foo(const auto& c2) const {
c2.print(); // 正确
}
void print() const;
};
class C2 {
public:
void foo(const auto& c1) const {
c1.print(); // 正确
}
void print() const;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这并非什么新鲜事。将C1::foo()
声明为成员函数模板也能达到相同效果。不过,使用auto
让这件事变得更简单。
需要注意的是,auto
允许调用者传递任何类型的参数,只要该类型提供print()
成员函数。如果你不希望这样,可以使用标准概念std::same_as
来限制该成员函数仅对C2
类型的参数有效:
#include <concepts>
class C2;
class C1 {
public:
void foo(const std::same_as<C2> auto& c2) const {
c2.print(); // 正确
}
void print() const;
};
...
2
3
4
5
6
7
8
9
10
11
对于概念来说,不完整类型也能正常使用。
# 2.2.2 auto
函数与lambda表达式的区别
带有auto
参数的函数与lambda表达式有所不同。例如,如果不指定泛型参数,仍然无法将带有auto
参数的函数作为参数传递:
bool lessByNameFunc(const auto& c1, const auto& c2) { // 排序准则
return c1.getName() < c2.getName(); // - 按名称比较
}
...
std::sort(persons.begin(), persons.end(),
lessByNameFunc); // 错误:无法推导排序准则中参数的类型
2
3
4
5
6
请记住,lessByName()
的声明等效于:
template<typename T1, typename T2>
bool lessByNameFunc(const T1& c1, const T2& c2) { // 排序准则
return c1.getName() < c2.getName(); // - 按名称比较
}
2
3
4
由于函数模板不是直接被调用,编译器无法推导模板参数来编译调用。因此,在将函数模板作为参数传递时,必须显式指定模板参数:
std::sort(persons.begin(), persons.end(),
lessByName<Customer, Customer>); // 正确
2
而使用lambda表达式时,可以直接传递:
auto lessByNameLambda = [] (const auto& c1, const auto& c2) { // 排序准则
return c1.getName() < c2.getName(); // 按名称比较
};
...
std::sort(persons.begin(), persons.end(), lessByNameLambda); // 正确
2
3
4
5
原因在于lambda表达式是一个对象,它本身没有泛型类型。只有将该对象作为函数使用时才具有泛型特性。
另一方面,显式指定(简化的)函数模板参数更为容易:
- 只需在函数名后传递指定的类型:
void printFunc(const auto& arg) {
...
}
printFunc<std::string>("hello "); // 调用为std::string编译的函数模板
2
3
4
- 对于泛型lambda表达式,则需要这样做:
auto printFunc = [] (const auto& arg) {
...
};
printFunc.operator()<std::string>("hello "); // 调用为std::string编译的lambda表达式
2
3
4
5
对于泛型lambda表达式,函数调用运算符operator()
是泛型的。因此,必须将所需类型作为参数传递给operator()
,以显式指定模板参数。
# 2.3 详细解析auto作为函数参数的情况
让我们详细探讨一下缩写函数模板中auto作为函数参数的一些特性。
# 2.3.1 auto参数的基本约束
使用auto声明函数参数与在lambda表达式中使用auto声明参数遵循相同规则:
- 对于每个用auto声明的参数,函数都有一个隐式模板参数。
- 参数可以是参数包:
void foo(auto... args);
。这等同于下面这种形式(不引入Types
):template<typename... Types> void foo(Types... args);
。 - 不允许使用
decltype(auto)
。
缩写函数模板仍可以(部分)显式指定模板参数进行调用。模板参数的顺序与调用参数的顺序一致。
例如:
void foo(auto x, auto y) {
// ...
}
foo("hello", 42); // x的类型是const char*,y的类型是int
foo<std::string>("hello", 42); // x的类型是std::string,y的类型是int
foo<std::string, long>("hello", 42); // x的类型是std::string,y的类型是long
2
3
4
5
6
7
# 2.3.2 结合模板参数和auto参数
缩写函数模板仍可以有显式指定的模板参数。占位符类型生成的模板参数会添加在指定参数之后:
template<typename T>
void foo(auto x, T y, auto z) {
// ...
}
foo("hello", 42, ?); // x的类型是const char*,T和y的类型是int,z的类型是char
foo<long>("hello", 42, ?); // x的类型是const char*,T和y的类型是long,z的类型是char
2
3
4
5
6
7
因此,以下声明是等效的(除了使用auto的地方没有类型名称):
template<typename T>
void foo(auto x, T y, auto z);
template<typename T, typename T2, typename T3> void foo(T2 x, T y, T3 z);
2
3
4
正如我们稍后会介绍的,通过使用概念(concepts)作为类型约束,你可以约束占位符参数以及模板参数。然后,模板参数可用于这样的限定。
例如,以下声明确保第二个参数y
具有整数类型,并且第三个参数z
的类型可以转换为y
的类型:
template<std::integral T>
void foo(auto x, T y, std::convertible_to<T> auto z) {
// ...
}
foo(64, 65, 'c'); // 正确,x是int,T和y是int,z是char
foo(64, 65, "c"); // 错误:"c" 无法转换为int类型(65的类型)
foo<long,char>(64, 65, 'c'); // 注意:x是char,T和y是long,z是char
2
3
4
5
6
7
8
注意,最后一条语句指定参数类型的顺序是错误的。
模板参数顺序不符合预期这一情况可能会导致一些难以察觉的错误。考虑以下示例:
// lang/tmplauto.cpp
#include <vector>
#include <ranges>
void addValInto(const auto& val, auto& coll) {
coll.insert(val);
}
template<typename Coll> // 注意:模板参数顺序不同,需要std::ranges::random_access_range<Coll>
void addValInto(const auto& val, Coll& coll) {
coll.push_back(val);
}
int main() {
std::vector<int> coll;
addValInto(42, coll); // 错误:存在歧义
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
由于在addValInto()
的第二个声明中仅对第一个参数使用了auto,模板参数的顺序有所不同。根据被C++20接受的 http://wg21.link/p2113r0 (opens new window),这意味着重载决议不会优先选择第二个声明,因此我们会得到一个歧义错误(目前并非所有编译器都能正确处理这种情况)。
由于这个原因,在混合使用模板参数和auto参数时要格外小心。理想情况下,应使声明保持一致。
# 2.4 后记
普通函数参数使用auto这一特性,最初是由Ville Voutilainen、Thomas Köppe、Andrew Sutton、Herb Sutter、Gabriel Dos Reis、Bjarne Stroustrup、Jason Merrill、Hubert Tong、Eric Niebler、Casey Carter、Tom Honermann和Erich Keane在 http://wg21.link/p1141r0 (opens new window) 中提出的,同时还包括对这些参数使用类型约束的选项。最终被接受的措辞由Ville Voutilainen、Thomas Köppe、Andrew Sutton、Herb Sutter、Gabriel Dos Reis、Bjarne Stroustrup、Jason Merrill、Hubert Tong、Eric Niebler、Casey Carter、Tom Honermann、Erich Keane、Walter E. Brown、Michael Spertus和Richard Smith在 http://wg21.link/p1141r2 (opens new window) 中确定。