函数模板

一、auto自动推导类型

  1. auto声明的变量必须在定义时初始化。

  2. 初始化的右值可以是具体的数值,也可以是表达式和函数的返回值。

    string func()
    {
        return("hhh");
    } 
    
    int main()
    {
        auto a = 3;             cout << a << endl;
        auto b = 3 + 0.0f;      cout  << b << endl;
        auto c = "abc";         cout << c << endl;
        auto d = func();        cout << d << endl;
        return 0;
    }
    
  3. auto不能作为函数形参类型,因为函数形参的类型需要在编译时就已经确定。

    void fun(auto a){}   //参数不能为包含“auto”的类型
    
  4. auto不能直接声明数组,数组的大小必须在编译时是已知的常量表达式。

    int arr[] = {1, 2, 3, 4};
    auto anotherArr = arr; 
    // 这实际上是一个指向 int 的指针,而不是 int 数组
    
    auto myArray[] = {5, 6, 7, 8}; // 错误
    
  5. auto不能定义类的非静态成员变量。

    auto 的设计初衷是为了在声明变量时自动推导其类型,而类的成员变量需要在类的定义中明确指出类型,以便编译器在编译时能够知道其确切的类型和大小。

    class MyClass 
    {
    public:
        auto myVar = 42; 
        // 错误:auto不能用于类的成员变量
    };
    

auto的真实用途:

  1. 代替冗余复杂的变量声明。
    double func(int a, double b, const char *c, float d, short e, long f)
    {
        cout << a << " " << b << " " << c << " " << d << " " << e << " " << f << endl;
    }
    int main()
    {
        //声明函数指针
        double(*p1)(int a, double b, const char *c, float d, short e, long f);
        p1 = func;
        p1(1, 2.2, "hhh", 0.0f, 3, 9);
        
        //引入auto
        auto p2 = func;
        p2(1, 2.2, "hhh", 0.0f, 3, 9);
        return 0;
    }
    
  2. 在模版中,用于声明依赖模版参数的变量。
    #include <iostream>
    #include <vector>
    
    // 模板函数,计算并返回容器中所有元素的和
    template<typename T>
    T sum(const std::vector<T>& vec) 
    {
        T total = 0; // 初始化总和为0,类型为T
        for (const auto& element : vec) 
        {
            // 使用auto来迭代容器中的元素,但这里auto主要用于简化迭代器的使用
            // 我们也可以声明一个auto类型的变量来存储中间结果
            auto temp = element * 2; 
            // 假设我们需要将每个元素乘以2后再加到总和中
            
            // 注意:这里的temp变量类型实际上是T,因为element的类型是T,所以element * 2的类型也是T
            total += temp;
        }
        return total;
    }
    
    int main() 
    {
        std::vector<int> intVec = {1, 2, 3, 4, 5};
        std::vector<double> doubleVec = {1.1, 2.2, 3.3, 4.4, 5.5};
    
        std::cout << "Sum of intVec elements doubled: " << sum(intVec) << std::endl;
        std::cout << "Sum of doubleVec elements doubled: " << sum(doubleVec) << std::endl;
    
        return 0;
    }
    
  3. 函数模版依赖模版参数的返回值。
    #include <iostream>
    
    // 模板函数,接受两个相同类型的参数并返回它们的和
    template<typename T>
    auto add(T a, T b) 
    {
        return a + b;
    }
    
    int main() 
    {
        int x = 5, y = 10;
        double a = 2.5, b = 3.5;
    
        // 调用模板函数,编译器将根据传递的参数类型自动推导返回类型
        std::cout << "Sum of integers: " << add(x, y) << std::endl;       // 返回int类型
        std::cout << "Sum of doubles: " << add(a, b) << std::endl;        // 返回double类型
    
        return 0;
    }
    
  4. 用于lambda表达式中。

二、函数模版

C++98 添加关键字typename之前,C++使用class来创建模版。
考虑到向后兼容,应该使用typename,而不是class。

template<typename T>
void Swap(T &a, T &b)
{
    T temp = a;
    a = b;
    b = temp;
}

int main()
{
    int a = 1, b = 2;
    Swap(a, b);
    return 0;
}

