CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++20 完全指南 说明
  • 第1章 比较和<=>运算符
  • 第2章 函数参数的占位符类型
    • 2.1 普通函数参数的auto
      • 2.1.1 成员函数参数的auto
    • 2.2 在实践中对参数使用auto
      • 2.2.1 auto实现的延迟类型检查
      • 2.2.2 auto函数与lambda表达式的区别
    • 2.3 详细解析auto作为函数参数的情况
      • 2.3.1 auto参数的基本约束
      • 2.3.2 结合模板参数和auto参数
    • 2.4 后记
  • 第3章 概念、要求和约束
  • 第4章 概念、需求和约束详解
  • 第5章 标准概念详解
  • 第6章 范围与视图
  • 第7章 范围和视图的实用工具
  • 第8章 视图类型详解
  • 第9章 跨度(Spans)
  • 第10章 格式化输出
  • 第11章 <chrono>中的日期和时区
  • 第12章 std::jthread和停止令牌
  • 第13章 并发特性
  • 第14章 协程
  • 第15章 协程详解
  • 第16章 模块
  • 第17章 Lambda扩展
  • 第18章 编译期计算
  • 第19章 非类型模板参数(NTTP)扩展
  • 第20章 新的类型特性
  • 第21章 核心语言的小改进
  • 第22章 泛型编程的小改进
  • 第23章 C++标准库的小改进
  • 第24章 已弃用和移除的特性
  • cpp20completeguides
zhangxf
2025-03-20
目录

第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";
    }
};
1
2
3
4
5

这些占位符允许我们传递任何类型的参数,前提是Lambda表达式内部的操作支持该类型:

std::vector coll{1, 2, 4, 5};
...
printColl(coll);                     // 为vector<int>编译Lambda表达式
printColl(std::string{ "hello "});   // 为std::string编译Lambda表达式
1
2
3
4

自C++20起,你可以在所有函数(包括成员函数和运算符)中使用诸如auto这样的占位符:

void printColl(const auto& coll)   // 泛型函数
{
    for (const auto& elem : coll) {
        std::cout << elem << "\n";
    }
}
1
2
3
4
5
6

这样的声明只是声明如下模板的一种快捷方式:

template<typename T>
void printColl(const T& coll)        // 等效的泛型函数
{
    for (const auto& elem : coll) {
        std::cout << elem << "\n";
    }
}
1
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
1
2
3
4
5
6

# 2.1.1 成员函数参数的auto

你也可以使用这个特性来定义成员函数:

class MyType {
   ...
    void assign(const auto& newVal);
};
1
2
3
4

这个声明等同于(区别在于没有定义类型T):

class MyType {
   ...
    template<typename T>
    void assign(const T& newVal);
};
1
2
3
4
5

注意,模板不能在函数内部声明。使用带有auto参数的成员函数时,你不能再在函数内部局部定义类或数据结构:

void foo() {
    struct Data {
        void mem(auto);   // 错误:不能在函数内部声明模板
    };
}
1
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;
};
1
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();                    // 正确
}
1
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;
};
1
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;
};
...
1
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);         // 错误:无法推导排序准则中参数的类型
1
2
3
4
5
6

请记住,lessByName()的声明等效于:

template<typename T1, typename T2>
bool lessByNameFunc(const T1& c1, const T2& c2) {       // 排序准则
    return c1.getName() < c2.getName();                 // - 按名称比较
}
1
2
3
4

由于函数模板不是直接被调用,编译器无法推导模板参数来编译调用。因此,在将函数模板作为参数传递时,必须显式指定模板参数:

std::sort(persons.begin(), persons.end(),
          lessByName<Customer, Customer>);   // 正确
1
2

而使用lambda表达式时,可以直接传递:

auto lessByNameLambda = [] (const auto& c1, const auto& c2) {    // 排序准则
    return c1.getName() < c2.getName();   // 按名称比较
};
...
std::sort(persons.begin(), persons.end(), lessByNameLambda);     // 正确
1
2
3
4
5

原因在于lambda表达式是一个对象,它本身没有泛型类型。只有将该对象作为函数使用时才具有泛型特性。

另一方面,显式指定(简化的)函数模板参数更为容易:

  • 只需在函数名后传递指定的类型:
void printFunc(const auto& arg) {
   ...
}
printFunc<std::string>("hello ");   // 调用为std::string编译的函数模板
1
2
3
4
  • 对于泛型lambda表达式,则需要这样做:
auto printFunc = [] (const auto& arg) {
   ...
};

printFunc.operator()<std::string>("hello ");   // 调用为std::string编译的lambda表达式
1
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
1
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
1
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);
1
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
1
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);     // 错误:存在歧义
}
1
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) 中确定。

上次更新: 2025/03/20, 19:44:38
第1章 比较和<=>运算符
第3章 概念、要求和约束

← 第1章 比较和<=>运算符 第3章 概念、要求和约束→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式