C++霧中風景17:模板的非推斷語境與std::type_identity

乍一看這個標題很玄乎,但是其實這只是涉及一個很簡單的CPP的模板推導的知識點。
筆者近期進行CPP開發工作時,在編譯時遇到了如下的模板類型的推斷錯誤:note: candidate template ignored: deduced conflicting types for parameter T (long long vs. long int)。通過一番梳理之後總結成文,希望對大家有所幫助。

1.非推斷語境

眾所周知,函數模板的使用是C++編譯期進行類型推導的過程。通過分析源程式碼之中函數實參的類型,進一步推斷出調用的函數參數的類型,從而自動生成對應的函數,來達到精簡程式碼邏輯的效果。

而所謂非推斷語境呢?則是模板的類型不參與模板實參推導,取而代之地使用可在別處推導或顯式指定的模板實參。

單看上述文字可能很難理解,咱們直接看程式碼就能明白了。

2.舉個栗子

我們先來看看下面的一段簡單的程式碼:

template<typename T>
struct TestTemplate {
     T t; 
};

template<typename T>
T add(TestTemplate<T>& test, T val) {
   return test.t + val; 
}

int main() {
    TestTemplate<long> test_template{100};
    return add(test_template, 10); 
}

在進行編譯的時候出現如下的報錯:

note:   template argument deduction/substitution failed:
note:   deduced conflicting types for parameter 'T' ('long int' and 'int')

通過gcc的編譯報錯我們可以看出,這裡出現了錯誤的模板推斷問題。模板函數add在進行類型推斷時出現了衝突,在同一個函數中,模板類型T被同時推斷為longint

我們來分析一下模板推斷的流程。

  • 首先,參數test_template的類型為TestTempalate<long>, 它作為add函數的第一個參數傳入,此時T的類型被推導為了long
  • 接著,參數val的類型為int, 它作為add函數的第二個參數傳入,而此時由於13為int類型,所以T被推導為int類型。

正是因為這樣,在add函數進行模板推導的過程之中,兩個參數testval同時參與了模板類型的推導,導致出現了上述的問題。

我們可以嘗試將add函數的調用改為如下:add(test_template, 10l)。此時val也作為參數T也被推導為long類型,則編譯不再報錯。

3. 利用非推斷語境解決問題

顯然,上面的程式碼我們希望編譯器支援將int類型自動推導為long,而不要出現惱人的報錯。那我們就需要利用非推斷語境來解決問題了,讓val的類型不要參與到類型推導過程之中來,那麼問題就解決了。

模板的非推斷語境出現比較複雜,有需要的可以參考cppreference部分的詳細解釋。我們將利用第一種,也是最常見的非推斷語境來解決上文提到的問題。

The nested-name-specifier (everything to the left of the scope resolution operator ::)

簡單來說就是::左側作用域的類型,不參與模板類型的推導。

所以上述程式碼改為如下程式碼,就可以規避原先的問題了。

template<typename T>
struct TestTemplate {
    T t;
};

template<typename T> struct identity { typedef T type; };

template<typename T>
T add(TestTemplate<T>& test, typename identity<T>::type val) {
  return test.t + val;
}

int main() {
   TestTemplate<long> test_template{1000};
   return add(test_template, 10);
}

這裡我們新添加了類型identity, 並利用typename identity<T>::type規避了模板的類型推斷過程,從而讓val的類型推斷直接利用了test參數的類型推斷結果,所以此時val的類型為long,模板類型推斷也就不再出錯了。

正是因為非推斷語境在模板推斷中會被使用,所以C++20提供了新的trait:
std::type_identitystd::type_identity_t來幫助我們解決上述的問題。它們的實現與功能與上面展示的identity一致,都是利用模板的非推斷語境來規避類型推斷不同導致的編譯失敗問題。

4.小結

C++的一些模板推斷的問題常常讓人抓狂,很多時候gcc給出的一長串報錯很容易勸退萌新。本篇聊了聊筆者實際在開發中遇到的模板推斷問題出發,一步步分析報錯,希望大家對解決編譯問題有耐心,並擅用搜索引擎,功力必不唐捐。(當然,更新的C++標準也給我們解決問題的武器庫添磚加瓦,多多學習才是正道,日常一念:C++20好~~~

希望大家能夠有所收穫,筆者水平有限。成文之處難免有理解謬誤之處,歡迎大家多多討論,指教。

5.參考資料

CppReference

Tags: