首页 > 编程语言 >C++ 模板基础知识——可变参数模板

C++ 模板基础知识——可变参数模板

时间:2024-09-07 19:52:17浏览次数:5  
标签:std ... 递归 C++ 基础知识 参数 myvtfunct 模板

目录

C++ 模板基础知识——可变参数模板

可变参数模板(Variadic Templates)是C++11引入的新特性,允许模板定义中包含任意数量的模板参数。这大大简化了以往实现类似功能所需的复杂代码。

1. 可变参函数模板

1.1 基本含义

首先看一个没有形参的普通函数:

void myptfunct() { }

再来看一个名为myvtfunct的可变参函数模板:

#include <iostream>

template <typename... T>
void myvtfunct(T... args)
{
    std::cout << "接收到的参数数量:" << sizeof...(args) << std::endl;
    std::cout << "接收到的类型数量:" << sizeof...(T) << std::endl;
}

int main()
{
    myvtfunct(); // 输出 0 0
    myvtfunct(10, 20); // 输出 2 2
    myvtfunct(10, 25.8, "abc", 68); // 输出 4 4
    // 指定部分类型,让编译器推导另一部分类型是允许的
    myvtfunct<double, double>(10, 25.8, "abc", 68, 73); // 输出 5 5

    return 0;
}

这个结果展示了每次调用myvtfunct()时,都会打印出接收到的参数数量。以下是代码中的一些注意点:

  1. 使用模板声明时,typename...位于T之前,表示参数包。
  2. void myvtfunct(T... args)这行代码中,T后面的...表示可变参类型,它代表了零到多个不同的类型。T不应被视为单一类型,而是一组类型。args通常代表一个参数包或一组参数,这些参数的类型可能各不相同。这个参数包可以包含零到多个任意类型的参数。
  3. sizeof...语法在C++11中引入,用于在可变参数函数模板或类模板内部表示接收到的模板参数个数或类型数量。它只适用于可变参数,使用时必须放在圆括号中,可以是函数模板的参数(args)或类型模板参数(T)。

在收到参数包之后,myvtfunct()必须处理这些参数的展开。通常,参数包的展开是通过递归函数调用来实现的。为此,可变参数函数模板应包含一个参数包展开函数和一个对应的递归终止函数,二者共用相同的函数名。

现在来重新设计myvtfunct(),包含一个参数包展开函数如下:

template <typename T, typename... U>
void myvtfunct(T firstarg, U... otherargs)
{
    std::cout << "接收到的参数值为:" << firstarg << std::endl;
    myvtfunct(otherargs...); // 递归调用,注意传入的是一包形参
}

配合一个递归终止函数:

void myvtfunct() // 这是一个普通函数,不是函数模板
{
    std::cout << "参数包展开时执行了递归终止函数myvtfunct()" << std::endl;
}

main()主函数中,调整代码如下:

myvtfunct(10, "abc", 12.7);

输出结果显示参数逐步被展开:

接收到的参数值为:10
接收到的参数值为:abc
接收到的参数值为:12.7
参数包展开时执行了递归终止函数myvtfunct()

这个示例说明了每次调用myvtfunct()时,otherargs中的参数数量逐渐减少,直到没有剩余参数时触发递归终止函数myvtfunct()

1.2 利用 constexpr if 优化递归函数

在C++17标准中引入的constexpr if语句为模板编程提供了更加灵活和高效的条件分支选择。与传统的if语句类似,constexpr if允许在编译期间根据条件进行分支判断,从而避免了某些不必要的函数实例化和递归操作。下面将详细说明如何利用constexpr if简化可变参数模板的实现,并进一步探讨其行为特点。

在传统的可变参数模板递归展开中,为了处理参数包的最后一个元素,通常需要定义一个递归终止函数。然而,使用constexpr if,可以在同一个模板函数中实现条件判断,避免定义额外的递归终止函数。

以下是使用constexpr if改写的myvtfunct()模板函数:

#include <iostream>

template <typename T, typename... U>
void myvtfunct(T firstarg, U... otherargs)
{
    std::cout << "接收到的参数值为:" << firstarg << std::endl;
    if constexpr (sizeof...(otherargs) > 0)
    {
        myvtfunct(otherargs...); // 递归调用,继续展开参数包
    }
}