在使用的时候,会根据参数的类型自动推导模版参数的类型。
在生成函数实例的时候,编译器会进行名称修饰,以区分不同类型的函数实例。

//类似于
void #DD%$Swap#$@@&(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
}

int main()
{
    int a = 1;
    int b = 2;
    #DD%$Swap#$@&(a, b);
}

还可以手动指定模版参数的类型。

Swap<int>(a, b);

三、函数模版注意事项

  1. 可以为类的成员函数创建模版,但不能是虚函数和析构函数。

    class MyClass
    {
    public:
        template <typename T>
        MyClass(T a)
        {
            cout << "a = " << a << endl;
        }
    
        template <typename T>
        void Show(T b)
        {
            cout << "b = " << b << endl;
        }
    
        //错误,虚函数不能使用函数模板
        template <typename T>
        virtual void fun(T c)
        {
            cout << "c = " << c <<endl;
        } 
    
        //析构函数没有参数,不需要模板
        ~MyClass()
        {
            cout << "析构函数" << endl;
        }
    };
    
    int main()
    {
        MyClass a(3);
        a.Show("hhh");
        a.Show(3 + 3.1);
        //a.fun(3.1415);      error
        return 0;
    }
    
  2. 使用函数模版时,必须明确数据类型,确保实参与函数模版匹配上。

    //错误,参数类型不一致
    template <typename T>
    void Swap(T& a, T&b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
    
    int main()
    {
        int a = 10;
        float b = 3.14f;
        Swap(a, b);
    }
    

    特殊情况:

    template <typename T>
    void Swap()
    {
        cout << "调用Swap()函数 " << endl;
    }
    int main()
    {
        Swap<int>();        //此处<>什么类型都可以
    }
    
  3. 使用函数模版时,推导的数据类型必须适应函数模版中的代码。

    这个例子中a + b是类类型的相加,没有重载+运算符,所以会报错。

    template <typename T>
    class MyClass
    {
    public:
        MyClass(T a) { cout << "a = " << endl;}
    };
    
    template <typename T>
    T add(T a, T b)
    {
        return a + b;
    }
    
    int main()
    {
        MyClass a(1);
        MyClass b(20);
        add(a, b);
        return 0;
    }
    
  4. 使用函数模版时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模版的数据类型,可以发生隐式类型转换。

    template <typename T>
    T Add(T a, T b)
    {
        return a + b;
    }
    
    int main()
    {
        int a = 10;
        char b = 20;
    
        //显式指定函数模板的类型,编译器可以对实参进行隐式的类型转换。
        int c = Add<int>(a, b); 
        //int c = Add(a, b);    //报错,类型不一致。
        cout << c << endl;
        return 0;
    }
    
  5. 函数模板支持多个通用数据的类型。

    template <typename T1, typename T2>
    void Show(T1 num, T2 name)
    {
        cout << "号码是:" << num  << " 名字是:" << name << endl;
    }
    
    int main()
    {
        Show(10,"hhh");
        return 0;
    }
    

四、函数模板的具体化

可以提供一个具体化的函数定义,当编译器找到函数调用匹配的具体化定义时,将使用该定义,不再寻找模板。

template<> void 函数模板名 <数据类型> (参数列表)
template<> void 函数模板名 (参数列表)
{
    //函数体
}

下面这个例子,如果仅需要交换类中的排名rank,该如何实现,这里就要引入函数模板的具体化。

template <typename T>
void Swap(T& a, T& b)
{
    T temp = a;
    a = b;
    b = temp;
}

class MyClass
{
public:
    int _rank;
    string name;
};

引入具体化函数:

//两种方式相同
//template<> void Swap<MyClass>(MyClass &m1, MyClass &m2)
template<> void Swap(MyClass &m1, MyClass &m2)
{
    int temp = m1._rank;
    m1._rank = m2._rank;
    m2._rank = temp;
    cout << "调用了Swap(MyClass &m1, MyClass &m2)" << endl;
}

int main()
{
    MyClass m1;
    m1.name = "hh";
    m1._rank = 10;
    MyClass m2;
    m2.name = "jj";
    m2._rank = 3;
    Swap(m1, m2);
    //当编译器推导实参的数据类型,如果与具体化函数能匹配上,
    //会优先使用具体化函数
    cout << "hh :" << m1._rank << " jj :" << m2._rank << endl;
}

对于给定的函数名,可以有普通函数、模板函数和具体化的模板函数,以及它们重载的版本。

编译器调用规则:

  1. 普通函数 > 具体化的模板函数 > 模板函数
void Swap(int a, int b)
{
	cout << "调用了普通函数" << endl;
}

template <typename T>
void Swap(T a, T b)
{
	cout << "调用了函数模板"  << endl;
}


template<>
void Swap(int a, int b)
{
	cout << "调用了具体化的函数模板" << endl;
}

int main()
{
    Swap(1,3);      //调用普通函数
    return 0;
}
template <typename T>
void Swap(T a, T b)
{
	cout << "调用了函数模板"  << endl;
}


template<>
void Swap(int a, int b)
{
	cout << "调用了具体化的函数模板" << endl;
}

int main()
{
    Swap(1,3);      //调用具体化的函数模板
    return 0;
}
  1. 如果希望使用函数模板,可以用空模板参数强制使用函数模板
template <typename T>
void Swap(T a, T b)
{
	cout << "调用了函数模板"  << endl;
}


template<>
void Swap(int a, int b)
{
	cout << "调用了具体化的函数模板" << endl;
}

int main()
{
    Swap<>(1,3);      //空模板参数,调用具体化的函数模板
    //如果注释掉具体化的函数模板,调用函数模板
    return 0;
}
  1. 虽然普通函数 > 函数模板,但是如果函数模板能产生更好的匹配,将优先于 非模板函数。
void Swap(int a, int b)
{
	cout << "调用了普通函数" << endl;
}

template <typename T>
void Swap(T a, T b)
{
	cout << "调用了函数模板"  << endl;
}


template<>
void Swap(int a, int b)
{
	cout << "调用了具体化的函数模板" << endl;
}

int main()
{
    Swap('a','b');        //此处是字符类型,字符类型可以隐式转换为int类型
    //此处字符类型可以和函数模板完全匹配,不需要进行隐式转换。
    //所以,调用函数模板
    return 0;
}

五、函数模版分文件编写

函数模版只是函数的描述,没有实体,声明和定义函数模版的代码放在头文件中。

函数模版的具体化和普通函数,声明放在头文件,定义放在源文件。

public.h

#include <iostream>
using namespace std;

void Swap(int a, int b);    //普通函数在.h中声明

template <typename T>       //函数模版在.h中声明并定义
void Swap(T a, T b)
{
	cout << " 使用了函数模版" << endl;
}

template <>                 //函数模版的具体化在.h中声明
void Swap(int a, int b);

public.cpp

#include "public.h"

void Swap(int a, int b)                 //普通函数在.cpp文件中定义
{
	cout << "调用了普通函数" << endl;
}

template <>
void Swap(int a, int b)                 //具体化的函数模版在.cpp中定义
{
	cout << "调用了具体化的函数模版" << endl;
}

main.cpp

#include "public.h"

int main()
{
	Swap(1,2);			//调用普通函数
	Swap('a', 'b');		//调用函数模版
    Swap<>(1, 2);		//将使用函数模版的具体化版本
    return 0;
}

六、函数模版高级技巧

1、引出问题

我们定义一个函数模版,使得T1和T2相加,那么他们相加的类型是什么?

我们可以使用auto解决这个问题。

template <typename T1, typename T2>
void func(T1 x, T2 y)
{
	auto temp = x + y;
	cout << " temp = " << temp << endl;
	//return temp;
}

那如果需要他返回一个值呢,使用auto也同样可以。

auto func(T1 x, T2 y);

我们也可以用decltype来解决这个问题。

2、decltype关键字

在C++11中,用于查询表达式的数据类型。

语法: decltype (表达式) var;

decltype分析表达式并得到它的类型,不会计算执行表达式。如果表达式是函数调用的话,不用担心使用decltype时会执行函数。

总结:decltype要么是表达式的类型,要么是表达式的引用类型。

decltype推导规则:

  1. 如果表达式是一个没有用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const 等限定符。

  2. 如果 表达式 是一个函数调用,则var的类型与函数的返回值类型相同(不能返回void ,但是可以返回void *)

  3. 如果 表达式 是一个左值(能取地址,要排除第一种情况),或用括号括起来的标识符,则var的类型是该表达式的类型的引用。

  4. 如果上面的条件都不满足,则var的类型与表达式的类型相同。

  5. 如果需要多次使用decltype,可以结合typedef 和 using

  6. 表达式是没有括号的标识符

int main()
{
	short a = 5;
	decltype(a) da;		
	//这里a是原本定义时的括号,不算,此处应遵循1,da的类型应该和a相同
	//都是short类型。

	short *b;
	decltype(b) db;		//db是 short*类型

	const short *c;
	decltype(c) dc; 	//dc是const short*类型

	short &d = a;
	decltype(d) dd = a;		//dd是short &类型
	return 0;
}
  1. 表达式是函数调用
int fun()
{
    cout << "调用了fun函数" << endl;
	return 1;
}

int main()
{
	decltype(fun()) a;      

    //decltype(fun) a;      //此时的a是一个函数类型

    //decltype(fun) *a;     //此时的a是一个函数指针

    //decltype(fun) *a = fun;
    //a();              //此时就调用了fun函数    

    //此处注意区分函数调用和函数名
    //如果是函数名,得到的是函数类型,而不是函数返回值的类型。
    //如果是函数调用,不会指向这个函数,得到的是函数返回值的类型。
	cout << a << endl;
	return 0;
}

  1. 表达式是一个左值,要与情况一区分
	short a = 5;

	decltype(++a) da = a;    
    //情况三:表达式是左值,da是short&类型

    decltype(a) da;
    //情况一:表达式是没有括号的标识符,应该与a的类型相同,da是short类型

    decltype((a)) da = a;
    //情况三:用括号括起来的标识符,da应该是表达式的引用类型。


    decltype(fun) da;    
    //情况一:表达式是函数调用,da是函数类型

    decltype((fun)) da = fun;
    //情况三:用括号括起来的标识符,da是函数的引用类型。(注意:引用都需要初始化)
	return 0;
  1. 结合typedef 和 using
#include <iostream>
#include <array>
 
int main() {
    // 定义一个简单的整数数组
    std::array<int, 5> myArray = {1, 2, 3, 4, 5};
 
    // 使用decltype获取myArray的类型,并使用using定义一个别名
    using MyArrayType = decltype(myArray);
 
    // 现在可以使用MyArrayType来声明另一个相同类型的数组
    MyArrayType anotherArray = {6, 7, 8, 9, 10};
 
    // 输出anotherArray的内容
    for (int value : anotherArray) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
 
    return 0;
}

3、函数后置返回类型

int func(int x, double y);
等同于:
auto func(int x, double y) -> int;
将返回类型移到了函数声明后面。

//以上面的相加为例
template <typename T1, typename T2>
auto func(T1 x, T2 y) -> decltype(x + y) 
{
	decltype(x + y) temp = x + y;
	cout << " temp = " << temp << endl;
	return temp;
}
int main()
{
	func(1, 2.5);
    return 0;
}

C++14中对函数返回值类型推导规则进行了优化,函数的返回值可以用auto,不必尾随返回类型。

//意思是可以不要函数后面的-> decltype(x + y)
template <typename T1, typename T2>
auto func(T1 x, T2 y) //-> decltype(x + y) 
{
	decltype(x + y) temp = x + y;
	cout << " temp = " << temp << endl;
	return temp;
}

七、非模版类型参数

一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。非类型模版参数的模版实参必须是常量表达式,在编译时就可以确定其值。

template <typename T, int N>
class MyArray
{
public:
	MyArray()
	{
		cout << "调用了MyArray的构造函数" << endl;
	}
private:
	T arr[N];  
};

int main()
{
	MyArray<int, 10> arr;
	return 0;
}
posted @ 2025-01-09 23:25  baobaobashi  阅读(33)  评论(0)    收藏  举报