第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;
}
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;
}
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`
};
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可选
...
}
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>
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之前错误
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起正确
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起,即使没有推导指引也正确
2
3
4
5
6
7
注意,这个特性在使用括号初始化聚合类型时同样适用:
Aggr a3(42); // 自C++20起,即使没有推导指引也正确
推导规则可能很微妙,如下例所示:
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}; // 正确
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(); } };
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} { }
...
};
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);
}
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();
};
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}; // 正确
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}}); // 正确(插入包含字符串和月份的元素)
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);
// ...
};
}
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)中制定。