int main()
{
    myvtfunct(10, "abc", 12.7);

    return 0;
}

在这段代码中,constexpr if语句确保只有在otherargs参数包不为空时才递归调用myvtfunct()。如果otherargs为空,编译器不会实例化进一步的递归调用,这有效地省去了定义递归终止函数的必要。

编译器的函数实例化过程

对于调用myvtfunct(10, "abc", 12.7);的情况,编译器在展开模板时会实例化以下几个函数:

void myvtfunct<int, const char*, double>(int, const char*, double);
void myvtfunct<const char*, double>(const char*, double);
void myvtfunct<double>(double);

这些函数对应于参数包逐步展开的过程。constexpr if的条件判断在编译期执行,因此无须定义显式的递归终止函数,编译器可以自动处理参数包的展开。

1.3 关于 constexpr if 的进一步理解

constexpr if的引入完善了模板与泛型编程中的条件选择机制,使得在编译期间进行更加精细的控制成为可能。这不仅简化了代码结构,还提高了代码的执行效率。然而,需要注意constexpr if的条件必须是编译期常量,同时条件分支内的代码依然会被编译。因此,合理使用constexpr if可以显著优化模板编程中的递归展开过程,但也要谨慎避免潜在的编译期错误。

  1. 代码块的编译:即使constexpr if的条件不满足,条件内的代码块仍会被编译。这意味着,如果条件分支内包含无效代码(如未定义的函数调用),编译器仍会报错。例如:

    if constexpr (sizeof...(otherargs) > 100)
    {
        testfunc();  // 如果 testfunc() 未定义,编译会报错
    }
    

    尽管sizeof...(otherargs)显然不可能大于100,编译器依然会检查testfunc()的定义。如果该函数未定义,编译会失败。

  2. 条件必须是常量constexpr if的条件必须在编译期确定。因此,不能使用在运行时才能确定的变量。例如,以下代码将会导致编译错误:

    int i = 8;
    if constexpr (i > 0)
    {
        // 编译错误:`i` 不是编译期常量
    }
    

    这里,i是一个普通变量,其值在运行时确定,因此不能用作constexpr if的条件。

1.4 重载

可变参数函数模板同样支持重载。下面的示例展示了两个可变参数函数模板和一个普通函数的重载情况:

#include <iostream>

void myvtfunct(int arg)
{
    std::cout << "myvtfunct(int arg)执行了" << std::endl;
}

template<typename ...T>
void myvtfunct(T... arg)
{
    std::cout << "myvtfunct(T... arg)执行了" << std::endl;
}

template<typename ...T>
void myvtfunct(T*... arg)
{
    std::cout << "myvtfunct(T*... arg)执行了" << std::endl;
}

int main()
{
    myvtfunct(NULL); // myvtfunct(T... arg)执行了
    myvtfunct(nullptr); // myvtfunct(T... arg)执行了
    myvtfunct((int*)nullptr); // myvtfunct(T*... arg)执行了

    return 0;
}

编译器在选择合适的重载函数时,遵循一套复杂的排序规则。这些规则不仅适用于普通函数和模板函数之间的选择,也适用于不同模板之间的选择。通常,编译器会优先选择最为匹配的函数版本。

一般情况下,当普通函数和实例化后的模板函数都适合被调用时,编译器会优先选择普通函数。例如,在上述代码中,myvtfunct(NULL)执行的是普通函数myvtfunct(int arg)。如果注释掉普通函数,编译器则会选择与模板myvtfunct(T... arg)进行匹配。

虽然这些选择规则相对复杂,但在实践中,可以通过测试和实验来理解编译器的行为。对重载规则掌握不够精准时,可以通过实际编写代码并观察结果来获得更好的理解。


2. 折叠表达式

折叠表达式(Fold Expressions)是C++17标准引入的一项特性,用于简化处理可变参数模板中的参数包展开和运算。通过折叠表达式,您可以直接对一组参数执行某种操作,而无需显式地展开和处理每个参数。这使得代码更加简洁和易读。

