C++ 参考手册
- C++11
- C++14
- C++17
- C++20
- C++ 编译器支持情况表
- 独立与宿主实现
- C++ 语言
- 变量模板(C++14 起)
- 整数字面量
- 聚合初始化
- 比较运算符
- 默认比较(C++20 起)
- 转义序列
- for 循环
- while 循环
- 用户定义转换
- SFINAE
- 主函数
- ASCII 码表
- 标识符
- 类型
- 内存模型
- 对象
- 基本概念
- 表达式
- 声明
- 初始化
- 函数
- 语句
- 类
- 运算符重载
- 模板
- 异常
- 事务性内存
- 占位符类型说明符 (C++11 起)
- decltype 说明符
- 函数声明
- final 说明符 (C++11 起)
- override 说明符(C++11 起)
- 引用声明
- 移动构造函数
- 移动赋值运算符
- 枚举声明
- constexpr 说明符(C++11 起)
- 列表初始化 (C++11 起)
- 构造函数与成员初始化器列表
- using 声明
- nullptr,指针字面量
- 基础类型
- 类型别名,别名模版 (C++11 起)
- 形参包
- 联合体声明
- 字符串字面量
- 用户定义字面量 (C++11 起)
- 属性说明符序列(C++11 起)
- Lambda 表达式 (C++11 起)
- noexcept 说明符 (C++11 起)
- noexcept 运算符 (C++11 起)
- alignof 运算符(C++11 起)
- alignas 说明符 (C++11 起)
- 存储类说明符
- 基于范围的 for 循环 (C++11 起)
- static_assert 声明
- 隐式转换
- 代用运算符表示
- 自增/自减运算符
- 折叠表达式(C++17 起)
- 类模板实参推导(C++17 起)
- 模板形参与模板实参
- if 语句
- inline 说明符
- 结构化绑定声明 (C++17 起)
- switch 语句
- 字符字面量
- 命名空间
- 求值顺序
- 复制消除
- consteval 说明符 (C++20 起)
- constinit 说明符 (C++20 起)
- 协程 (C++20)
- 模块 (C++20 起)
- 约束与概念 (C++20 起)
- new 表达式
- do-while 循环
- continue 语句
- break 语句
- goto 语句
- return 语句
- 动态异常说明
- throw 表达式
- try 块
- 命名空间别名
- 类声明
- cv(const 与 volatile)类型限定符
- 默认初始化
- 值初始化(C++03 起)
- 零初始化
- 复制初始化
- 直接初始化
- 常量初始化
- 引用初始化
- 值类别
- C++ 运算符优先级
- 布尔字面量
- 浮点字面量
- typedef 说明符
- 显式类型转换
- static_cast 转换
- dynamic_cast 转换
- const_cast 转换
- reinterpret_cast 转换
- delete 表达式
- 构造函数与成员初始化器列表
- this 指针
- 访问说明符
- 友元声明
- virtual 函数说明符
- explicit 说明符
- 静态成员
- 默认构造函数
- 复制构造函数
- 复制赋值运算符
- 析构函数
- 类模板
- 函数模板
- 显式(全)模板特化
- 汇编声明
- C++ 的历史
- 作用域
- 生存期
- 定义与单一定义规则(ODR)
- 名字查找
- 有限定的名字查找
- 无限定的名字查找
- 如同规则
- 未定义行为
- 翻译阶段
- 常量表达式
- 赋值运算符
- 算术运算符
- 逻辑运算符
- 成员访问运算符
- 其他运算符
- sizeof 运算符
- typeid 运算符
- 指针声明
- 数组声明
- 语言链接
- 详述类型说明符
- 默认实参
- 变长实参
- 实参依赖查找
- 重载决议
- 重载函数的地址
- 注入类名
- 非静态数据成员
- 非静态成员函数
- 嵌套类
- 派生类
- 空基类优化
- 抽象类
- 位域
- 转换构造函数
- 成员模板
- 模板实参推导
- 部分模板特化
- sizeof... 运算符
- 待决名
- 函数 try 块
- 扩充命名空间 std
- 字母缩写
- RAII
- 三/五/零之法则
- PImpl
- 零开销原则
- 类型
- 隐式转换
- 注释
- C++ 关键词
- 预处理器
- C++ 标准库头文件
- 具名要求
- 功能特性测试 (C++20)
- 工具库
- 类型支持(基本类型、RTTI、类型特性)
- 概念库 (C++20)
- 错误处理
- 动态内存管理
- 日期和时间工具
- 字符串库
- 容器库
- 迭代器库
- 范围库 (C++20)
- 算法库
- 数值库
- 输入/输出库
- 文件系统库
- 本地化库
- 正则表达式库
- 原子操作库
- 线程支持库
- 实验性 C++ 特性
- 有用的资源
- 索引
- std 符号索引
- 协程支持 (C++20)
- C++ 关键词
类模板实参推导(C++17 起)
为了实例化类模板,必须知晓每个模板实参,但不必每个模板实参都被指定。在下列语境中,编译器会从初始化器的类型推导缺失的模板实参:
- 任何指定变量及变量模板初始化的声明
std::pair p(2, 4.5); // 推导出 std::pair<int, double> p(2, 4.5); std::tuple t(4, 3, 2.5); // 同 auto t = std::make_tuple(4, 3, 2.5); std::less l; // 同 std::less<void> l;
template<class T> struct A { A(T,T); }; auto y = new A{1,2}; // 分配的类型是 A<int>
- 函数式转型表达式
auto lck = std::lock_guard(mtx); // 推导出 std::lock_guard<std::mutex> std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); // 或 std::back_inserter(vi2) std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // 推导 Foo<T>,其中 T // 是独有的 lambda 类型
template<class T> struct X { X(T) {} auto operator<=>(const X&) const = default; }; template<X x> struct Y { }; Y<0> y; // OK , Y<X<int>(0)> |
(C++20 起) |
类模板的推导
隐式生成的推导指引
当函数式转型或变量声明使用主类模板 C
的名字为类型说明符,而不带实参列表时,以如下方式进行推导:
- 若已定义
C
,则对所指名的主模板(若其已定义)中所声明的每个构造函数(或构造函数模板)Ci
,构造一个虚设的函数模板Fi
,使得
-
Fi
的模板形参是C
的模板形参后随(若Ci
是构造函数模板)Ci
的模板形参(亦包含默认模板实参) -
Fi
的函数形参是构造函数形参 -
Fi
的返回类型是C
后随环绕于 <> 中的类模板的模板形参
-
- 若未定义
C
或未其声明任何构造函数,则添加一个导出自假想的构造函数C()
的额外的虚设函数模板 - 任何情况下,都添加一个导出自假想构造函数
C(C)
的额外的虚设函数模板,称之为复制推导候选。
template<class T> struct A { T t; struct { long a, b; } u; }; A a{1, 2, 3}; // 聚合推导候选: template<class T> A<T> F(T, long, long); template<class... Args> struct B : std::tuple<Args...>, Args... {}; B b{ std::tuple<std::any, std::string>{}, std::any{} }; // 聚合推导候选: // template<class... Args> B<Args...> F(std::tuple<Args...>, Args...); // 推出 b 的类型为 B<std::any, std::string> |
(C++20 起) |
然后,针对某个假想类类型的虚设对象的初始化,进行模板实参推导和重载决议,对于组成重载集而言,该类的各构造函数的签名与各个指引(除了返回类型)相匹配,并且由进行类模板实参推导的语境提供其初始化器。但若其初始化器列表由单个(可为 cv 限定的)U
类型的表达式组成,其中 U
是 C
的特化或派生自 C
的特化的类,则省去列表初始化的第一阶段(考虑初始化器列表构造函数)。
这些虚设构造函数是该假想类类型的公开成员。若推导指引从显式构造函数组成,则它们为 explicit。若重载决议失败,则程序非良构。否则,选中的 F
的返回类型就成为推导出的类模板特化。
template<class T> struct UniquePtr { UniquePtr(T* t); }; UniquePtr dp{new auto(2.0)}; // 一个声明的构造函数: // C1:UniquePtr(T*); // 隐式生成的推导指引集: // F1:template<class T> UniquePtr<T> F(T *p); // F2:template<class T> UniquePtr<T> F(UniquePtr<T>); // 复制推导候选 // 要初始化的假想类: // struct X { // template<class T> X(T *p); // 从 F1 // template<class T> X(UniquePtr<T>); // 从 F2 // }; // X 对象的直接初始化,以“new double(2.0)”为初始化器 // 选择对应于 T = double 的指引 F1 的构造函数 // 对于 T=double 的 F1,返回类型是 UniquePtr<double> // 结果: // UniquePtr<double> dp{new auto(2.0)}
或者,对于更加复杂的例子(注意:“S::N”无法编译:作用域解析限定符并非可推导内容):
template<class T> struct S { template<class U> struct N { N(T); N(T, U); template<class V> N(V, U); }; }; S<int>::N x{2.0, 1}; // 隐式生成的推导指引是(注意已知 T 是 int) // F1:template<class U> S<int>::N<U> F(int); // F2:template<class U> S<int>::N<U> F(int, U); // F3:template<class U, class V> S<int>::N<U> F(V, U); // F4:template<class U> S<int>::N<U> F(S<int>::N<U>); (复制推导候选) // 以“{2.0, 1}”为初始化器的直接列表初始化的重载决议 // 选择 U=int 与 V=double 的 F3。 // 返回类型为 S<int>::N<int> // 结果: // S<int>::N<int> x{2.0, 1};
用户定义推导指引
用户定义推导指引的语法是带尾随返回类型的函数声明的语法,但它以类名为函数名:
explicit-说明符(可选) 模板名 ( 形参声明子句 ) -> 简单模板标识 ;
|
|||||||||
用户定义推导指引必须指名一个类模板,且必须在类模板的同一语义作用域(可以是命名空间或外围类)中引入,而且对于成员类模板,必须拥有同样的访问,但推导指引不成为该作用域的成员。
推导指引不是函数且没有函数体。推导指引不会被名字查找所找到,并且除了在推导类模板实参时与其他推导指引之间的重载决议之外,不参与重载决议。不能在同一翻译单元中为同一类模板再次声明推导指引。
// 模板的声明 template<class T> struct container { container(T t) {} template<class Iter> container(Iter beg, Iter end); }; // 额外推导指引 template<class Iter> container(Iter b, Iter e) -> container<typename std::iterator_traits<Iter>::value_type>; // 使用 container c(7); // OK:用隐式生成的指引推出 T=int std::vector<double> v = { /* ... */}; auto d = container(v.begin(), v.end()); // OK:推出 T=double container e{5, 6}; // 错误:无 std::iterator_traits<int>::value_type
为重载决议而虚设的构造函数(如上文所述),若其对应于从显式构造函数组成的隐式生成的推导指引,或对应于声明为 explicit
的用户定义推导指引,则为 explicit。像往常一样,在复制初始化语境中忽略这些构造函数:
template<class T> struct A { explicit A(const T&, ...) noexcept; // #1 A(T&&, ...); // #2 }; int i; A a1 = { i, i }; // 错误:不能从 #2 的右值引用推导,且 #1 为 explicit,复制初始化中不予考虑。 A a2{i, i}; // OK,#1 推出 A<int> 并且初始化 A a3{0, i}; // OK,#2 推出 A<int> 并且初始化 A a4 = {0, i}; // OK,#2 推出 A<int> 并且初始化 template<class T> A(const T&, const T&) -> A<T&>; // #3 template<class T> explicit A(T&&, T&&) -> A<T>; // #4 A a5 = {0, 1}; // 错误:#3 推出 A<int&> 且 #1 & #2 生成形参相同的构造函数。 A a6{0,1}; // OK,#4 推出 A<int> 并以 #2 初始化 A a7 = {0, i}; // 错误:#3 推出 A<int&> A a8{0,i}; // 错误:#3 推出 A<int&>
在构造函数或构造函数模板的形参列表中使用成员 typedef 或别名模板,此行为自身不会使隐式生成的指引的对应形参变为非推导语境。
template<class T> struct B { template<class U> using TA = T; template<class U> B(U, TA<U>); //#1 }; // 从 #1 产生的隐式推导指引等价于 // template<class T, class U> B(U, T) -> B<T>; // 而非 // template<class T, class U> B(U, typename B<T>::template TA<U>) -> B<T>; // 这是无法推导的 B b{(int*)0, (char*)0}; // OK,推出 B<char*>
别名模版的推导函数风格转型或变量定义以不带实参列表的别名模板
template<class T> class unique_ptr { /* ... */ }; template<class T> class unique_ptr<T[]> { /* ... */ }; template<class T> unique_ptr(T*) -> unique_ptr<T>; // #1 template<class T> unique_ptr(T*) -> unique_ptr<T[]>; // #2 template<class A> requires(!std::is_array_v<A>) using unique_ptr_nonarray = unique_ptr<A>; template<class A> using unique_ptr_array = unique_ptr<A[]>; // 对 unique_ptr_nonarray 生成的推导指引: // 从 #1 ( unique_ptr<T> 从 unique_ptr<A> 的推导产生 T = A ): // template<class A> // requires(argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<A>>) // auto F(A*) -> unique_ptr<A>; // 从 #2 ( unique_ptr<T[]> 从 unique_ptr<A> 的推导不产生结果): // template<class T> // requires(argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<T[]>>) // auto F(T*) -> unique_ptr<T[]>; // 其中 argument_of_unique_ptr_nonarray_is_deducible_from 能定义为 // template<class> class AA; // template<class A> class AA<unique_ptr_nonarray<A>> {}; // template<class T> // concept argument_of_unique_ptr_nonarray_is_deducible_from = // requires { sizeof(AA<T>); }; // 对 unique_ptr_array 生成的推导指引: // 从 #1 ( unique_ptr<T>从 unique_ptr<A[]> 的推导生成 T = A[] ): // template<class A> // requires(argument_of_unique_ptr_array_is_deducible_from<unique_ptr<A[]>>) // auto F(A(*)[]) -> unique_ptr<A[]>; // 从 #2 ( unique_ptr<T[]> 从 unique_ptr<A[]> 的推导生成 T = A ): // template<class A> // requires(argument_of_unique_ptr_array_is_deducible_from<unique_ptr<A[]>>) // auto F(A*) -> unique_ptr<A[]>; // 其中 argument_of_unique_ptr_array_is_deducible_from 能定义为 // template<class> class BB; // template<class A> class BB<unique_ptr_array<A>> {}; // template<class T> // concept argument_of_unique_ptr_array_is_deducible_from = requires { sizeof(BB<T>); }; // 用法: unique_ptr_nonarray p(new int); // 推导出 unique_ptr<int> // 从 #1 生成的推导指引返回 unique_ptr<int> // 从 #2 生成的推导指引返回 unique_ptr<int[]> ,它被忽略,因为 // argument_of_unique_ptr_nonarray_is_deducible_from<unique_ptr<int[]>> 得不到满足 unique_ptr_array q(new int[42]); // 推导出 unique_ptr<int[]> // 从 #1 生成的推导指引失败(不能从 new int[42] 推出 A(*)[] 中的 A ) // 从 #2 生成的推导指引返回 unique_ptr<int[]> |
(C++20 起) |
注解
仅当不存在模板实参列表时才进行类模板实参推导。若指定了模板实参列表,则不发生推导。
std::tuple t1(1, 2, 3); // OK:推导 std::tuple<int, int, int> t2(1, 2, 3); // OK:提供所有实参 std::tuple<> t3(1, 2, 3); // 错误:tuple<> 中无匹配的构造函数。 // 不进行推导 std::tuple<int> t4(1, 2, 3); // 错误
聚合体的类模板实参推导常需要用户定义的推导指引: template<class A, class B> struct Agg {A a; B b; }; // 隐式生成的指引由默认、复制及移动构造函数组成 template<class A, class B> Agg(A a, B b) -> Agg<A, B>; // ^ 此推导指引在 C++20 中能隐式生成 Agg agg{1, 2.0}; // 从用户定义指引推出 Agg<int, double> template <class... T> array(T&&... t) -> array<std::common_type_t<T...>, sizeof...(T)>; auto a = array{1, 2, 5u}; // 从用户定义指引推出 array<unsigned, 3> |
(C++20 前) |
用户定义指引不必是模板:
template<class T> struct S { S(T); }; S(char const*) -> S<std::string>; S s{"hello"}; // 推出 S<std::string>
在类模板的作用域中,无形参列表的模板名是注入类名,并可用作类型。这种情况下,不发生类模板推导,而必须显式提供其模板形参:
template<class T> struct X { X(T) { } template<class Iter> X(Iter b, Iter e) { } template<class Iter> auto foo(Iter b, Iter e) { return X(b, e); // 无推导:X 是当前的 X<T> } template<class Iter> auto bar(Iter b, Iter e) { return X<typename Iter::value_type>(b, e); // 必须指定所需的实参 } auto baz() { return ::X(0); // 非注入类名;推导为 X<int> } };
在重载决议中,偏序在是否从用户定义推导指引生成函数模板问题上更优先:若从构造函数生成的函数模板比从用户定义推导指引生成者更特化,则选择从构造函数生成的。因为复制推导候选常常比包装构造函数更特殊,故此规则表明复制通常更优先于包装。
template<class T> struct A { A(T, int*); // #1 A(A<T>&, int*); // #2 enum { value }; }; template<class T, int N = T::value> A(T&&, int*) -> A<T>; //#3 A a{1,0}; // 使用 #1 推出 A<int> 并以 #1 初始化 A b{a,0}; // 使用 #2(比 #3 更特殊)推出 A<int> 并以 #2 初始化
当之前的决胜规则(包括偏序)无法分辨两个候选函数模板时,应用下列规则:
- 由用户定义指引生成的函数模板比从构造函数或构造函数模板隐式生成的函数模板更受偏好。
- 复制推导候选比所有其他从构造函数或构造函数模板隐式生成的函数模板更受偏好。
- 从非模板构造函数函数模板的函数模板比从构造函数模板的隐式生成的函数模板更受偏好。
template <class T> struct A { using value_type = T; A(value_type); // #1 A(const A&); // #2 A(T, T, int); // #3 template<class U> A(int, T, U); // #4 }; // A(A); #5,复制推导候选 A x (1, 2, 3); // 使用 #3,从非模板构造函数生成 template <class T> A(T) -> A<T>; // #6,比 #5 更不特殊 A a (42); // 使用 #6 推出 A<int> 并以 #1 初始化 A b = a; // 使用 #5 推出 A<int> 并以 #2 初始化 template <class T> A(A<T>) -> A<A<T>>; // #7,与 #5 一样特殊 A b2 = a; // 使用 #7 推出 A<A<int>> 并以 #1 初始化
若模板形参是类模板形参,则到该形参的无 cv 限定的右值引用不是转发引用:
template<class T> struct A { template<class U> A(T&&, U&&, int*); // #1:T&& 不是转发引用 // U&& 是转发引用 A(T&&, int*); // #2:T&& 不是转发引用 }; template<class T> A(T&&, int*) -> A<T>; // #3:T&& 是转发引用 int i, *ip; A a{i, 0, ip}; // 错误,不能从 #1 推导 A a0{0, 0, ip}; // 使用 #1 推出 A<int> 并以 #1 初始化 A a2{i, ip}; // 使用 #3 推出 A<int&> 并以 #2 初始化
当从类模板某个特化类型的单个实参进行的初始化有问题时,通常与默认的包装相比,更偏好复制推导:
std::tuple t1{1}; //std::tuple<int> std::tuple t2{t1}; //std::tuple<int>,非 std::tuple<std::tuple<int>> std::vector v1{1, 2}; // std::vector<int> std::vector v2{v1}; // std::vector<int>,非 std::vector<std::vector<int>> (P0702R1) std::vector v3{v1, v2}; // std::vector<std::vector<int>>
除了复制 VS. 包装的特殊情形外,列表初始化中保持对初始化器列表构造函数的强偏好。
std::vector v1{1, 2}; // std::vector<int> std::vector v2(v1.begin(), v1.end()); // std::vector<int> std::vector v3{v1.begin(), v1.end()}; // std::vector<std::vector<int>::iterator>
在引入类模板实参推导前,避免显式指定实参的常用手段是使用函数模板:
std::tuple p1{1, 1.0}; // std::tuple<int, double>,使用推导 auto p2 = std::make_tuple(1, 1.0); // std::tuple<int, double>,C++17 前
缺陷报告
下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。
DR | 应用于 | 出版时的行为 | 正确行为 |
---|---|---|---|
P0702R1 | C++17 | 初始化器列表构造函数能架空复制推导候选,导致产生包装 | 复制时跳过初始化器列表阶段 |