了解模板的声明、定义和专用化

Understanding the declaration, definition and specialization of templates

本文关键字:定义 专用 声明 了解      更新时间:2023-10-16

>我试图理解下面的示例,但我对三个不同的模板和结构声明有点困惑。

您能否描述一下以下电话会发生什么? 将使用哪些模板以及何时使用?

另外,为什么第一个模板+类声明在结构声明之后缺少"<S...>"?(查看注释掉的内容)?什么时候添加它是正确的,什么时候不是?

#include <iostream>
#include <stdio.h>
using namespace std;

template<typename... S>
struct Example /* <S...> */ ; 
template<typename H, typename... T>
struct Example<H, T...>
{
static const size_t value = sizeof(H) + Example<T...>::value;
};
template<>
struct Example<>
{
static const size_t value = 0;
};

int main(){
cout << Example<long, int, char>::value << endl;
return 0;
}

输出:13

第一个声明名为Examplestruct的模板,接受任意数量的类型:

template<typename... S>
struct Example /* <S...> */ ;

如果新声明的模板的名称后跟<>,带或不带参数,那将是一种专业化!

第二个定义了至少一个类型参数的部分专用化:

template<typename H, typename... T>
struct Example<H, T...>
{
static const size_t value = sizeof(H) + Example<T...>::value;
};

最后一个定义了没有类型参数的完全专用化:

template<>
struct Example<>
{
static const size_t value = 0;
};

请注意,template后跟空<>括号。

在完全专用化之前定义部分专用化并不重要,因为实例化必须推迟到模板类型参数已知。

您使用的特定实例Example<long,int,char>::value取决于Example<int, char>::value,这取决于Example<char>,这会导致基本情况:

Example<long, int, char>::value = sizeof(long) + Example<int, char>::value; // sizeof(long) + sizeof(int) + 1 + 0
Example<int, char>::value = sizeof(int) + Example<char>::value; // sizeof(int) + 1 + 0
Example<char>::value = sizeof(char) + Example<>::value; // 1 + 0
Example<>::value = 0;

当然,这个例子可以简化:

template <class... T>
struct Example {
static const size_t value = 0;
static_assert(!sizeof...(T), "The base-template only handles no template arguments.");
};
template <class H, class... T>
struct Example {
static const size_t value = sizeof(H) + Example<T...>::example;
};

或者用C++17个折叠表情:

template <class... T>
struct Example {
static const size_t value = 0 + ... + sizeof(T);
};

顺便说一句,有充分的理由永远不要使用using namespace std;,我想知道你为什么#include <stdio.h>return 0;main()来说是多余的。

只回答你问题的这一部分:

另外,为什么在结构声明之后缺少第一个模板+类声明< S...>?看看注释掉了什么)?什么时候添加它是正确的,什么时候不是?

  • 当您对模板化函数/类/结构/类型进行(常规)声明时,只需在声明之前使用尖括号< >一次:

    template <typename T> 
    void foo(T x);
    
  • 声明常规模板的特定实例化时,可以使用两次< >,一次在声明之前为空,然后再次使用要为其实例化的特定模板参数:

    template <>
    void foo<int>(int& x);
    
  • 声明常规模板的特定专用化时,< >一次,其中包含要实例化的特定模板参数:

    template 
    void foo<int>(int& x);
    

有关最后两项的更多信息(以及它们的不同之处):

C++ 模板中实例化和专用化的区别

还有为什么第一个模板+类声明缺少">"紧接在结构声明之后?(查看注释掉的内容)?什么时候添加它是正确的,什么时候不是?

在我看来,最好从这一点开始。

首先,以下内容(删除了注释<S...>)是接收类型模板参数的可变参数列表的模板结构Example的声明(注意:仅声明,而不是定义)

template<typename... S>
struct Example; 

您也可以避免使用S并简单地编写

template <typename...>
struct Example; 

因为在此上下文中不使用可变参数列表的名称。

此时,编译器知道有一个可变参数模板结构Example但不知道如何制作。

接下来,我们添加接收一个或多个模板参数的Example专用化的定义(请注意,Example被定义为接收零个或多个参数,因此接收一个或多个参数的专用化是Example的特殊情况)

//....... one --> V          VVVVV <- or more template parameter
template<typename H, typename... T>
struct Example<H, T...>
{ // .........^^^^^^^^^  <- this is a specialization
static const size_t value = sizeof(H) + Example<T...>::value;
};

Example之后的<H, T...>部分标识了专业化(如前所述)。

此专用化定义了一个static const size_t变量,该变量使用sizeof(H)(第一个类型模板参数的sizeof())与另一个Example类中定义的value之和进行初始化:Example<T...>

因此,您正在观察递归定义:value 是第一个参数(类型)的sizeof()与以下类型的sizeof()之和。

建议:如果使用可变参数模板,也可以使用constexpr,因此最好将value定义为constexpr

static constexpr std::size_t value = sizeof(H) + Example<T...>::value;

或者更好的是,您可以从std::integral_constant继承

template <typename H, typename... T>
struct Example <H, T...> 
: public std::integral_constant<std::size_t, sizeof(H) + Example<T...>{}>
{ };

因此,您可以使用其他有用的工具从std::integral_constant继承value(例如:在需要std::size_t的上下文中自动转换为std::size_t)

每个递归都需要一个基本案例,所以你有

template<>
struct Example<>
{
static const size_t value = 0;
};

声明另一个Example的特化;这次的情况模板参数正好为零(Example<>)。在这种情况下,您有一个value的定义,即零来终止递归。

和以前一样,您可以将value定义为constexpr,或者更好的恕我直言,再次使用std::integral_constant

template <>
struct Example<> : public std::integral_constant<std::size_t, 0u>
{ };

现在,您已经为Example定义了两个专用化:一个用于一个或多个参数情况,一个用于零参数情况。因此,您已经涵盖了声明接收零个或多个参数的Example的所有情况;无需声明Example的通用(非专用版本)。

正如重复数据删除器所观察到的,您可以定义通用情况,并且只能定义一种专用化:如果您编写

template <typename...>
struct Example : public std::integral_constant<std::size_t, 0u>
{ };
template <typename T, typename ... Ts>
struct Example<T, Ts...>
: public std::integral_constant<std::size_t, sizeof(T)+Example<Ts...>{}>
{ };

首先声明Example接收零个或多个参数,并定义具有value零的通用情况(基本情况),然后定义一个或多个专用化。

考虑到编译器选择更专业的版本(当更多版本匹配时),编译器在存在一个或多个参数(机器人版本匹配但专用化更专业)时选择专用化,当参数为零时选择通用版本(因为专用化不匹配)。

这种方式有点合成,但可能不太清楚。

您能否描述一下以下电话会发生什么? 将使用哪些模板以及何时使用?

现在应该很容易理解。

当你写的时候

Example<long, int, char>::value

你要求Example<long, int, char>value.

三个参数,因此选择一个或多个专用化,即

value = sizeof(long) + Example<int, char>::value;

出于同样的原因,Example<int, char>中的value

value = sizeof(int) + Example<char>::value;

Example<char>value

value = sizeof(char) + Example<>::value;

现在,对于Example<>::value,选择零参数专用化,Example<>::value为零。

最后,我们有Example<long, int, char>中的value初始化为

value = sizeof(long) + sizeof(int) + sizeof(char) + 0;

您标记了 C++11,所以很遗憾您不能使用 C++17(模板折叠),您可以在其中完全避免递归并将Example定义为using

template <typename ... Ts>
using Example = std::integral_constant<std::size_t, (... + sizeof(Ts))>;