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

HappenLee發表於2021-04-30

乍一看這個標題很玄乎,但是其實這只是涉及一個很簡單的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

相關文章