在本章中,您将学习 C++ 中类型推导的所有来龙去脉,包括 C++ 17 中的一些新增内容。这一章很重要,因为它将教会你所有的方法,编译器将试图为你自动推导类型信息。如果没有对 C++ 中类型推导的工作原理有一个坚定的理解,就有可能创建出不像预期那样工作的代码,尤其是在使用auto
和模板编程的时候。从本章获得的知识将为您提供在自己的应用中适当利用类型推导的技能。
本章中的配方如下:
- 使用自动和类型推导
- 学习
decltype
类型推导规则如何工作 - 使用模板函数类型推导
- 在 C++ 17 中利用模板类类型推导
- 在 C++ 17 中使用用户定义的类型推导
要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须安装以下内容:
> sudo apt-get install build-essential git cmake
如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。
本章的代码文件可以在https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 12找到。
在这个食谱中,我们将学习编译器如何处理auto
关键字,特别是类型推导。这个方法很重要,因为如何处理auto
不是直观的,如果不清楚auto
是如何工作的,你的代码很可能包含错误和性能问题。本食谱中包含的主题有auto
的一般描述、类型推断、转发(或通用)参考、l 值和 r 值。
开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容:
> sudo apt-get install build-essential git cmake
这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。
执行以下步骤来尝试配方:
- 从新的终端,运行以下命令下载源代码:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter12
- 要编译源代码,请运行以下命令:
> cmake .
> make recipe01_examples
- 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例:
> ./recipe01_example01
i1 = int
i2 = int
i3 = std::initializer_list<int>
i4 = std::initializer_list<int>
c = char
r = int
> ./recipe01_example02
i1 = int
i2 = const int
i3 = volatile int
i4 = const volatile int
> ./recipe01_example03
i1 = int
i2 = int&
a1 = int
a2 = int
a3 = int
a4 = int&
i3 = int&&
a5 = int&
a6 = int&
a7 = int&
a8 = int&
a9 = int&&
a10 = int&&
> ./recipe01_example04
i1 = int
i2 = const int&
i3 = const int&&
在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。
auto
关键字是添加到 C++ 11 中的一个特性,称为占位符类型说明符。换句话说,auto
关键字用来告诉编译器一个变量的类型将从它的初始化式中推导出来。与其他使用占位符类型的语言不同,auto
关键字必须仍然遵循 C++ 严格的类型系统,这意味着auto
不应与std::any
混淆。
例如std::any
可能出现以下情况:
std::any i = 42;
i = "The answer is: 42";
auto
不允许出现以下情况:
auto i = 42;
i = "The answer is: 42";
在第一个例子中,我们定义std::any
,它存储一个整数。然后我们用 C 型字符串替换std::any
中的整数。关于auto
,这是不可能的,因为一旦编译器在初始化时推导出变量的类型,该类型就不能改变(与 C++ 中的任何其他变量没有区别)。
让我们看一个如何使用auto
初始化变量的简单例子:
int main(void)
{
auto i1 = 42;
auto i2{42};
auto i3 = {42};
auto i4 = {4, 8, 15, 16, 23, 42};
show_type(i1);
show_type(i2);
show_type(i3);
show_type(i4);
char c = 0;
auto r = c + 42;
show_type(c);
show_type(r);
}
运行此示例会产生以下输出:
如前面的代码所示,我们使用auto
创建四个变量,初始化它们,然后使用名为show_type()
的函数返回变量类型的输出。
For more information about how the show_type()
function works, please see the code that comes with this chapter (the details of this function will make more sense after you finish reading this entire chapter).
我们示例中的第一个变量i1
被推导为整数。这是因为 C++ 中的数值类型总是被推导为整数,在我们的例子中我们也看到了c
和r
变量。原因是在编译过程中允许编译器增加任何变量的大小,也就是说,当编译器看到c + 42
时,它做的第一件事就是在完成加法之前将c
的值存储在一个临时整数中。
在我们的例子中,第二个变量i2
也被推导为整数,因为{}
符号是 C++ 中任何类型的另一种初始化形式,带有一些额外的规则。具体来说,i3
和i4
被推导为整数的std::initializer_list
,因为最后两个使用了= {}
符号,这是由 C++ 规范定义的,在 C++ 17 中总是推导为std::initializer_list
。需要注意的是,这是假设编译器遵循规范,在这个特定的例子中并不总是这样,这就是为什么像 AUTOSAR 这样的关键系统规范不允许这种类型的初始化。
auto
关键字也可以和 CV 限定词(即const
/ volatile
)组合。看看这个例子:
int main(void)
{
auto i1 = 42;
const auto i2 = 42;
volatile auto i3 = 42;
const volatile auto i4 = 42;
show_type(i1);
show_type(i2);
show_type(i3);
show_type(i4);
}
前面的示例导致以下输出:
如前面的截图所示,每个变量都用定义好的 CV 限定符修饰。
到目前为止,在每个例子中,我们都可以简单地用int
代替auto
的使用,没有什么会改变,这就引出了一个问题,为什么首先要使用auto
?原因有几个:
- 使用除
auto
以外的东西意味着您的代码可能会指定一个变量的类型两次。例如,int *ptr = new int;
声明ptr
变量是整数两次:一次在变量声明中,第二次在变量初始化中。 - C++ 中有些类型真的很长(例如迭代器),使用
auto
可以大大简化代码的冗长程度,例如auto i = v.begin()
。 - 编写模板代码时,要求
auto
正确处理引用类型,如转发引用。
使用参考文献是auto
的使用变得混乱的地方,也是大多数人犯错的地方。为了更好地解释,让我们看看下面的例子:
int main(void)
{
int i = 42;
int i1 = i;
int &i2 = i;
show_type(i1);
show_type(i2);
auto a1 = i1;
auto a2 = i2;
show_type(a1);
show_type(a2);
}
这将产生以下输出:
i1 = int
i2 = int&
a1 = int
a2 = int
如前例所示,我们创建一个整数i
,并将其设置为42
。然后我们再创建两个整数:一个是i
的副本,而第二个是i
的引用。如输出所示,我们得到期望的类型,int
和int&
。使用auto
关键字,我们可以预期,如果我们说类似auto a = i2
的话,我们会得到一个int&
类型,因为i2
是对一个整数的引用,并且由于auto
是根据它是如何初始化的来推导它的类型的,所以我们应该得到int&
。问题是,我们没有。相反,我们得到int
。
这样做的原因是auto
根据它是如何初始化的来获取它的类型,而不包括引用类型。换句话说,示例中auto
的使用只是拾取i2
类型,而不关注i2
是否为整数或对整数的引用。要强制auto
成为整数的引用,我们必须使用以下语法:
auto a3 = i1;
auto &a4 = i2;
show_type(a3);
show_type(a4);
这将产生以下输出:
a3 = int
a4 = int&
这个输出和预期的一样。同样的规则也适用于 r 值引用,但变得更加复杂。例如,考虑以下代码:
int &&i3 = std::move(i);
show_type(i3);
这将产生以下输出:
i3 = int&&
这个输出还是和预期的一样。根据我们已经了解到的情况,我们预计需要以下内容来获得 r 值参考:
auto &&a5 = i3;
show_type(a6);
问题是这会导致以下输出:
a5 = int&
如前面的例子所示,我们没有得到预期的 r 值引用。任何在 C++ 中被标记为auto &&
的东西都被认为是转发引用(这也被称为通用引用,一个由斯科特·迈耶斯创造的术语)。根据通用参考的初始化方式,通用参考将推导为 l 值或 r 值参考。
例如,考虑以下代码:
auto &&a6 = i1;
show_type(a6);
此代码导致以下结果:
a6 = int&
这是因为i1
之前被定义为一个整数,所以a6
变成了对i1
的 l 值引用。以下也是事实:
auto &&a7 = i2;
show_type(a7);
前面的代码导致以下结果:
a7 = int&
这是因为i2
之前被定义为对整数的 l 值引用,这意味着通用引用也变成了对整数的 l 值引用。
令人困惑的结果如下,如前面的代码片段所示:
auto &&a8 = i3;
show_type(a8);
这再次导致以下结果:
a8 = int&
这里,i3
早先被定义为对整数的 r 值引用(由结果输出证明),但是通用引用没有从i3
转发 r 值。这是因为,虽然i3
被定义为 r 值参考,但一旦被使用,它就变成了 l 值参考。正如斯科特·迈耶过去所说,如果一个变量有名字(在我们的例子中是i3
),它就是一个 l 值,即使它是从 r 值开始的。另一种看待这个问题的方式是,一旦使用了一个变量(就像以任何方式访问一样),这个变量就是一个 l 值。因此,前面的代码实际上正常工作。i3
虽然被定义为 r 值,但它是 l 值,因此通用参考成为整数的 l 值参考,就像i1
和i2
一样。
要使用auto
获得 r 值参考,您必须做与不使用auto
时相同的事情:
auto &&a9 = std::move(i3);
show_type(a9);
这将导致以下结果:
a9 = int&&
如前面的代码片段所示,思考auto
的最佳方式是简单地将单词auto
替换为实际类型(在本例中为int
,适用于实际类型的任何规则也适用于auto
。不同的是,如果你试图写int &&blah = i
,你会得到一个错误,因为编译器会识别出你试图从 l 值引用创建 r 值引用,这是不可能的(因为你只能从另一个 r 值引用创建 r 值引用)。
前面例子如此重要的原因是auto
不会产生编译器的抱怨。相反,当你打算创造一个 r 值时,它会产生一个 l 值,这可能导致效率低下或错误。了解auto
的用法最重要的是,如果有名字,就是 l 值;否则,它是一个 r 值。
例如,考虑以下代码:
auto &&a10 = 42;
show_type(a10);
此代码导致以下结果:
a10 = int&&
由于数值42
没有变量名,所以它是一个常数,因此,通用引用成为对整数的 r 值引用。
还需要注意的是,auto
的使用确实会在处理引用时混淆地继承 CV 限定符。看看这个例子:
int main(void)
{
const int i = 42;
auto i1 = i;
auto &i2 = i;
auto &&i3 = std::move(i);
show_type(i1);
show_type(i2);
show_type(i3);
}
这将导致以下结果:
如前面的截图所示,第一个整数仍然是int
类型,因为const int
的副本是int
。然而i2
和i3
都成为const int
的参考。如果我们将auto
替换为int
,我们会得到一个编译器错误,因为您不能创建对const int
的非const
引用,但是使用auto
会很乐意为您将您的非const
变量转换为const
变量。这样做的问题是,当你试图修改你的变量时,你会得到奇怪的错误消息,抱怨变量是只读的,而事实上,你没有明确地将变量定义为const
。一般来说,如果您期望const
,最好总是将一个用auto
定义的变量标记为const
,否则标记为非const
,以防止这些有时难以识别的错误。
在本食谱中,我们将学习类型推导如何与decltype()
和decltype(auto)
配合使用,以及如何使用decltype(auto)
来避免auto
的参照性问题。
这个配方很重要,因为auto
在处理decltype()
处理的引用时有一些奇怪的行为,这为 C++ 提供了一种更可预测地处理类型推断的方法,尤其是在使用 C++ 模板时。
开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容:
> sudo apt-get install build-essential git cmake
这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。
执行以下步骤来尝试此食谱:
- 从新的终端,运行以下命令下载源代码:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter12
- 要编译源代码,请运行以下命令:
> cmake .
> make recipe02_examples
- 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例:
> ./recipe02_example01
i = int
> ./recipe02_example02
i = short int
> ./recipe02_example03
i = short int
> ./recipe02_example04
i1 = int
i2 = int
> ./recipe02_example05
i1 = int
i2 = const int
i3 = volatile int
i4 = const volatile int
> ./recipe02_example06
i1 = int
i2 = int&
i3 = int&&
a1 = int
a2 = int
a3 = int
a4 = int
a5 = int&
a6 = int&&
d1 = int
d2 = int&
d3 = int&&
在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。
C++ 中的auto
和typename
都没有提供获取变量类型和使用该信息创建新类型的能力。为了更好地解释为什么您可能想要这样做,让我们看下面的例子:
template<typename FUNC>
auto question(FUNC &&func)
{
auto x = func() + 10;
return x;
}
我们从一个函数开始我们的例子,这个函数将任何函数作为输入,并返回这个函数加上10
的结果。然后,我们可以如下执行该函数:
short the_answer()
{
return 32;
}
int main(void)
{
auto i = question(the_answer);
show_type(i);
}
如前例所示,我们向question()
函数传递一个指向另一个返回short
的函数的指针。在执行该函数时,我们存储结果,然后使用名为show_type()
的函数,该函数被设计为输出所提供的类型是什么类型。这将导致以下结果:
这个例子的问题在于,返回的类型与我们得到的类型不同。C++ 允许根据需要增加任何变量的大小,并且通常在使用 short 时会这样做,尤其是当您试图用数值对 short 执行算术运算时,因为数值是以整数表示的。
由于我们不知道所提供的函数在question()
函数中的返回类型是什么,所以没有办法解决这个问题。进入decltype()
。为了解释,让我们更新我们的示例来解决前面的问题:
template<typename FUNC>
auto question(FUNC &&func)
{
decltype(func()) x = func() + 10;
return x;
}
如前例所示,我们将auto
替换为decltype(func())
。这告诉编译器获取func()
的返回类型,并使用该类型定义x
。因此,编译器将该模板转换为以下函数:
short question(short(*func)())
{
short x = func() + 10;
return x;
}
发生这种情况,而不是最初预期的以下情况:
int question(short(*func)())
{
int x = func() + 10;
return x;
}
执行时会产生以下输出:
如前一张截图所示,我们现在从question()
函数中返回了正确的类型。使用 C++ 14,我们可以将这个例子更进一步,这样写:
template<typename FUNC>
constexpr auto question(FUNC &&func) -> decltype(func())
{
return func() + 10;
}
在前面代码片段的示例中,我们将question()
函数转换为constexpr
,这允许编译器优化函数调用,用func() + 10
语句替换对question()
的调用。通过使用-> decltype()
函数返回语法明确告诉编译器我们希望函数返回什么类型,我们也消除了对基于堆栈的变量的需求。应该注意的是,这种语法是必需的,因为下面的语法不会编译:
template<typename FUNC>
constexpr decltype(func()) question(FUNC &&func)
{
return func() + 10;
}
前面的代码不会编译,因为编译器还没有func()
的定义,因此不知道它的类型是什么。->
语法通过将返回类型放在函数定义的末尾而不是前面来解决这个问题。
decltype()
说明符也可以用来代替auto
,如下所示:
int main(void)
{
decltype(auto) i1 = 42;
decltype(auto) i2{42};
show_type(i1);
show_type(i2);
}
这将产生以下输出:
在这个例子中,我们使用decltype(auto)
创建两个整数,并将它们初始化为42
。在这种特殊情况下,decltype(auto)
和auto
的操作完全相同。两者都将占位符类型定义为整数,因为两者都使用数值进行初始化,默认情况下,该数值为int
。
和auto
一样,可以用 CV 限定词(即const
/ volatile
)修饰decltype(auto)
如下:
int main(void)
{
decltype(auto) i1 = 42;
const decltype(auto) i2 = 42;
volatile decltype(auto) i3 = 42;
const volatile decltype(auto) i4 = 42;
show_type(i1);
show_type(i2);
show_type(i3);
show_type(i4);
}
这将产生以下输出:
decltype(auto)
的真正魔力在于它如何处理引用。为了演示这一点,让我们从下面的例子开始:
int main(void)
{
int i = 42;
int i1 = i;
int &i2 = i;
int &&i3 = std::move(i);
show_type(i1);
show_type(i2);
show_type(i3);
}
执行时,我们会看到以下输出:
i1 = int
i2 = int&
i3 = int&&
如前面的例子所示,我们已经创建了一个整数、一个对整数的 l 值引用和一个对整数的 r 值引用。让我们看看如果我们尝试使用auto
而不是int
会发生什么,如下所示:
auto a1 = i1;
auto a2 = i2;
auto a3 = std::move(i3);
show_type(a1);
show_type(a2);
show_type(a3);
然后,我们会看到以下输出:
a1 = int
a2 = int
a3 = int
如前面的例子所示,我们只得到整数。所有引用都已删除。用auto
获取引用的唯一方法是我们明确定义它们如下:
auto a4 = i1;
auto &a5 = i2;
auto &&a6 = std::move(i3);
show_type(a4);
show_type(a5);
show_type(a6);
这将产生以下预期输出:
a4 = int
a5 = int&
a6 = int&&
必须添加额外的&
运算符来显式定义引用类型的问题是,这假设在我们的模板代码中,我们实际上知道引用应该是什么。如果这些信息不可用,我们将无法编写模板函数,也无法知道是否可以创建 l 值或 r 值引用,很可能会产生一个副本。
为了克服这一点,decltype(auto)
不仅在初始化时继承了类型和 CV 限定符,还如下继承了引用:
decltype(auto) d1 = i1;
decltype(auto) d2 = i2;
decltype(auto) d3 = std::move(i3);
show_type(d1);
show_type(d2);
show_type(d3);
前面的代码在执行时会产生以下结果:
d1 = int
d2 = int&
d3 = int&&
如前例所示,decltype(auto)
可用于继承其正在初始化的值的所有类型信息,包括引用性。
在这个食谱中,我们将学习模板函数类型推导是如何工作的。具体来说,这个食谱将教你模板函数类型推导如何与auto
类型推导一样工作,以及函数类型推导如何与一些奇数类型(例如 C 风格数组)一起使用。
这个方法很重要,因为它将教你如何正确地编写函数模板,消除了调用函数模板时显式定义类型信息的需要。
开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容:
> sudo apt-get install build-essential git cmake
这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。
执行以下步骤来尝试此食谱:
- 从新的终端,运行以下命令下载源代码:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter12
- 要编译源代码,请运行以下命令:
> cmake .
> make recipe03_examples
- 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例:
> ./recipe03_example01
t = int
t = int
> ./recipe03_example02
t = const int&
> ./recipe03_example03
t = int&
> ./recipe03_example04
t = int&
> ./recipe03_example05
t = int&&
> ./recipe03_example06
t = int&&
> ./recipe03_example07
t = const int&
> ./recipe03_example08
t = const int&&
> ./recipe03_example09
t = int (&&)[6]
在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。
在 C++ 11 中,标准委员会增加了根据传递给函数的参数自动推断模板函数类型信息的能力。
看看这个例子:
template<typename T>
void foo(T t)
{
show_type(t);
}
前面的函数创建了一个标准的模板函数,该函数执行一个名为show_type()
的函数,该函数旨在输出提供给它的类型信息。
在 C++ 11 之前,我们将如下使用这个函数:
int main(void)
{
int i = 42;
foo<int>(i);
foo<int>(42);
}
编译器已经知道模板应该将T
类型定义为整数,因为这就是函数的用途。C++ 11 消除了这种冗余,允许以下情况:
int main(void)
{
int i = 42;
foo(i);
foo(42);
}
执行时会产生以下输出:
然而,像auto
一样,当使用 r 值引用时,这种类型的推导变得有趣起来,如下所示:
template<typename T>
void foo(T &&t)
{
show_type(t);
}
前面的例子将t
定义为转发引用(也称为通用引用)。通用引用采用它所传递的任何引用类型。例如,我们这样调用这个函数:
int main(void)
{
int i = 42;
foo(i);
}
我们得到以下输出:
前面的输出显示模板函数被赋予了一个整数的 l 值引用。这是因为i
,在我们的主函数中,是一个 l 值,即使这个函数看起来是在请求一个 r 值引用。要获得 r 值参考,我们必须提供一个 r 值,如下所示:
int main(void)
{
int i = 42;
foo(std::move(i));
}
执行时会产生以下输出:
如前面的截图所示,现在我们已经给了通用引用一个 r 值,我们得到了一个 r 值。应该注意的是,普遍参考文献只有以下签名:
template<typename T>
void foo(T &&t)
例如,以下内容不是通用参考:
template<typename T>
void foo(const T &&t)
以下也不是普遍参考:
void foo(int &&t)
前面两个例子都是 r 值引用,因此需要提供一个 r 值(换句话说,这两个函数都定义了移动操作)。通用参考将接受 l 值和 r 值参考。虽然这看起来是一个优点,但它也有缺点,那就是有时很难知道您的模板函数是收到了 l 值还是 r 值。目前,确保模板函数充当 r 值引用而不是通用引用的最佳方法是使用 SFINAE:
std::is_rvalue_reference_v<decltype(t)>
最后,还可以对不太常见的类型(如 C 风格的数组)执行类型推导,如本例所示:
template<typename T, size_t N>
void foo(T (&&t)[N])
{
show_type(t);
}
前面的函数声明我们希望将类型为T
和大小为N
的 C 风格数组传递给函数,然后在执行时输出其类型。我们可以如下使用这个函数:
int main(void)
{
foo({4, 8, 15, 16, 23, 42});
}
这将自动推导为类型为int
且大小为6
的 C 型数组的 r 值引用。如本食谱所示,C++ 提供了几种机制,允许编译器确定模板函数中使用了哪些类型。
在这个食谱中,我们将学习如何在 C++ 17 中使用类模板进行类类型推导。这个方法很重要,因为 C++ 17 增加了从构造函数推导模板类类型的能力,这减少了代码的冗长和冗余。
从这个方法中获得的知识将为您提供编写 C++ 类的能力,这些类可以从类构造函数中正确地推导出它们的类型,而不需要显式的类型声明。
开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容:
> sudo apt-get install build-essential git cmake
这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。
执行以下步骤来尝试此食谱:
- 从新的终端,运行以下命令下载源代码:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter12
- 要编译源代码,请运行以下命令:
> cmake .
> make recipe04_examples
- 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例:
> ./recipe04_example01
t = int
t = int
> ./recipe04_example02
t = int&
> ./recipe04_example03
t = int&&
t = int&&
> ./recipe04_example04
t = int&&
u = int&
> ./recipe04_example05
t = int&&
> ./recipe04_example06
t = const char (&)[16]
u = int&&
在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。
类模板类型推断是 C++ 17 中添加的一个新特性,它提供了从模板类的构造函数推断模板类类型的能力。假设我们有以下类模板:
template<typename T>
class the_answer
{
public:
the_answer(T t)
{
show_type(t);
}
};
如前面的代码片段所示,我们有一个简单的类模板,它在构造过程中采用类型T
,并使用show_type()
函数输出给定的任何类型。在 C++ 17 之前,这个类应该已经使用以下内容进行了实例化:
int main(void)
{
the_answer<int> is(42);
}
使用 C++ 17,我们现在可以如下实例化这个类:
int main(void)
{
the_answer is(42);
}
这样做的原因是类的构造函数以一个类型T
作为参数。由于我们提供了一个数字整数作为参数,类的类型T
被推导为一个整数。这种类型的推导也包括对引用的支持。看看这个例子:
template<typename T>
class the_answer
{
public:
the_answer(T &t)
{
show_type(t);
}
};
在前面的例子中,我们的类将T&
作为类的构造函数中的参数,这允许我们如下实例化该类:
int main(void)
{
int i = 42;
the_answer is(i);
}
执行时会产生以下结果:
如前例所示,该类的类型T
被推导为对整数的 l 值引用。大多数适用于函数模板的类型推导规则也适用于类模板,但也有一些例外。例如,类模板构造函数不支持转发引用(通用引用)。考虑以下代码:
template<typename T>
class the_answer
{
public:
the_answer(T &&t)
{
show_type(t);
}
};
前面的构造函数不是通用引用;这是一个 r 值参考,意味着我们不能执行以下操作:
the_answer is(i);
这是不可能的,因为它试图将 l 值绑定到 r 值,这是不允许的。相反,像任何其他 r 值引用一样,我们必须使用以下内容实例化该类:
the_answer is(std::move(i));
或者我们可以用以下内容绑定它:
the_answer is(42);
类模板类型推导不支持通用引用的原因是,类模板类型推导使用构造函数推导类型,然后根据推导出的类型为类的其余部分填充类型,这意味着在编译构造函数时,它看起来像这样:
class the_answer
{
public:
the_answer(int &&t)
{
show_type(t);
}
};
这定义了一个 r 值参考。
要在构造函数或任何其他函数中获得通用引用,必须使用成员函数模板,该模板本身仍然支持类型推导,但不用于推导类的任何类型。看看这个例子:
template<typename T>
class the_answer
{
public:
template<typename U>
the_answer(T &&t, U &&u)
{
show_type(t);
show_type(u);
}
};
在前面的例子中,我们创建了一个类型为T
的类模板,我们将构造函数定义为成员函数模板。建造师本身取T &&t``U &&u
。然而,在这种情况下,t
是一个 r 值参考,u
是一个通用参考,尽管它们看起来相同。两者都可以由 C++ 17 编译器推导出来,如下所示:
int main(void)
{
int i = 42;
the_answer is(std::move(i), i);
}
还应该注意的是,构造函数不一定要有任何特定顺序的类型才能进行推导。唯一的要求是所有类型都出现在构造函数的参数中。例如,考虑以下代码:
template<typename T>
class the_answer
{
public:
the_answer(size_t size, T &&t)
{
show_type(t);
}
};
前面的示例可以实例化如下:
int main(void)
{
the_answer is_2(42, 42);
}
最后,类型推导也支持多个模板类型,如本例所示:
template<typename T, typename U>
class the_answer
{
public:
the_answer(const T &t, U &&u)
{
show_type(t);
show_type(u);
}
};
前面的示例创建了一个具有两种泛型类型的类模板。这个类的构造函数创建了一个对类型T
的 l 值引用,同时也获取了一个对类型U
的 r 值引用。这个类可以实例化如下:
int main(void)
{
the_answer is("The answer is: ", 42);
}
这将产生以下输出:
如前例所示,T
和U
都推导成功。
在本食谱中,我们将学习如何使用用户定义的推导指南来帮助编译器进行类模板类型的推导。大多数情况下,不需要用户定义的推导指南,但是在某些情况下,它们可能是为了确保编译器推导出正确的类型。这个方法很重要,因为没有用户定义的类型推导,某些类型的模板方案是不可能的,如下所示。
开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容:
> sudo apt-get install build-essential git cmake
这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。
执行以下步骤来尝试此食谱:
- 从新的终端,运行以下命令下载源代码:
> cd ~/
> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git
> cd Advanced-CPP-CookBook/chapter12
- 要编译源代码,请运行以下命令:
> cmake .
> make recipe05_examples
- 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例:
> ./recipe05_example01
t = unsigned int
t = int
> ./recipe05_example02
t = unsigned int
> ./recipe05_example03
t = std::__cxx11::basic_string<char>
在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。
类模板类型推断是 C++ 17 中非常需要的特性,因为它有助于减少 C++ 的冗余和冗长。然而,在某些情况下,编译器会推导出错误的类型——如果我们不依赖类型推导,这个问题是可以解决的。为了更好地理解这种类型的问题,让我们看下面的例子:
template<typename T>
class the_answer
{
public:
the_answer(T t)
{
show_type(t);
}
};
在前面的例子中,我们创建了一个简单的类模板,它的构造函数采用类型T
,并使用show_type()
函数输出给定的任何类型。现在假设我们希望使用这个类来实例化一个采用无符号整数的版本。有两种方法可以做到这一点:
the_answer<unsigned> is(42);
前面的方法是最明显的,因为我们明确地告诉编译器我们想要什么类型,而根本不使用类型推导。获取无符号整数的另一种方法是使用正确的数字文字语法,如下所示:
the_answer is(42U);
在前面的例子中,我们利用了类型推导,但是我们必须确保总是将U
添加到整数中。这种方法的优点是代码是显式的。这种方法的缺点是,如果我们忘记添加U
来声明我们希望有一个无符号整数,我们可能会无意中创建一个具有int
类型而不是unsigned
类型的类。
为了防止这个问题,我们可以利用用户定义的类型推断来告诉编译器,如果它看到整数类型,我们实际上是指无符号类型,如下所示:
the_answer(int) -> the_answer<unsigned>;
前面的语句告诉编译器,如果它看到一个具有int
类型的构造函数,int
应该生成一个具有unsigned
类型的类。
The left-hand side takes a constructor signature, while the right-hand side takes a class template signature.
使用此方法,我们可以获取我们看到的任何构造函数签名,并将其转换为我们希望的类模板类型,如本例所示:
the_answer(const char *) -> the_answer<std::string>;
用户定义的类型推导指南告诉编译器,如果看到 C 风格的字符串,应该改为创建std::string
。然后,我们可以用以下内容运行我们的示例:
int main(void)
{
the_answer is("The answer is: 42");
}
然后,我们得到以下输出:
如前面的截图所示,这个类是用std::string
(或者至少是 GCC 对std::string
的内部表示)构造的,而不是 C 风格的字符串。