目錄
1.什么是SFINAE
2.SFINAE(替換失敗不是錯誤)
3.通過std::decltype來SFINAE掉表達式
1.什么是SFINAE
????????SFINAE 技術,即匹配失敗不是錯誤,英文Substitution Failure Is Not An Error,其作用是當我們在進行模板特化的時候,會去選擇那個正確的模板,避免失敗。
????????SFINAE一般用于函數重載和編譯期間類型檢查,標準庫中很多type traits模板就是通SFINAE來實現的。
????????看個具體的例子:
#include <iostream>
#include <type_traits>
using namespace std;template<typename T>
struct check_has_member_id
{// 僅當T是一個類類型時,“U::*”才是存在的,從而這個泛型函數的實例化才是可行的// 否則,就將觸發SFINAEtemplate<typename U>static void check(decltype(&U::id)){}// // 僅當觸發SFINAE時,編譯器才會“被迫”選擇這個版本template<typename U>static int check(...){}enum {value = std::is_void<decltype(check<T>(NULL))>::value};
};struct TEST_STRUCT
{int rid;
};struct TEST_STRUCT2
{int id;
};int main()
{check_has_member_id<TEST_STRUCT> t1;cout << t1.value << endl;check_has_member_id<TEST_STRUCT2> t2;cout << t2.value << endl;check_has_member_id<int> t3;cout << t3.value << endl;return 0;
}
// g++ --std=c++11 xxx.c
????????核心的代碼是在實例化check_has_member_id對象的時候,通過模板參數T的類型,決定了結構體中對象value的值。而value的值是通過check<T>函數的返回值是否是void決定的。如果T中含有id成員的話,那么就會匹配第一個實例,返回void;如果不包含id的話,會匹配默認的實例,返回int。
????????利用這個機制還可以做很多類似的判斷,比如判斷一個類是否是結構體。
#include <iostream>
#include <type_traits>// 2. 判斷變量是否是一個struct 或者 類
// https://www.jianshu.com/p/d09373b83f86
template <typename T>
struct check
{template <typename U>static void check_class(int U::*) {}template <typename U>static int check_class(...) {}enum { value = std::is_void<decltype(check_class<T>(0))>::value };
};class myclass {};int main()
{check<myclass> t;std::cout << t.value << std::endl;check<int> t2;std::cout << t2.value << std::endl;return 0;
}
std::is_void的用法可參考:
C++17之std::void_t-CSDN博客
2.SFINAE(替換失敗不是錯誤)
????????在一個函數調用的備選方案中包含函數模板時,編譯器首先要決定應該將什么樣的模板參數 用于各種模板方案,然后用這些參數替換函數模板的參數列表以及返回類型,最后評估替換 后的函數模板和這個調用的匹配情況(就像常規函數一樣)。
????????但是這一替換過程可能會遇到問題:替換產生的結果可能沒有意義。不過這一類型的替換不會導致錯誤,C++語言規則要 求忽略掉這一類型的替換結果。
????????考慮如下的例子:
// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{return t.size();
}
當傳遞的參數是裸數組或者字符串常量時,只有那個為裸數組定義的函數模板能夠匹配:
int a[10];
std::cout << len(a); // OK: only len() for array matches
std::cout << len("tmp"); //OK: only len()
????????如果只是從函數簽名來看的話,對第二個函數模板也可以分別用 int[10]和 char const [4]替換 類型參數 T,但是這種替換在處理返回類型 T::size_type 時會導致錯誤。因此對于這兩個調用, 第二個函數模板會被忽略掉。
????????如果傳遞的是裸指針,以上兩個模板都不會被匹配上(但是不會因此而報錯)。此時編譯 期會抱怨說沒有發現合適的 len()函數:
int* p;
std::cout << len(p); // ERROR: no matching len() function found
但是這和傳遞一個有 size_type 成員但是沒有 size()成員函數的情況不一樣。比如如果傳遞的參數是 std::allocator<>:
std::allocator<int> x;
std::cout << len(x); // ERROR: len() function found, but can’t size()
此時編譯器會匹配到第二個函數模板。因此不會報錯說沒有發現合適的 len()函數,而是會 報一個編譯期錯誤說對 std::allocator而言 size()是一個無效調用。此時第二個模板函數不 會被忽略掉。
如果忽略掉那些在替換之后返回值類型為無效的備選項,那么編譯器會選擇另外一個參數類 型匹配相差的備選項。比如:?
// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{return t.size();}
// 對所有類型的應急選項:
std::size_t len (…)
{return 0;
}
????????此處額外提供了一個通用函數 len(),它總會匹配所有的調用,但是其匹配情況也總是所有 重載選項中最差的(通過省略號...匹配)。
????????對于指針,只有應急選項能夠匹配上,此時編譯器不會再報缺少適用 于本次調用的 len()。不過對于 std::allocator的調用,雖然第二個和第三個函數都能匹配 上,但是第二個函數依然是最佳匹配項。因此編譯器依然會報錯說缺少 size()成員函數。
3.通過std::decltype來SFINAE掉表達式
????????對于有些限制條件,并不總是很容易地就能找到并設計出合適的表達式來 SFINAE 掉函數模 板。
????????比如,對于有 size_type 成員但是沒有 size()成員函數的參數類型,我們想要保證會忽略掉函 數模板 len()。如果沒有在函數聲明中以某種方式要求 size()成員函數必須存在,這個函數模 板就會被選擇并在實例化過程中導致錯誤:
template<typename T>
typename T::size_type len (T const& t)
{return t.size();
}
std::allocator<int> x;
std::cout << len(x) << ’\n’; //ERROR: len() selected,
處理這種情況有一個常見的模式或者習慣用法:
1)通過尾置返回類型語法(函數名前用auto修飾,并在函數名后跟->,再加末尾的返回類型) 來制定返回類型。
2)使用std::decltype和逗號運算符來定義返回類型。
3)將所有必須成立的表達式放置于逗號運算符開頭(表達式轉換為void類型,以防逗號運算符重載)。
4)在逗號運算符末尾定義一個實際返回類型(類型為返回類型)的對象。
例如:
template<typename T>
auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() )
{return t.size();
}
這里返回類型定義為:
decltype( (void)(t.size()), T::size_type() )
? ? ? ? 由于decltype構造的操作數是以逗號分隔的表達式列表,因此,最后一個表達式T::size_type()生成所需返回類型的值(decltype將其轉換為返回類型)。(最后一個)逗號之前的表達式是必須成立的。在本例中就是t.size()。將表達式強制轉換為void,是為了避免由于用戶自定義重載表達式對于類型的逗號運算符而帶來的問題。
? ? ? ? 請注意,decltype的實參是一個未求值的操作數。這意味著可以在不調用構造函數的情況下創建"虛對象",請參考:
C/C++中decltype關鍵字用法總結_c++ decltype用法-CSDN博客