c++函数模板的使用

C++函数模板的使用

简介

使用c++函数模板有非常方便的功能,可以利用模板函数的自动类型推导功能完成很多功能。比如:生成一些模板类实例(例如:stl make_pair, make_tuple,还比如boost::typeindex::type_id等几个方法…),可以对提前定义的模板类来做一些提前定义某些特性,比如对某个提前声明的模板类重载流操作…… 。

函数模板虽然非常好,但是它也存在一些潜在的问题,虽然你可能没有关注到这个问题,但是如果你使用不当的话,那么那些潜在的性能问题,可能就会发生。

潜在类型转换问题

#include <iostream>
#include <string>
using namespace std;

template <typename T>
void println(T t)
{
    cout << t << endl;
}

int main(int argc, char ** argv)
{
    string hello("hello");
    string &hello_ref = hello;
    const string &hello_const_ref = hello;

    println(hello);
    println(hello_ref);
    println(hello_const_ref);

    return 0;
}

几个小问题

上面的小程序非常简单,就是定义一个模板方法:println,这个println模板方法很简单,仅仅完成通过标准流输出,然后输出换行符。

然后在main函数中,对三种不同的字符串类型都调用这个模板方法来进行输出。那么我有几个问题想问您?

调用println(hello)方法,这个模板方法的实际参数类型是什么?就是模板中的T的具体类型是什么?这儿很明显应该是string类型。那么请看下面几个相似的问题。

println(hello_ref)方法,这个模板方法中T的真正类型是什么。是string &类型吗?还是string类型?

println(hello_const_ref)方法,这个方法中T的真正类型是什么。是const string &类型吗? 还是string类型?

我想很多人都认为是第一个答案(我原本也以为),但是实际呢?下面就编个小程序来验证下。

#include <iostream>
#include <string>
#include <boost/type_index.hpp>

using namespace std;
using namespace boost;

template <typename T>
void println(T t)
{
    // 输入T的真实类型
    cout << typeindex::type_id_with_cvr<T>().pretty_name() << endl;
    cout << t << endl;
}

int main(int argc, char ** argv)
{
    string hello("hello");
    string &hello_ref = hello;
    const string &hello_const_ref = hello;

    println(hello);
    println(hello_ref);
    println(hello_const_ref);

    return 0;
}

运行结果为:

std::string
hello
std::string
hello
std::string
hello

答案

额,居然是第二个答案,确实让人吃惊。

这肯定不是我们的原有意图,因为T的真实类型是std::string的话,那么实际参数传给形参的话,肯定会发生string的构造和销毁操作。但是对于println这个函数功能来讲的话,这些损耗都是不必要的。

从这个例子我们可以总结,对于以类似T t(T是模板参数)这样写的形参,那么在实参这个模板函数的时候,编译器会自动将实参的不带cvr修饰的类型作为T的真实类型,然后生成真实类型的函数定义,使用这个函数来完成函数调用过程。发生这个问题的罪魁祸首就是函数模板的类型自动推导功能实际上模板参数类型T,他确实可以表示任何类型,包含常量类型和引用类型,造成上面的情况的原因就是因为编译器对于模板函数的类型推导

问题解决方案

那么解决这个的方案有两种,一种是我们调用模板方法时手动指定模板参数类型。比如下面这样:

#include <iostream>
#include <string>
#include <boost/type_index.hpp>

using namespace std;
using namespace boost;

template <typename T>
void println(T t)
{
    // 输入T的真实类型
    cout << typeindex::type_id_with_cvr<T>().pretty_name() << endl;
    cout << t << endl;
}

int main(int argc, char ** argv)
{
    string hello("hello");
    string &hello_ref = hello;
    const string &hello_const_ref = hello;

    // 调用模板函数时,手动指定模板参数类型
    println<string>(hello);
    println<string&>(hello_ref);
    println<const string&>(hello_const_ref);

    return 0;
}

运行结果为:

std::string
hello
std::string&
hello
std::string const&
hello

手动指定模板参数

上面的解决方案,是在调用模板函数的时候,手动指定实际模板参数类型,使用这种方法适用于一些比较新的编译器,但是对于老的编译器来讲,可能不支持这个写法。并且就算是对支持的情况,每次调用模板方法的时候,都指定实际的模板参数类型,这样写,您可能会觉得很不方便,并且一旦某个函数调用漏了指定实际模板参数操作,那么就会像最先描述的那样,会产生多余的拷贝和析构损耗,对于大对象来讲,这些消耗是非常严重的。

根据情况,使用const T &作为参数类型

其实,个人更推荐下面,这个解决方案,对于某些模板方法,他如果是仅仅获取参数的内容的话,那么推荐使用const T &,形参类型,详情见下面:

#include <iostream>
#include <string>
#include <boost/type_index.hpp>

using namespace std;
using namespace boost;

//使用const  T &作为形参类型,这样不会发生多余的拷贝
//只是这样写,就不能改变t的值
template <typename T>
void println(const T &t)
{
    // 输入T的真实类型
    cout << typeindex::type_id_with_cvr<const T&>().pretty_name() << endl;
    cout << t << endl;
}

int main(int argc, char ** argv)
{
    string hello("hello");
    string &hello_ref = hello;
    const string &hello_const_ref = hello;

    println(hello);
    println(hello_ref);
    println(hello_const_ref);

    return 0;
}

输出结果为:

std::string const&
hello
std::string const&
hello
std::string const&
hello

总结

现在总结一下上面遇到的问题:

对于模板函数void println(T t)来讲,因为模板函数的自动类型推导功能,T总是一个不带cvr修饰的类型,当这样的模板方法在被调用的时候,他总是会发生参数的拷贝构造和析构过程,对于大对象来讲,这会损害程序的性能的。并且你也不能想当然的告诉自己,T t只能表示某种不带cvr修饰的类型。其实T也是可以表示任何类型的,造成这个原因的罪魁祸首是因为编译器自带的对于模板参数的自动类型推导功能

解决上面的问题有两种方案:
- 不使用函数模板参数的类型推导,我们手动指定具体的模板参数类型
- 当函数模板不改变参数的值的时候,那么就将onst T &作为具体参数类型。

这儿确实是C++模板函数的一个坑,使用函数模板的时候,就要问自己,在这个模板方法中是否要修改参数的值,如果不要修改的话,那么果断使用const T &。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章