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 &。

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