当前位置:   article > 正文

C++进阶,一文带你迅速入门c++模板元编程!

c++模板元编程

一 、c++模板元简介

C++模板元编程是一种利用C++模板特性在编译时进行计算和代码生成的技术。c++源码在编译时,首先由模板编译器来编译模板相关的代码,之后才会由c++编译器来编译源码。通过模板元编程,可以在编译阶段实现高度的泛化和优化,使代码更加灵活和高效。简单来说,c++的模板元编程有点类似于宏编程,都可以在编译阶段生成代码并完成一些计算。但总的来说,C++的模板元编程相比宏编程更加灵活、类型安全和功能强大,是一种更现代化和推荐的编程技术

在这里插入图片描述

二、c++模板

模板是C++中功能非常强大的特性,也是C++模板元编程的基础。模板最主要的作用是用于提高代码的可重用性,类似于其它语言(Java、Go、C#)中的泛型,而C++中模板的作用简单来说就是让相同的一份代码可以去兼容不同的数据类型。而模板的声明语法也很简单,使用template关键字和class或者typename便可以声明一个模板。举个例子

//在<>中声明模板参数T
template<class T>
//接受T类型的a,b两个参数,返回T类型
T add(T a, T b) {
	return a + b;
}

int main() {
	//调用add函数,这里需要用<>指明T的类型,以进行类型约束
	std::cout << add<int>(5, 8) << std::endl; //13
	std::cout << add<float>(2.5, 2.5) << std::endl; //5
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在上面的例子中我们只声明了一个模板函数,却能传入不同的数据类型,这就是模板最大的作用,即提高代码的可重用性

当然也可以支持多个模板类型参数,不同的模板类型参数中间用逗号隔开。例如:

//声明同时3个模板参数
template<class A, class B, class C>
void func(A a, B b, C c) {
    std::cout << a << std::endl;
    std::cout << b << std::endl;
    std::cout << c << std::endl;
}

int main() {
	//为模板参数指定类型,并传入相应类型的参数
    func<int, float, const char*>(4, 8.4f, "hello");
    /* 打印:
    	4
    	4.8
    	hello
    */
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

结构体和类也支持使用模板,声明方式和函数一样。

//声明带有两个参数的模板
template<class T, class E>
struct A {
    T _t;
    E _e;
    A(T t, E e) : _t(t), _e(e) {}
    void print() { std::cout << this->_t << " " << this->_e << std::endl; }
};

int main() {
	//创建结构体时,指明模板参数类型
    auto a = A<int, const char*>{3, "hello"};
    a.print();
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

模板补充1:模板特化

模板特化其实是一种为特定类型提供定制化实现的一种机制。简单来说,就是当遇到某种特殊类型需要我们使用一套特殊的逻辑进行处理时,就可以使用模板特化。举个例子,我们现在有一个结构体A,A有两个类型为std::string和int的属性。如果我们也想让A能使用add函数的话,就会发现A无法实现加法逻辑。当然你可能说可以通过重载运算符解决,但这里我们还可以使用模板特化,来单独为A提供一套逻辑

template<class T>
T add(T a, T b) {
	return a + b;
}

//模板特化
//逻辑为将a与b的id相加存入新结构体中并返回
template<>
A add<A>(A a, A b) {
	return { "test", a.id + b.id };
}

int main() {
	A a = { "a", 5 };
	A b = { "b", 6 };
	//这里因为指明的模板类型为A,所以会调用特化版本的add
	std::cout << add<A>(a, b).id << std::endl; //11
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

简单来说,模板特化类似于一种模板的重载决议,如果在使用时指明的类型符合特化模板的类型,就会优先调用特化版本,否则调用普通版本。

模板补充2:模板偏特化

模板偏特化,顾名思义,就是特化一部分模板参数,因此模板偏特化适用于声明了多个模板参数的场景。举个例子:

//声明一个模板结构体,有两个模板参数A和B
template<class A, class B>
struct S {
    A a;
    B b;
    void print() {
        std::cout << "standard template" << std::endl;
    }
};


//偏特化版本,对B类型进行特化,特化为int
template<class A>
struct S<A, int> {
    A a;
    int b;
    void print() {
        std::cout << "partial template" << std::endl;
    }
};

int main() {
    auto s1 = S<int, double> {3, 5.4}; //调用普通版本
    auto s2 = S<int, int> {3, 5}; /*因为特化的模板参数B特化为了int,而这里
    							指定B的约束类型也是int,所以优先使用偏特化版本
    							*/
    s1.print(); //standard template
    s2.print(); //partial template
    return 0;
}
  • 1
  • 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
  • 30

这里尤其需要注意的一点是,函数是不支持模板偏特化的。例如下面的代码让函数试图使用偏特化,但是编译器会直接报错。

template<class A, class B>
void test(A a, B b) {}

template<class A>
void test<A, int>(A a, int b) {}
//报错:Function template partial specialization is not allowed
//函数模板偏特化不被允许
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

函数不支持偏特化是因为一方面让函数支持偏特化会时代码变得更加复杂,另一方面,使用函数重载便可以实现模板片特的效果。例如:

template<class A, class B>
void test(A a, B b) {
    std::cout << "standard specialization" << std::endl;
}

//函数重载
template<class A>
void test(A a, int b) {
    std::cout << "similar partial specialization" << std::endl;
}


int main() {
	//调用普通版本
    test<int, double>(3, 4.9); //standard specialization
    //调用函数重载后的版本
    test<int>(3, 5); //similar partial specialization
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

模板补充3:模板编译的特性

在代码中使用模板时,编译器会根据模板代码生成对应数据类型的函数或类。每次使用不同的数据类型,编译器都会生成一个新的实例化版本。这就是为什么模板在C++中可以实现泛型编程,能够让代码适用于不同的数据类型。

三、模板元编程

模板元编程是c++中的一个非常强大的特性,他以c++的模板为基础,可以让源代码在编译阶段就能运行出结果,对于一个复杂的模板元编程系统,编译器在编译时就如同“解释器”一般,在编译时已经将其解释执行,因此也有人将c++的模板元编程戏称为c++的“黑魔法”或“屠龙技”。我们接下来就用代码来详细介绍模板元编程。

函数模板和类模板都能实现模板元编程。先举一个函数的例子。

//声明一个元数据N,这个N在使用时必须是常量或常量表达式
template<int N>
void print() {
	//使用N做为循环次数
    for (int i = 0; i < N; i++)
        std::cout << i << " ";
    std::cout << std::endl;
}

int main() {
	//调用该函数,并指定N为8,即print中的循环会执行8次
    print<8>(); //打印:0 1 2 3 4 5 6 7
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在上述例子中,print函数中使用了模板参数N来限制循环次数,在调用该函数时,指定N的值(必须是常量或常量表达式,也就是编译时能确定的值),比如例子中N的值为8,print中的循环就会循环8次。

你或许会疑问这么写有什么意义,将N当作参数传进去似乎也能起到一样的作用,使用模板参数和直接传参的区别在于,因为模板参数在编译时已经确认,因此在模板参数参与代码逻辑构成的时候,编译器可以对其进行一定的优化。再举个例子:

//定义模板参数E,类型为bool
template<bool E>
void test() {
	//分支判断
    if (E) {
        std::cout << "true" << std::endl;
    } else {
        std::cout << "false" << std::endl;
    }
}

int main() {
    test<true>(); //true
    test<false>(); //false
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在上面这个例子中,因为E的值是编译时就确定了的,于是在test函数中,编译器会优化掉不执行的分支,在运行时就会少一次判断提高性能。当然,c++的编译器十分强大,随着你使用的模板越来越复杂,编译器的编译优化也会越来越明显。

接下来我们将使用c++模板来完成一些简单的算法。

四、模板元编程的使用

1.求斐波那契数列的第n项

先来个简单的例子,我们使用模板元编程递归的来实现求斐波那契数列第n项的算法。斐波那契数列的定义如下:
f ( n ) = { 0 , n = 0 , 1 , n = 1 , f ( n − 1 ) + f ( n − 2 ) , n > 1 f(n)={0,n=0,1,n=1,f(n1)+f(n2),n>1 f(n)= 0,1,f(n1)+f(n2),n=0,n=1,n>1
首先定义一个模板结构体,模板参数类型为int,结构体中存储着一个整型的静态常量表达式res,
res等于“调用自己的类型”的N-1加上N-2,也就是斐波那契数列的数学定义。这个过程本质上是递归的。

//定义模板参数
template<int N>
struct Fib {
	//递归的“调用自己”,求出第N-1项和第N-2项
    constexpr static int res = Fib<N - 1>::res + Fib<N - 2>::res;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

写过斐波那契数列递归求第n项算法的同学应该对上面的代码感到熟悉,但是上面的代码存在致命的问题,就像递归函数需要终止条件一样,上述计算过程并没有终止条件,我们需要它在N=0和N=1时便停止递归,这时我们就需要模板特化,来特化出N=0和N=1的模板结构体。如下面代码所示:

//特化出n=1的情况
template<>
struct Fib<1> {
    constexpr static int res = 1;
};
//特化出n=0的情况
template<>
struct Fib<0> {
    constexpr static int res = 0;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

现在上面的Fib就可以正常使用了,我们在main函数中”调用一下“。

template<int N>
struct Fib {
    constexpr static int res = Fib<N - 1>::res + Fib<N - 2>::res;
};

template<>
struct Fib<1> {
    constexpr static int res = 1;
};

template<>
struct Fib<0> {
    constexpr static int res = 0;
};

int main() {
	//求斐波那契数列的第6项的值
    int n = Fib<6>::res;
    std::cout << n << std::endl; //8
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

事实上如果你的代码编辑器比较智能可以显示编译时变量的值的话,你在编写完代码时就可以知道n的值了,也就是说在编译的时候就已经计算出第6项的值为8,而不需要到运行时才能知道,这也是模板元编程最为强大的地方,即编译时计算。比如笔者的编辑器是clion,在编写完代码后就已经知道计算后的值了,如图:
在这里插入图片描述
接下来我们再来几个复杂的算法来练练手。

2.求n的阶乘

阶乘的定义如下:
n ! = n × ( n − 1 ) × ( n − 2 ) × . . . × 2 × 1 n!=n \times (n-1) \times (n-2) \times ... \times 2 \times 1 n!=n×(n1)×(n2)×...×2×1
同样使用模板递归,编写表示出递归式,再编写出特化来作为终止条件。代码如下所示:

//使用模板类来定义出求阶乘的递归式
template<int N>
struct Factorial {
    constexpr static int res = N * Factorial<N - 1>::res;
};

//特化出终止条件,也就是n = 0时,值为1
template<>
struct Factorial<0> {
    constexpr static int res = 1;
};

int main() {
	//这里求4的阶乘,同样,这个值在编译时就已经求出了
    std::cout << Factorial<4>::res << std::endl; //24
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.求最大公约数

求两个数的最大公约数,递推式如下:
g c d ( a , b ) = { a , b = 0 , g c d ( b , a % b ) , b > 0 gcd(a,b)={a,b=0,gcd(b,a%b),b>0 gcd(a,b)={a,gcd(b,a%b),b=0,b>0
代码如下所示:

//通过两个模板参数的模板结构体来构造递归式
template<int A, int B>
struct Gcd {
    constexpr static int res = Gcd<B, A % B>::res;
};
template<int A>
//通过特化来实现终止条件,当b=0时,a就是最大公约数
struct Gcd<A, 0> {
    constexpr static int res = A;
};
int main() {
	//计算8和12的最大公约数,同样,在编译时就已经知道了结果。
    std::cout << Gcd<8, 12>::res << std::endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

4.求01背包问题

01背包问题是经典的动态规划问题,其问题描述如下:
有N件物品和一个容量是Cap的背包。每件物品只能使用一次。
第 i件物品的体积是 value[i],价值是 weight[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
01背包的状态可定义为dp[i][j],即只有前i个物品和背包容量为j时,能取到的最大价值。状态转移方程如下:
d p ( i , j ) = m a x { d p ( i − 1 , j ) , j < w e i g h t [ i ] , d p ( i − 1 , j − w e i g h t [ i ] ) + v a l u e [ i ] ) , j ≥ w e i g h t [ i ] dp(i, j)=max{dp(i1,j),j<weight[i],dp(i1,jweight[i])+value[i]),jweight[i] dp(i,j)=max{dp(i1,j),dp(i1,jweight[i])+value[i]),j<weight[i],jweight[i]
使用模板元编程解决该问题的算法如下:

//Cap为背包体积,N为物体数量
template<int Cap, int N>
struct Solvation {
	//背包中物体的价值数组
    constexpr static int value[N] = {2, 5, 4, 6, 3};
    //背包中物体的重量数组
    constexpr static int weight[N] = {4, 5, 3, 6, 2};
    //将该函数定义为常量表达式,在编译时计算
    constexpr static int solve() {
        int dp[N + 5][Cap + 5] = {};
        for (int i = 1; i <= N; i++)
            for (int j = 0; j <= Cap; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= weight[i - 1])
                    dp[i][j] = std::max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
            }
        return dp[N][Cap];
    }
};

int main() {
	//背包容积为13,物品总数为5的问题的解
	//编译时便可得出答案
    std::cout << Solvation<13, 5>::solve() << std::endl; //14
    return 0;
}
  • 1
  • 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

5.类型检查器

我们可以使用模板元编程来实现参数的类型检查的功能。首先定义一个模板结构体IsInt,模板中有一个模板参数T,IsInt中有一个bool静态常量value,默认为false。然后再特化一个模板,模板参数类型特化为intvalue的默认值特化为true。如下列代码所示:

template<class T>
struct IsInt {
    constexpr static bool value = false;
};

template<>
struct IsInt<int> {
    constexpr static bool value = true;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

通过上面代码我们可以发现当T为int时,valuetrue,当T为其它类型时,valuefalse
接下来,我们实现一个名叫isInt的模板函数,该函数接受两个模板参数,一个为A,里另一个为默认模板参数B,类型为bool,值为IsInt<A>::value。代码如下所示:

template<class A, bool B = IsInt<A>::value>
void isInt(A a) {
    if (B) std::cout << "is int" << std::endl;
    else std::cout << "is not int" << std::endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5

我们在main函数中调用一下,isInt()函数已经具有了检查一个类型是否为int的功能。

template<class T>
struct IsInt {
    constexpr static bool value = false;
};

template<>
struct IsInt<int> {
    constexpr static bool value = true;
};

//当A为int时,B为true,否则为假
template<class A, bool B = IsInt<A>::value>
void isInt(A a) {
    if (B) std::cout << "is int" << std::endl;
    else std::cout << "is not int" << std::endl;
}

int main() {
	//模板参数可以由参数自动推导
    isInt(5.5); //is not int
    isInt(9); // is int
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

上述代码其实也是c++中类型萃取的简单实现,可以在编译阶段检查变量的类型。

五、总结

模板元编程是一种利用模板在编译时进行计算和生成代码的技术,可以用于优化计算和生成通用算法。在编译时进行计算可以提高程序性能、减少运行时开销。不过有意思的一点是,模板元编程并不是被有意设计出来的,而是一个意外发现。这也导致了模板元编程的语法总让人一言难尽,可读性极差。而且由于设计大量模板的源码会在编译时进行大量的计算,这也导致了一些涉及了大量的模板的项目在编译时,编译速度极其的缓慢,这也是c++被人诟病的一点之一。但是,由于模板的一系列重要特性,导致模板在c++高级编程中担任着重要角色。因此,c++模板元编程也是c++进阶编程的必备技能之一。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/799451
推荐阅读
相关标签
  

闽ICP备14008679号