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章 函数参数的占位符类型
  • 第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章 泛型编程的小改进
    • 22.1 模板参数类型成员的隐式typename
      • 22.1.1 隐式typename的详细规则
    • 22.2 泛型代码中聚合类型的改进
      • 22.2.1 聚合类型的类模板参数推导(CTAD)
    • 22.3 条件显式
      • 22.3.1 标准库中的条件显式(构造函数)
    • 22.4 补充说明
  • 第23章 C++标准库的小改进
  • 第24章 已弃用和移除的特性
  • cpp20completeguides
zhangxf
2025-03-20
目录

第22章 泛型编程的小改进

# 第22章 泛型编程的小改进

本章介绍C++20中尚未在本书中提及的泛型编程的其他特性和扩展。

# 22.1 模板参数类型成员的隐式typename

在使用模板参数的类型成员时,通常需要使用typename关键字进行限定:

template<typename  T>
typename  T::value_type  getElem(const  T&  cont,  typename  T::iterator  pos) {
    using  Itor  =  typename  T::iterator;
    typename  T::value_type  elem;
   ...
    return  elem;
}
1
2
3
4
5
6
7

在C++20之前,对T的类型成员value_type和iterator的所有限定都是必要的。自C++20起,在类型传递明确的上下文中,可以省略typename。在这种情况下,这适用于返回类型的指定以及别名声明中使用的类型(其中using为类型引入新名称):

template<typename  T>
T::value_type  getElem(const  T&  cont,  typename  T::iterator  pos) {
    using  Itor  =  T::iterator;
    typename  T::value_type  elem;
   ...
    return  elem;
}
1
2
3
4
5
6
7

注意,参数pos和变量elem仍然需要typename。现在可以省略typename的最重要场景如下:

  • 在声明返回类型时(局部前向声明除外);
  • 在类模板中声明成员时;
  • 在类模板中声明成员函数或友元函数的参数时;
  • 在声明requires表达式的参数时;
  • 在别名声明中的类型。

特别是在声明类模板时,大多数情况下现在可以省略typename:

template<typename  T> 
class  MyClass  {
    T::value_type  val;               //自C++20起无需使用`typename`
public:
   ...
    T::iterator  begin()  const;      //自C++20起无需使用`typename`
    T::iterator  end()  const;        //自C++20起无需使用`typename`
    void  print(T::iterator)  const;  //自C++20起无需使用`typename`
};
1
2
3
4
5
6
7
8
9

然而,由于隐式typename的规则在某种程度上相当微妙,在使用模板参数的类型成员时(至少在类模板之外),你可能仍然总是使用typename 1。

# 22.1.1 隐式typename的详细规则

自C++20起,在以下情况下使用模板参数的类型成员时可以省略typename:

  • 在别名声明中(即使用using声明类型名称时);请注意,使用typedef的类型声明仍然需要typename;
  • 在定义或声明函数的返回类型时(除非声明发生在函数或块作用域内);
  • 在声明后置返回类型时;
  • 在指定static_cast、const_cast、reinterpret_cast或dynamic_cast的目标类型时;
  • 在指定new的类型时;
  • 在类内部:
    • 声明数据成员时;
    • 声明成员函数的返回类型时;
    • 声明成员函数、友元函数或lambda的参数时(默认参数可能仍然需要它);
  • 在requires表达式中声明参数类型时;
  • 在声明模板类型参数的默认值时;
  • 在声明非类型模板参数的类型时。

注意,在C++20之前,在其他一些情况下也不需要typename:

  • 在指定继承类的基类型时;
  • 在构造函数中向基类传递初始值时;
  • 在类声明内部使用类型成员时。

以下示例展示了上述大多数情况(这里,在自C++20起typename可选的地方使用TYPENAME):

template<typename  T,
         auto  ValT  =  typename  T::value_type{}>  	// typename必需
class  MyClass  {
    TYPENAME  T::value_type  val;  						// typename可选
public:
    using  iterator  =  TYPENAME  T::iterator;  		// typename可选
    TYPENAME  T::iterator  begin()  const;  			// typename可选
    TYPENAME  T::iterator  end()  const;  				// typename可选
    void  print(TYPENAME  T::iterator)  const;  		// typename可选
    template<typename  T2  =  TYPENAME  T::value_type>  // 第二个typename可选
    void  assign(T2); 
};