折叠表达式通过简化可变参数模板中的运算,使得处理一组参数更加直观和方便。折叠表达式的四种形式——一元左折、一元右折、二元左折、二元右折——分别适用于不同的运算需求。在实际应用中,可以根据具体的需求选择适当的折叠形式,以实现所需的功能。

2.1 一元左折(Unary Left Fold)

格式(... 运算符 一包参数)

计算方式((( 参数1 运算符 参数2 ) 运算符 参数3 ) … 运算符 参数N )

一元左折从第一个参数开始依次进行操作,直到最后一个参数。例如,加法运算中的一元左折会从左到右依次累加所有参数。

#include <iostream>

template <typename... T>
auto add_val(T... args) {
    return (... + args);  // 左折叠,从左向右进行加法
}

int main() {
    std::cout << add_val(10, 20, 30) << std::endl;  // 输出 60
    return 0;
}

在这个例子中,add_val函数使用一元左折来计算所有参数的和。计算顺序为:((10 + 20) + 30),结果是60

2.2 一元右折(Unary Right Fold)

格式(一包参数 运算符 ...)

计算方式( 参数1 运算符 ( … ( 参数N-1 运算符 参数N )))

一元右折从最后一个参数开始依次进行操作,直到第一个参数。例如,减法运算中的一元右折会从右到左依次减去所有参数。

#include <iostream>

template <typename... T>
auto sub_val_left(T... args) {
    return (... - args);  // 左折叠,从左向右进行减法
}

template <typename... T>
auto sub_val_right(T... args) {
    return (args - ...);  // 右折叠,从右向左进行减法
}

int main() {
    std::cout << sub_val_left(10, 20, 30, 40) << std::endl;  // 输出 -80
    std::cout << sub_val_right(10, 20, 30, 40) << std::endl;  // 输出 -20
    return 0;
}

在这个例子中,sub_val_leftsub_val_right分别使用了一元左折和一元右折来计算参数的差:

  • sub_val_left(10, 20, 30, 40) 的计算顺序为:((10 - 20) - 30) - 40 = -80
  • sub_val_right(10, 20, 30, 40) 的计算顺序为:10 - (20 - (30 - 40)) = -20

2.3 二元左折(Binary Left Fold)

格式(init 运算符 ... 运算符 一包参数)

计算方式((( init 运算符 参数1 ) 运算符 参数2 ) … 运算符 参数N )

二元左折从左向右进行操作,但首先将一个初始值(init)与第一个参数进行运算,然后依次与后续参数进行运算。

#include <iostream>

template <typename... T>
auto sub_val_left_b(T... args) {
    return (220 - ... - args);  // 左折叠,从左向右进行减法,初始值为220
}

int main() {
    std::cout << sub_val_left_b(10, 20, 30, 40) << std::endl;  // 输出 120
    return 0;
}

在这个例子中,sub_val_left_b使用二元左折来计算从220开始减去所有参数的结果。计算顺序为:(((220 - 10) - 20) - 30) - 40 = 120

2.4 二元右折(Binary Right Fold)

格式(一包参数 运算符 ... 运算符 init)

计算方式( 参数1 运算符 (… ( 参数N 运算符 init )))

二元右折从右向左进行操作,首先将最后一个参数与初始值(init)进行运算,然后依次与前面的参数进行运算。

#include <iostream>

template <typename... T>
auto sub_val_right_b(T... args) {
    return (args - ... - 220);  // 右折叠,从右向左进行减法,初始值为220
}

int main() {
    std::cout << sub_val_right_b(10, 20, 30, 40) << std::endl;  // 输出 200
    return 0;
}

在这个例子中,sub_val_right_b使用二元右折来计算从最后一个参数开始减去所有前面的参数,并最终减去220的结果。计算顺序为:10 - (20 - (30 - (40 - 220))) = 200


3. 可变参表达式

在C++17中,引入了折叠表达式这一特性,极大地简化了对可变参数模板中参数包的处理。除了折叠表达式外,还可以通过可变参表达式对参数包中的每个参数进行运算,从而实现更加灵活的操作。可变参表达式允许对参数包中的各个参数进行独立运算,然后将结果传递给其他函数或用于进一步的计算。