template<typename  T>
TYPENAME  T::value_type  								// typename可选
foo(const  T&  cont,  typename  T::value_type  arg)  	// typename必需
{
    typedef  typename  T::value_type  ValT2;  			// typename必需
    using  ValT1  =  TYPENAME  T::value_type;  			// typename可选
    typename  T::value_type  val;  						// typename必需
    typename  T::value_type  other1(void);  			// typename必需
    auto  other2(void)  ->  TYPENAME  T::value_type;  	// typename可选
    auto  l1  =  []  (TYPENAME  T::value_type)  {  		// typename可选
    };
    auto  p  =  new  TYPENAME  T::value_type;  			// typename可选
    val  =  static_cast<TYPENAME  T::value_type>(0);  	// typename可选
   ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 22.2 泛型代码中聚合类型的改进

C++20为聚合类型提供了一些改进。对于泛型代码,现在我们有:

  • 对聚合类型使用类模板参数推导(CTAD,Class Template Argument Deduction)。
  • 聚合类型可用作非类型模板参数(NTTP,Non-Type Template Parameter)。

本节将介绍前者。

注意,聚合类型还有其他新特性:

  • (部分)支持指定初始化器(特定成员的初始值)。
  • 可以使用括号初始化聚合类型。
  • 聚合类型的固定定义以及对std::is_default_constructible<>的影响。

# 22.2.1 聚合类型的类模板参数推导(CTAD)

自C++17起,构造函数可用于推导类模板的模板参数。例如:

template<typename  T>
class  Type  {
    T  value;
public:
    Type(T  val)
        :  value{val}  { }
   ...
};

Type<int>  t1{42};
Type  t2{42}; // 自C++17起推导为Type<int>
1
2
3
4
5
6
7
8
9
10
11

然而,即使对于简单的聚合类型,以前也不支持根据对象的初始化方式进行类似的推导:

template<typename  T>
struct  Aggr  {
    T  value; 
};

Aggr<int>  a1{42}; 	// 正确
Aggr  a2{42};   	// 在C++20之前错误
1
2
3
4
5
6
7

你必须提供一个推导指引:

template<typename  T>
struct  Aggr  {
    T  value; 
};

template<typename  T>
Aggr(T)  ->  Aggr<T>;

Aggr<int>  a1{42}; 	// 正确
Aggr  a2{42};  		// 自C++17起正确           
1
2
3
4
5
6
7
8
9
10

自C++20起,不再需要推导指引,这意味着以下代码就足够了:

template<typename  T>
struct  Aggr  {
    T  value; 
};

Aggr<int>  a1{42};   // 正确
Aggr  a2{42};        // 自C++20起,即使没有推导指引也正确
1
2
3
4
5
6
7

注意,这个特性在使用括号初始化聚合类型时同样适用:

Aggr  a3(42);        // 自C++20起,即使没有推导指引也正确
1

推导规则可能很微妙,如下例所示:

template<typename  T>
struct  S  {
    T  x;
    T  y;
};

template<typename  T>
struct  C  {
    S<T>  s;
    T  x;
    T  y;
};

C  c1  =  {{1,  2},  3,  4};	// 正确,推导为C<int>
C  c2  =  {{1,  2},  3};		// 正确,推导为C<int>(y为0)
C  c3  =  {{1,  2},  3.3,  4.4};// 正确,推导为C<double>
C  c4  =  {{1,  2},  3,  4.4};	// 错误:为T推导出int和double
C  c5  =  {{1,  2}}; 			// 错误:T只能间接推导
C  c6  =  {1,  2,  3};			// 错误:不知道S<T>需要多少个值
C<int>  c7  =  {1,  2,  3};		// 正确
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

注意,类模板参数推导甚至适用于元素数量可变的聚合类型:

// 具有可变数量基类型的聚合类型:
template<typename ...  T>
struct  C  :  T ...  { };

struct  Base1  { };
struct  Base2  { };

// 用Base1和Base2类型的两个元素初始化聚合类型:
C  c1{Base1{},  Base2{}};

// 用三个lambda元素初始化聚合类型:
C  c2{[]  {  f1();  }, []  {  f2();  },
     []  {  f3();  } };
1
2
3
4
5
6
7
8
9
10
11
12
13

# 22.3 条件显式

为了禁用隐式类型转换,构造函数可以声明为explicit。然而,对于泛型代码,你可能希望仅当类型参数具有显式构造函数时,才将构造函数声明为显式的。这样,你可以完美地将类型转换支持委托给包装类型。

指定条件显式(conditional explicit)的方式与指定条件noexcept类似。在explicit之后,你可以在括号中指定一个布尔编译时表达式。以下是一个示例:

// lang/wrapper.hpp
#include  <type_traits>   // 用于std::is_convertible_v<>

template<typename  T>
class  Wrapper  {
    T  value;
public:
    template<typename U>
    explicit(! std::is_convertible_v<U,  T>) Wrapper(const U&  val)
        :value{val}  { }
   ...
};
1
2
3
4
5
6
7
8
9
10
11
12

持有类型为T的值的类模板Wrapper<>有一个泛型构造函数,这意味着你可以用任何可隐式转换为T的类型来初始化value。如果从U到T没有隐式转换,该构造函数就是显式的。

因此,只有当存在启用的隐式类型转换时,才能初始化某个类型的Wrapper。例如:

// lang/explicitwrapper.cpp
#include  "wrapper.hpp"
#include  <string>
#include  <vector>

void  printStringWrapper(Wrapper<std::string>)  { }
void  printVectorWrapper(Wrapper<std::vector<std::string>>)  { }

int main() {
    // 从字符串字面量到string的隐式转换:
    std::string  s1{ "hello "};
    // 正确
    // 正确
    // 正确
    std::string  s2  =  "hello ";
    Wrapper<std::string>  ws1{ "hello "};   
    Wrapper<std::string>  ws2  =  "hello ";
    printStringWrapper( "hello ");

    // 从size到vector<string>没有隐式转换:
    // 错误:显式
    // 错误:显式
    // 错误:显式
    std::vector<std::string>  v1{42u};   
    std::vector<std::string>  v2  =  42u;
    Wrapper<std::vector<std::string>>  wv1{42u};   
    Wrapper<std::vector<std::string>>  wv2  =  4u2;
    printVectorWrapper(42u);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

为了展示条件显式的效果,我们对字符串和字符串向量使用Wrapper<>:

  • 对于std::string类型,构造函数启用了从字符串字面量的隐式转换。因此,Wrapper<>类型的构造函数不是显式的,这使得我们可以通过传递字符串字面量来复制初始化一个字符串包装器,或者将字符串字面量传递给一个接受字符串包装器的函数。
  • 对于std::vector<std::string>类型,在C++标准库中,接受无符号大小的构造函数被声明为显式的。因此,从size到vector的std::is_convertible<>为false,Wrapper<>构造函数变为显式的。因此,我们也不能传递一个size来初始化字符串向量的包装器,或者将一个size传递给接受该包装器类型的函数。

请记住,隐式转换对于复制初始化(使用=进行初始化)和参数传递是必需的。

条件显式可用于任何可以使用explicit的地方。因此,你也可以用它来使转换运算符成为条件显式的:

template<typename  T>
class  MyType  {
public:
   ...
    explicit(! std::is_convertible_v<T,  bool>)  operator  bool();
};
1
2
3
4
5
6

不过,我没有这方面有用的示例。转换为bool(这是转换运算符最有用的应用)通常应该总是显式的,这样你就不会意外地将MyType对象传递给期望布尔值的函数。

# 22.3.1 标准库中的条件显式(构造函数)

C++标准库在多个地方使用了条件显式(构造函数)。例如,std::pair<>和std::tuple<>使用它来确保仅当存在隐式转换时,才支持对类型略有不同的pair和tuple进行赋值操作。

例如:

std::pair<int, int> p1{11, 11};

std::pair<long, long> p2{};
p2 = p1;  // 正确:从int到long的隐式转换

std::pair<std::chrono::day, std::chrono::month> p3{};
p3 = p1;  // 错误
p3 = std::pair<std::chrono::day, std::chrono::month>{p1}; // 正确
1
2
3
4
5
6
7
8

由于chrono日期类型中接受整数值的构造函数是显式的,因此从int类型的pair赋值给day和month类型的pair会失败。你必须使用显式转换。

这在向map中插入元素时同样适用(因为元素是键值对):

std::map<std::string, std::string> coll1;
coll1.insert({"hi", "ho"});                      // 正确:使用到字符串的隐式转换

std::map<std::string, std::chrono::month> coll2;
coll2.insert({"XI", 11});                        // 错误:没有合适的隐式转换
coll2.insert({"XI", std::chrono::month{11}}); 	 // 正确(插入包含字符串和月份的元素)
1
2
3
4
5
6

std::pair<>的这种行为并不是新特性。然而,在C++20之前,标准库的实现必须使用SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)来实现显式构造函数的条件行为(声明两个构造函数,并在条件不满足时禁用其中一个) 。

再例如,std::span<>的构造函数是条件显式的:

namespace std {
    template<typename ElementType, size_t Extent = dynamic_extent>
    class span {
    public:
        static constexpr size_type extent = Extent;
        // ...
        constexpr span() noexcept;
        template<typename It>
        constexpr explicit(extent != dynamic_extent) span(It first, size_type count);
        template<typename It, typename End>
        constexpr explicit(extent != dynamic_extent) span(It first, End last);
        // ...
    };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

因此,只有当span具有动态范围时,才允许对其进行隐式类型转换。

# 22.4 补充说明

在某些情况下使typename可选的提议最初由达韦德·范德沃德(Daveed Vandevoorde)在http://wg21.link/p0634r0 (opens new window)中提出。最终被接受的措辞由尼娜·兰斯(Nina Ranns)和达韦德·范德沃德在http://wg21.link/p0634r3 (opens new window)中制定。

聚合体的类模板参数推导(Class template argument deduction for aggregates)最初由迈克·斯珀特斯(Mike Spertus)在http://wg21.link/p1021r0 (opens new window)中提出。最终被接受的措辞由蒂穆尔·杜姆勒(Timur Doumler)在http://wg21.link/p1816r0 (opens new window)和http://wg21.link/p2082r1 (opens new window)中制定。

条件显式特性最初由巴里·列夫津(Barry Revzin)和斯蒂芬·T·拉瓦韦(Stephan T. Lavavej)在http://wg21.link/p0892r0 (opens new window)中提出。最终被接受的措辞由巴里·列夫津和斯蒂芬·T·拉瓦韦在http://wg21.link/p0892r2 (opens new window)中制定。

上次更新: 2025/03/20, 19:44:38
第21章 核心语言的小改进
第23章 C++标准库的小改进

← 第21章 核心语言的小改进 第23章 C++标准库的小改进→

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