可变参表达式允许对一包参数进行统一运算,并将结果用于进一步的处理。这些表达式提供了处理可变参数的灵活性,使代码更加简洁、可读。在实际应用中,灵活使用不同形式的可变参表达式,可以有效提高代码的表达能力和可维护性。

3.1 可变参表达式的基本范例

首先,展示一个基本范例,用于创建一个函数模板,该模板打印一组参数并返回它们的和:

#include <iostream>

template<typename... T>
auto print_result(T const& ...args)
{
    (std::cout << ... << args) << " 结束" << std::endl;  // 打印参数
    return (... + args);  // 计算并返回参数的和
}

int main()
{
    std::cout << print_result(10, 20, 30, 40) << std::endl;  // 输出 100
    return 0;
}

运行结果:

10203040 结束
100

在此例中,print_result函数模板首先利用折叠表达式打印所有参数,然后计算并返回它们的和。

3.2 可变参表达式中的运算

在某些场景下,可能需要对每个参数进行运算后再求和。例如,希望将每个参数扩大到原来的2倍后再求和。为了实现此需求,可以引入一个中间函数模板,如下所示:

#include <iostream>

template<typename... T>
auto print_result(T const& ...args)
{
    (std::cout << ... << args) << " 结束" << std::endl;
    return (... + args);  // 计算并返回参数的和
}

template<typename... T>
void print_calc(T const& ...args)
{
    std::cout << print_result(2 * args...) << std::endl;  // 对每个参数进行2倍运算后传递
}

int main()
{
    print_calc(10, 20, 30, 40);  // 输出 200
    return 0;
}

运行结果:

20406080 结束
200

在上述代码中,print_calc函数对每个参数进行2倍运算,然后将结果传递给print_result函数。

3.3 不同的可变参表达式写法

可变参表达式的写法有多种形式,以下代码展示了几种可能的写法及其效果:

#include <iostream>

template<typename... T>
auto print_result(T const& ...args)
{
    (std::cout << ... << args) << " 结束" << std::endl;
    return (... + args);
}

template<typename... T>
void print_calc(T const& ...args)
{
    // std::cout << print_result(2 * args) << std::endl;     // 语法错误,编译不通过
    // std::cout << print_result(args... * 2) << std::endl;  // 语法错误,编译不通过
    // std::cout << print_result(args * 2...) << std::endl;  // 语法错误,编译不通过, ...不可以直接跟在一个数字之后
    std::cout << print_result(args * 2 ...) << std::endl;     // 成功,数字与...之间应用空格分割
    std::cout << print_result((2 * args) ...) << std::endl;   // 成功,括号括起整个表达式
    std::cout << print_result(args + args...) << std::endl;   // 成功,每个参数与自身相加
}

int main()
{
    print_calc(10, 20, 30, 40);
    return 0;
}

解释:

  1. 2 * args... - 此写法无法通过编译,因为...不能直接跟在运算符后面。应在2 * args外加括号或在运算符与...之间加空格。

  2. args * 2... - 同样无法通过编译,原因与上述相同。

  3. args * 2 ... - 此写法是合法的,但容易造成可读性问题,建议谨慎使用。

  4. (2 * args)... - 通过使用括号明确表达式的优先级和范围,此写法是清晰且可读的。

  5. args + args... - 此写法将每个参数与自身相加,例如(10 + 10), (20 + 20), (30 + 30), (40 + 40),然后将结果传递给print_result


4. 可变参类模板

可变参类模板允许在模板定义中包含任意数量的模板参数,从而提供了极高的灵活性和扩展性。与可变参函数模板相比,可变参类模板的参数包展开方式更为多样且复杂,对理解和实践的要求也更高。以下介绍一些典型的参数包展开方式。

4.1 通过递归继承方式展开类型、非类型、模板模板参数包

类型参数包的展开

这个例子展示了如何通过递归继承展开类型参数包,每个实例化处理一个类型的值,继承链中的每一层都负责一个类型。

#include <iostream>

// 基本模板定义,不包含任何成员。
template<typename... Args>
class myclass {};

// 递归模板特化,用于处理至少一个类型参数。
template<typename First, typename... Others>
class myclass<First, Others...> : public myclass<Others...> 
{
    First value;  // 存储当前类型的值
public:
    // 构造函数,初始化当前类型的值并递归初始化基类。
    myclass(First f, Others... others) : myclass<Others...>(others...), value(f) {}

    // 打印函数,打印当前值并调用基类的print方法。
    void print() 
    {
        std::cout << value << " ";
        myclass<Others...>::print();
    }
};

// 模板特化,用作递归终止条件。
template<>
class myclass<>
{
public:
    void print() {}  // 空的打印函数。
};

int main()
{
    myclass<int, double, char> obj(1, 3.14, 'a');
    obj.print();  // 输出: 1 3.14 a
    return 0;
}

在这个示例中,myclass模板通过递归继承的方式展开类型参数包。每个实例化处理一个类型的值,并将剩余的类型参数传递给基类。特别是,基类构造函数的调用(myclass<Others...>(others...))和成员初始化列表中的值初始化(value(f)),使得每个类型的值都能被正确地初始化和存储。此外,print方法展示了如何递归地访问和打印存储在继承链中的每个值。

非类型参数包的展开

非类型模板参数包可以用类似的方式展开,但适用于非类型参数:

#include <iostream>

// 基本模板定义,不包含任何成员。
template<int... Nums>
class myclassn {};

// 递归模板特化,处理至少一个非类型参数。
template<int First, int... Others>
class myclassn<First, Others...> : public myclassn<Others...> 
{
public:
    myclassn()
    {
        std::cout << "Value: " << First << std::endl;  // 打印当前非类型参数的值。
    }
};

// 模板特化,用作递归终止条件。
template<>
class myclassn<> {};

int main() 
{
    myclassn<10, 20, 30> obj;  // 输出: Value: 10 Value: 20 Value: 30
    return 0;
}

在这个例子中,每个模板实例化代表一个整数值。这里,递归继承同样被用于处理每个整数,并在构造函数中打印出来。由于非类型参数包直接关联到具体的值(在这个案例中为整数),构造函数中可以直接访问这些值并执行打印操作。

模板模板参数包的展开

模板模板参数包涉及将模板作为参数传递给另一个模板。它可以通过递归继承的方式来展开这些模板参数:

#include <iostream>
#include <vector>
#include <list>

// 基本模板定义,不包含任何成员。
template<template<typename> class... Containers>
class NestedContainer
{
public:
    NestedContainer()
    {
        std::cout << "NestedContainer created\n";  // 创建消息。
    }
};

// 递归模板特化,处理至少一个模板模板参数。
template<template<typename> class First, template<typename> class... Others>
class NestedContainer<First, Others...> : public NestedContainer<Others...>
{
public:
    NestedContainer()
    {
        std::cout << "Container of type " << typeid(First<int>).name() << " created\n";  // 打印容器类型。
    }
};

// 模板特化,用作递归终止条件。
template<>
class NestedContainer<> {};

int main()
{
    NestedContainer<std::vector, std::list> obj;
    return 0;
}

在这个例子中,这种参数允许将模板作为参数传递给另一个模板。在这里,NestedContainer模板通过递归继承创建了一系列容器类型的实例。每个容器的类型通过模板模板参数指定,构造函数中通过typeid(First<int>).name()打印出每个容器的类型信息,展示了如何在运行时检查和使用这些类型信息。

4.2 通过递归组合方式展开参数包

递归组合是处理可变参数模板的另一种有效方法,它与递归继承相比,侧重于组合而不是继承。在递归组合中,每个参数都被视为一个独立的子对象,这些子对象被逐一组合到一个结构体或类中。下面的示例展示了如何使用递归组合来实现一个容器,该容器能够存储和访问不同类型的数据:

#include <iostream>

// 声明模板结构体 Container,此处仅作声明,具体实现在后面定义
template<typename... Args>
struct Container;

// 特化版本:当没有更多类型时,定义一个空的容器
template<>
struct Container<> {};

// 主模板:处理至少一个类型的情况
template<typename Head, typename... Tail>
struct Container<Head, Tail...>
{
    Head head;                       // 当前元素
    Container<Tail...> tail;         // 递归定义剩余元素的容器

    // 构造函数:初始化当前元素和递归构造剩余元素
    Container(Head h, Tail... t) : head(h), tail(t...) {}

    // 获取当前元素的值
    Head getHead() const
    {
        return head;
    }
};

int main()
{
    // 创建Container对象,存储不同类型的数据
    Container<int, double, char> myContainer(42, 3.14, 'a');

    // 输出头部元素,即第一个元素
    std::cout << "Head: " << myContainer.getHead() << std::endl;

    // 如果需要访问下一个元素,可以继续使用tail成员,如:
    // std::cout << "Next Head: " << myContainer.tail.getHead() << std::endl;

    return 0;
}
  • Container 结构体:这是一个模板结构体,用于递归地组合不同类型的数据。
  • 空特化 Container<>:这是递归的基本情形,当没有更多的类型时,提供一个空的容器实现。
  • 主模板 Container<Head, Tail...>:这个模板处理至少一个类型的情况。它包含两个主要部分:head 成员存储当前类型的数据,而 tail 是一个递归定义的 Container,用于处理剩余的类型。
  • 构造函数:使用初始化列表语法来同时初始化 headtail。这允许在创建 Container 实例时传入所有需要的数据。
  • getHead 方法:提供对 head 成员的访问,返回存储在 head 中的数据。

递归组合方法是处理参数包的一个非常强大的技术,它允许以类型安全的方式存储和操作不同类型的数据集合。这种技术在实际编程中尤其有用,特别是在需要灵活处理多种数据类型的场景中。

4.3 通过元组和递归调用展开参数包

利用标准库中的std::tuple,可以方便地存储和处理参数包。这在C++中是一种常见的技术,特别是在需要将不同类型的多个参数作为单个实体处理时。下面是如何使用std::tuple来封装和访问参数包的示例:

#include <tuple>
#include <iostream>

// 定义一个模板类 TupleWrapper,内部使用 std::tuple 来存储参数包
template<typename... Args>
class TupleWrapper
{
    // data 成员变量是一个元组,存储传入的所有参数
    std::tuple<Args...> data;

public:
    // 构造函数,使用 std::make_tuple 将所有参数打包成一个元组
    TupleWrapper(Args... args) : data(std::make_tuple(args...)) {}

    // get 成员函数模板,用于获取元组中指定位置的值
    template<std::size_t N>
    decltype(auto) get() const
    {
        // 使用 std::get 来访问元组中的特定元素
        return std::get<N>(data);
    }
};

int main()
{
    // 创建 TupleWrapper 对象,初始化时传入三个不同类型的参数
    TupleWrapper<int, double, char> wrapper(42, 3.14, 'a');

    // 访问并打印元组中的每个元素
    std::cout << "Int: " << wrapper.get<0>() << "\n";      // 输出整数部分
    std::cout << "Double: " << wrapper.get<1>() << "\n";   // 输出浮点数部分
    std::cout << "Char: " << wrapper.get<2>() << "\n";     // 输出字符部分

    return 0;
}

解释和细节:

  • TupleWrapper 类:这个类模板接收任意类型的参数,并将它们存储在一个std::tuple中。这样做的好处是可以在单一对象中安全地存储多种类型的数据。
  • 构造函数:使用std::make_tuple函数,它接收任意数量和类型的参数,并返回一个相应的元组对象。这个元组然后被用来初始化data成员变量。
  • get 方法:这是一个模板方法,其目的是提供对元组中存储的数据的访问。std::size_t N是一个编译时常量,表示要访问的元素的索引。decltype(auto)用于推断返回类型,保证返回的类型与元组中元素的类型完全相同。

通过这种方式,TupleWrapper类隐藏了元组的复杂性,为用户提供了一个简洁的接口来存储和访问多种类型的数据。这种模式在实际编程中非常有用,尤其是在需要处理多种数据类型但又希望保持代码整洁和类型安全的情况下。

4.4 基类参数包的展开

直接看范例,演示某个类的基类也可以是可变参。

#include <iostream>

// 定义一个模板类,它通过可变参数列表继承多个基类
template<typename... BaseClasses>
class myclasst : public BaseClasses...
{
public:
    // 使用展开表达式调用每个基类的构造函数
    myclasst() : BaseClasses()...
    {
        std::cout << "myclasst::myclasst, this = " << this << std::endl;
    }
};

// 定义三个基类,每个基类在构造时打印自身的地址
class PA1
{
public:
    PA1()
    {
        std::cout << "PA1::PA1, this = " << this << std::endl;
    }
private:
    char m_s1[100];
};

class PA2
{
public:
    PA2()
    {
        std::cout << "PA2::PA2, this = " << this << std::endl;
    }
private:
    char m_s1[200];
};

class PA3
{
public:
    PA3()
    {
        std::cout << "PA3::PA3, this = " << this << std::endl;
    }
private:
    char m_s1[300];
};

int main()
{
    // 创建myclasst对象,继承自PA1, PA2, PA3
    myclasst<PA1, PA2, PA3> obj;
    std::cout << "sizeof(obj) = " << sizeof(obj) << std::endl; // 输出对象大小,应为600
}

运行结果:

PA1::PA1, this = 0x6cfbf0
PA2::PA2, this = 0x6cfc54
PA3::PA3, this = 0x6cfd1c
myclasst::myclasst, this = 0x6cfbf0
sizeof(obj) = 600

代码解释:

  • 模板类 myclasst:此类通过一个可变参数模板继承多个基类。构造函数使用初始化列表和参数包展开语法来依次调用每个基类的构造函数。
  • 基类 PA1, PA2, PA3:每个基类在构造时打印其this指针的地址。它们还包含一个字符数组成员,其大小分别为100、200、300字节,这影响了整个对象的大小。
  • 主函数:创建myclasst5的实例,该实例继承自PA1, PA2, PA3。输出该对象的总大小,这反映了所有基类大小的总和。

运行结果和分析:

  • 每个基类的构造函数被调用,并打印出各自的this指针地址。
  • myclasst5的构造函数被调用,并打印出与PA1相同的this指针地址,说明myclasst对象的开始地址与PA1的开始地址相同,这是由于PA1是第一个基类。
  • sizeof(obj)输出为600,这是因为PA1, PA2, PA3的内存布局紧密相连,总和正好是它们各自数组大小的总和。

4.5 特化

模板特化是一种通过为特定类型或条件提供专门的模板实现来优化或改变模板行为的方法。虽然可变参数模板不能进行全特化,但它们支持偏特化和显式特化。通过定义泛化版本和多个偏特化版本,可以根据不同的参数特征执行不同的操作。下面的示例演示了如何为一个可变参数模板类定义一个泛化版本和一个特化版本,以便根据参数的类型执行不同的操作。

#include <iostream>

// 声明模板类 Specialized,此处仅作声明,具体实现在后面定义
template<typename... Args>
class Specialized;

// 泛化版本:适用于任意类型的参数组合
template<typename... Args>
class Specialized
{
public:
    void print()
    {
        std::cout << "Generalized implementation\n";
    }
};

// 特化版本:当所有参数类型都是int并且参数数量为3时
template<>
class Specialized<int, int, int>
{
public:
    void print()
    {
        std::cout << "Specialized for three ints\n";
    }
};

int main()
{
    // 创建泛化版本的对象,参数类型为 double, char, float
    Specialized<double, char, float> genObj;
    genObj.print(); // 输出:"Generalized implementation"

    // 创建特化版本的对象,参数类型和数量均为 int, int, int
    Specialized<int, int, int> specObj;
    specObj.print(); // 输出:"Specialized for three ints"
    return 0;
}

解释和细节:

  • 模板类的声明:首先对模板类Specialized进行声明,这允许在类的定义之前引用该模板。
  • 泛化版本:这是一个通用实现,适用于任何类型和数量的参数。当不存在更具体的特化版本匹配时,会使用这个版本。
  • 特化版本:这个版本仅适用于当模板参数是三个int类型时。这显示了模板特化的能力,可以为特定的参数类型组合提供特定的实现。
  • main 函数中的测试:创建两个Specialized类的对象,一个使用泛化版本,一个使用特化版本,并调用它们的print方法以展示不同的行为。

标签:std,...,递归,C++,基础知识,参数,myvtfunct,模板
From: https://blog.csdn.net/qq_68194402/article/details/141832809

相关文章

  • 模板
    模板可变参数模板可变参数模板(VariadicTemplates)是C++11引入的一种强大特性,它允许模板接受可变数量的模板参数或函数参数。可变参数模板为编写通用性更强的代码提供了极大的灵活性,特别是在处理多种类型或不确定数量的参数时。模板参数包定义:template<typename...Args>中......
  • C++对象模型
    C++对象模型在C++面向对象的类中,有两种数据成员和三种成员函数:classBase{public:Base(inti):baseI(i){};intgetI(){returnbaseI;}staticvoidcountI(){};virtualvoidprint(void){cout<<"Base::print()";}virtual~Base(){}privat......
  • C++继承基础
    虚函数默认实参C++默认实参是靠编译器实现的,因此默认实参不参与动态绑定,默认实参有静态类型决定。访问控制每个类分别控制自己的成员初始化过程,还分别控制其成员对于派生类来说是否可访问,友元不继承。成员:protected:派生类可见、自己、friend可见public:所有可见private:自己......
  • 【C++算法全真练习题】迷宫问题
    目录题目描述思路AC解答题目描述‌题目描述‌:‌给定一个二维迷宫,‌其中 0 表示可以走的路,‌1 表示障碍物。‌起点坐标为 (0,0),‌终点坐标为 (m-1,n-1),‌其中 m 和 n 分别是迷宫的行数和列数。‌你需要使用广度优先搜索(‌BFS)‌找到从起点到终点的一条路径......
  • C++常见异常汇总(二): undefined reference to
    文章目录1、undefinedreferencetoA2、undefinedreferenceto`vtable2.1模版函数定义方案1:定义与实现均一起定义在头文件中2.2模版函数定义方案2:定义的同一个文件中,显示声明具体类型3、multipledefinitionof1、undefinedreferencetoA检查所有main相......
  • C++复习day06
    一、内存管理1.课件上关于内存分配的题目intglobalVar=1;staticintstaticGlobalVar=1;voidTest(){staticintstaticVar=1;intlocalVar=1;intnum1[10]={1,2,3,4};charchar2[]="abcd";constchar*pChar3="abcd";int*ptr1=(in......
  • windows C++-并行编程-转换使用异常处理的 OpenMP 循环以使用并发运行时
    此示例演示如何将执行异常处理的OpenMP并行for循环转换为使用并发运行时异常处理机制。在OpenMP中,在并行区域中引发的异常必须由同一线程在同一区域中捕获和处理。未处理的异常处理程序会捕获逃离并行区域的异常,默认情况下会终止进程。在并发运行时中,在传递给任务组(例......
  • c++元对象实现
    c++元对象实现在C++中,元对象技术通常指的是运行时检查类型信息和对象信息的能力。C++11标准引入了typetraits和reflection的概念,允许我们在编译时获取和使用类型信息。下面是一个简单的C++类,使用了C++11的typetraits和C++17的std::any来实现元对象:  #include<iostrea......
  • C++ 调用 C# - AOT 方案
    一些C#AOT编译的笔记,整体感觉:简单很方便,但限制也很多,适用于比较单一的功能点。跨语言调用C#代码的新方式-DllExport-InCerry-博客园在.NET8下,直接添加<PublishAot>true</PublishAot>就可以支持了,需要注意一些限制,这里比较相关的是,不能使用Newtonsoft.Json做序列......
  • C++ 调用 C# - DllExport 方案
    3F/DllExport:.NETDllExportwith.NETCoresupport(aka3F/DllExportakaDllExport.bat)目前还不支持.NET6以及后续的版本,看起来作者生活上遇到了一些麻烦,暂时也不打算处理了无法使用.NET6工作·问题#197·3F/DllExport---Unabletogetworkingusing.NET6......