std::is_detected 和 Detection Idoms
本文鎖定於 C++17 範圍內談實作。
關於 std::is_detected
確切地說,是指 std::experimental::is_detected
, std::experimental::detected_t
, std::experimental::detected_or
。因為尚未被納入正式庫,所以在現行的編譯器中,它們通常至少需要 C++17 規範指定,幷包含專門的標頭檔案 <experimental/type_traits>
。參考這裡:cppref。
但是編譯器的支援也是參差不齊。所以我們一般都採用自定義的版本,同樣需要 C++17 規範(如果需要更低規範適配版本,請自行搜尋),但表現能力更可靠、更可預測:
#if !defined(__TRAITS_VOIT_T_DEFINED)
#define __TRAITS_VOIT_T_DEFINED
// ------------------------- void_t
namespace cmdr::traits {
#if (__cplusplus > 201402L)
using std::void_t; // C++17 or later
#else
// template<class...>
// using void_t = void;
template<typename... T>
struct make_void { using type = void; };
template<typename... T>
using void_t = typename make_void<T...>::type;
#endif
} // namespace cmdr::traits
#endif // __TRAITS_VOIT_T_DEFINED
#if !defined(__TRAITS_IS_DETECTED_DEFINED)
#define __TRAITS_IS_DETECTED_DEFINED
// ------------------------- is_detected
namespace cmdr::traits {
template<class, template<class> class, class = void_t<>>
struct detect : std::false_type {};
template<class T, template<class> class Op>
struct detect<T, Op, void_t<Op<T>>> : std::true_type {};
template<class T, class Void, template<class...> class Op, class... Args>
struct detector {
using value_t = std::false_type;
using type = T;
};
template<class T, template<class...> class Op, class... Args>
struct detector<T, void_t<Op<Args...>>, Op, Args...> {
using value_t = std::true_type;
using type = Op<Args...>;
};
struct nonesuch final {
nonesuch() = delete;
~nonesuch() = delete;
nonesuch(const nonesuch &) = delete;
void operator=(const nonesuch &) = delete;
};
template<class T, template<class...> class Op, class... Args>
using detected_or = detector<T, void, Op, Args...>;
template<class T, template<class...> class Op, class... Args>
using detected_or_t = typename detected_or<T, Op, Args...>::type;
template<template<class...> class Op, class... Args>
using detected = detected_or<nonesuch, Op, Args...>;
template<template<class...> class Op, class... Args>
using detected_t = typename detected<Op, Args...>::type;
/**
* @brief another std::is_detected
* @details For example:
* @code{c++}
* template<typename T>
* using copy_assign_op = decltype(std::declval<T &>() = std::declval<const T &>());
*
* template<typename T>
* using is_copy_assignable = is_detected<copy_assign_op, T>;
*
* template<typename T>
* constexpr bool is_copy_assignable_v = is_copy_assignable<T>::value;
* @endcode
*/
template<template<class...> class Op, class... Args>
using is_detected = typename detected<Op, Args...>::value_t;
template<template<class...> class Op, class... Args>
constexpr bool is_detected_v = is_detected<Op, Args...>::value;
template<class T, template<class...> class Op, class... Args>
using is_detected_exact = std::is_same<T, detected_t<Op, Args...>>;
template<class To, template<class...> class Op, class... Args>
using is_detected_convertible = std::is_convertible<detected_t<Op, Args...>, To>;
} // namespace cmdr::traits
#endif // __TRAITS_IS_DETECTED_DEFINED
當然,也涉及到 std::void_t,也是 C++17 才進入標準庫的。但你可以自己宣告,如同上面的 VOID_T 部分一樣。
但 is_detected 的低版本相容部分我們就放棄了,太長了,我也不想加多單元測試負擔。
它的使用方式是這樣的:
#include <type_traits>
#include <string>
template<typename T>
using copy_assign_op = decltype(std::declval<T &>() = std::declval<const T &>());
template<typename T>
using is_copy_assignable = is_detected<copy_assign_op, T>;
template<typename T>
constexpr bool is_copy_assignable_v = is_copy_assignable<T>::value;
struct foo {};
struct bar {
bar &operator=(const bar &) = delete;
};
int main() {
static_assert(is_copy_assignable_v<foo>, "foo is copy assignable");
static_assert(!is_copy_assignable_v<bar>, "bar is not copy assignable");
return 0;
}
可以看到這是一種典型的 detection idioms 慣用法,能夠在編譯期測試出一個型別具有什麼特性。例如 is_chrono_duration, is_iterator, is_integer 等等,在標準庫中有大量預定義的檢測專用的 traits。
但在實際生活中我們通常還需要定義自己的。例如在我們的 undo-cxx 中就有一組:
namespace undo_cxx {
template<typename State,
typename Context,
typename BaseCmdT,
template<class S, class B> typename RefCmdT,
typename Cmd>
class undoable_cmd_system_t {
public:
~undoable_cmd_system_t() = default;
using StateT = State;
using ContextT = Context;
using CmdT = Cmd;
using CmdSP = std::shared_ptr<CmdT>;
using Memento = typename CmdT::Memento;
using MementoPtr = typename std::unique_ptr<Memento>;
using Container = std::list<MementoPtr>;
using Iterator = typename Container::iterator;
using size_type = typename Container::size_type;
template<typename T, typename = void>
struct has_save_state : std::false_type {};
template<typename T>
struct has_save_state<T, decltype(void(std::declval<T &>().save_state()))> : std::true_type {};
template<typename T, typename = void>
struct has_undo : std::false_type {};
template<typename T>
struct has_undo<T, decltype(void(std::declval<T &>().undo()))> : std::true_type {};
template<typename T, typename = void>
struct has_redo : std::false_type {};
template<typename T>
struct has_redo<T, decltype(void(std::declval<T &>().redo()))> : std::true_type {};
template<typename T, typename = void>
struct has_can_be_memento : std::false_type {};
template<typename T>
struct has_can_be_memento<T, decltype(void(std::declval<T &>().can_be_memento()))> : std::true_type {};
public:
// ...
void undo(CmdSP &undo_cmd) {
if constexpr (has_undo<CmdT>::value) {
// needs void undo_cmd::undo(sender, ctx, delta)
undo_cmd->undo(undo_cmd, _ctx, 1);
return;
}
if (undo_one()) {
// undo ok
}
}
// ...
};
}
你可能注意到這個例子中壓根沒有用到 is_detected。
確實如此,Detection Idioms 包含一系列手法,並不是一定要用到哪一個工具模板,重點還是在於目標,它們都是為了在編譯期測試出某個型別的特性,以便針對性地進行特化、偏特化,或者用於完成其他任務。
所以這是個巨大無比的話題。
Detection Idioms
在提案 [WG21 N4436 - Proposing Standard Library Support for the C++ Detection Idiom [pdf]](http://open-std.org/JTC1/SC22...) 中,檢測慣用法被稱作 Detection Idiom。這個提案是在 C++20 中被加入,一部分原因在於它可以成為一個補全性的方案,另一方面則是因為 Concepts 一直提了十幾年卻都沒有定論。當然最後我們知道了,去年 C++20 定案之後 concepts 終於被加入了。
但是可以預見的是,未來直到 2023 年,工程當中使用 concepts 的可能性還是基本上為零。事實上 2023 年 C++17 如能成為工程應用主流的話就阿彌陀佛了,多數工程還是 C++11 的,而且那些遺留專案連 C++11 都不用呢。
作為一個提案的代表,Detection Idiom 是一個專有名詞。但檢測慣用法,卻是早已有之。或者說,traits 本來就是幹這個的。在本文中的檢測慣用法,會包含型別測試與約束,以及函式簽名檢測等等檢測方法。
在 C++11 之後,藉助於 SFINAE 我們有幾種選擇來完成型別約束與選擇:特化方式,新增 enable_if 測試的特化方式,藉助於 is_detected 的約束力。
普通的特化方式
模板引數的特化能力,對於一般情況的約束就是足夠的:
template<typename T>
bool max(T l, T r) { return l > r ? l : r; }
bool max(bool l, bool r) { return false; }
bool max(float l, float r) {
return (l+0.000005f > r) ? l : (r+0.000005f>l) ? r : IS_NAN;
}
上面的例子沒有實際用處,只是用來展示特化的直接使用效果。
對於簡單的型別來說,這種特化能力就已經足夠了。不過遇到複雜的型別,特別是複合型別它的能力就比較短板了。
所以這種情況下我們需要藉助於 enable_if 來進行約束。
std::enable_if 方式
std::enable_if 可能的實現方法
std::enable_if 的實現方法可以比較簡單:
template <bool, typename T=void>
struct enable_if {};
template <typename T>
struct enable_if<true, T> {
using type = T;
};
約束返回型別
對於函式返回型別來說,用法略有不同:
#include <iostream>
#include <type_traits>
class foo;
class bar;
template<class T>
struct is_bar {
template<class Q = T>
typename std::enable_if<std::is_same<Q, bar>::value, bool>::type check() { return true; }
template<class Q = T>
typename std::enable_if<!std::is_same<Q, bar>::value, bool>::type check() { return false; }
};
int main() {
is_bar<foo> foo_is_bar;
is_bar<bar> bar_is_bar;
if (!foo_is_bar.check() && bar_is_bar.check())
std::cout << "It works!" << std::endl;
return 0;
}
這是測試型別並返回 bool 的用例,也是較為典型的如何編寫 traits 的用例。
不過為了真正說明函式返回型別模板化,還是要下面這個例子:
#include <iostream>
#include <type_traits>
namespace AAA {
template<class T>
class Y {
public:
template<typename Q = T>
typename std::enable_if<std::is_same<Q, double>::value || std::is_same<Q, float>::value, Q>::type foo() {
return 11;
}
template<typename Q = T>
typename std::enable_if<!std::is_same<Q, double>::value && !std::is_same<Q, float>::value, Q>::type foo() {
return 7;
}
};
} // namespace
int main(){
#define TestQ(typ) std::cout << "T foo() : " << (AAA::Y<typ>{}).foo() << '\n'
TestQ(short);
TestQ(int);
TestQ(long);
TestQ(bool);
TestQ(float);
TestQ(double);
}
輸出為:
T foo() : 7
T foo() : 7
T foo() : 7
T foo() : 1
T foo() : 11
T foo() : 11
這是一個實用化的用例,可以直接檢測 double 或者 float 型別。
在 traits 中使用
測試一個模板中是否有名為 value 的型別定義:
template <typename T, typename=void>
struct has_typed_value;
template <typename T>
struct has_typed_value<T, typename std::enable_if<T::value>::type> {
static constexpr bool value = T::value;
};
template<class T>
inline constexpr bool has_typed_value_v = has_typed_value<T>::value;
static_assert(has_typed_value<std::is_same<bool, bool>>::value, "std::is_same<bool, bool>::value is valid");
static_assert(has_typed_value_v<std::is_same<bool, bool>>, "std::is_same<bool, bool>::value is valid");
類似地:
template <typename T> struct has_typed_type;
template <typename T>
struct has_typed_type<T, typename std::enable_if<T::value>::type> {
static constexpr bool value = T::type;
};
template<class T>
inline constexpr bool has_typed_type_v = has_typed_type<T>::value;
使用 conjuction
C++17 之後就可以直接使用 std::conjuction
,它可以用於組合一組 detectors。
這裡只給出一個 sample 片段:
template <class T>
using is_regular = std::conjunction<std::is_default_constructible<T>,
std::is_copy_constructible<T>,
supports_equality<T,T>,
supports_inequality<T,T>, //assume impl
supports_less_than<T,T>>; //ditto
更多的
檢測成員函式存在性
declval 方式
這個用例比較獨立自主,什麼都自己來,自己定義了 void_t,自己定義了名為 supports_foo 的 traits,目的是為了檢測型別 T 是不是有 T::get_foo() 函式簽名的存在。最後,calculate_foo_factor() 的特化目的就顯而易見,無需解釋了。
template <class... Ts>
using void_t = void;
template <class T, class=void>
struct supports_foo : std::false_type{};
template <class T>
struct supports_foo<T, void_t<decltype(std::declval<T>().get_foo())>>
: std::true_type{};
template <class T,
std::enable_if_t<supports_foo<T>::value>* = nullptr>
auto calculate_foo_factor (const T& t) {
return t.get_foo();
}
template <class T,
std::enable_if_t<!supports_foo<T>::value>* = nullptr>
int calculate_foo_factor (const T& t) {
// insert generic calculation here
return 42;
}
它採用了 declval 的偽造例項的技術,對此可以參考我們的 std::declval 和 decltype 一文。
Is_detected 方式
改用 is_detected 方式:
template<typename T>
using to_string_t = decltype(std::declval<T &>().to_string());
template<typename T>
constexpr bool has_to_string = is_detected_v<to_string_t, T>;
struct AA {
std::string to_string() const { return ""; }
};
struct BB{};
static_assert(has_to_string<AA>, "");
static_assert(!has_to_string<BB>, "");
這個沒什麼好說的,你可以用 std::experimental::is_detected,也可以使用我們前文中定義的 is_detected 工具。
作為一個補充
在沒有 std::enable_if 的時候,需要藉助於 struct char[] 方式,這種技巧被稱作 Member Detector,是 C++11 之前的經典慣用法:
template<typename T>
class DetectX
{
struct Fallback { int X; }; // add member name "X"
struct Derived : T, Fallback { };
template<typename U, U> struct Check;
typedef char ArrayOfOne[1]; // typedef for an array of size one.
typedef char ArrayOfTwo[2]; // typedef for an array of size two.
template<typename U>
static ArrayOfOne & func(Check<int Fallback::*, &U::X> *);
template<typename U>
static ArrayOfTwo & func(...);
public:
typedef DetectX type;
enum { value = sizeof(func<Derived>(0)) == 2 };
};
而且還有配套的巨集定義 GENERATE_HAS_MEMBER(member)
,所以在應用程式碼中只需要:
GENERATE_HAS_MEMBER(att) // Creates 'has_member_att'.
GENERATE_HAS_MEMBER(func) // Creates 'has_member_func'.
std::cout << std::boolalpha
<< "\n" "'att' in 'C' : "
<< has_member_att<C>::value // <type_traits>-like interface.
<< "\n" "'func' in 'C' : "
<< has_member_func<C>() // Implicitly convertible to 'bool'.
<< "\n";
也是很瘋狂。
進一步延伸
上一節提供的技術,僅僅對函式簽名中的函式名本身進行檢測。有時候,可能我們會在想對形參表或者返回型別做檢測並約束。有可能嗎?
確實是有的。
抽出函式返回型別
return_type_of_t<Callable>
可以抽出函式的返回型別:
namespace AA1 {
template<typename Callable>
using return_type_of_t =
typename decltype(std::function{std::declval<Callable>()})::result_type;
int foo(int a, int b, int c, int d) {
return 1;
}
auto bar = [](){ return 1; };
struct baz_ {
double operator()(){ return 0; }
} baz;
void test_aa1() {
using ReturnTypeOfFoo = return_type_of_t<decltype(foo)>;
using ReturnTypeOfBar = return_type_of_t<decltype(bar)>;
using ReturnTypeOfBaz = return_type_of_t<decltype(baz)>;
// ...
}
}
在這個基礎上你就可以運用 enable_if 或者 is_detected 了。具體檢測略略略……
檢測函式形參表
至於形參表的檢測問題,有點而麻煩,而且這一話題也很大,方向較多,所以我暫且給出一個方向供你參考,其他方向也就是萬變不離其宗了。
bar_t 可以以 variadic 引數的方式羅列出型別表,並用來檢測型別 T 是不是有一個函式 bar() 而且還有相應的形參表:
template<class T, typename... Arguments>
using bar_t = std::conditional_t<
true,
decltype(std::declval<T>().bar(std::declval<Arguments>()...)),
std::integral_constant<
decltype(std::declval<T>().bar(std::declval<Arguments>()...)) (T::*)(Arguments...),
&T::bar>>;
struct foo1 {
int const &bar(int &&) {
static int vv_{0};
return vv_;
}
};
static_assert(dp::traits::is_detected_v<bar_t, foo1, int &&>, "not detected");
暫時我沒有想法將它通用化,所以你需要具體情況具體拷貝和修改。
誰要是有改進版,不妨通知我。
檢測 begin() 存在性並呼叫它
前文對成員函式存在性已經介紹過了,這裡是一個實用的片段:檢測成員函式存在與否,存在的話就呼叫它,否則的話呼叫我們的備用實現方案:
// test for `any begin() const`
template <typename T>
using begin_op = decltype(std::declval<T const&>().begin());
struct A_Container {
template <typename T>
void invoke_begin(T const& t){
if constexpr(std::experimental::is_detected_v<begin_op, T>){
t.begin();
}else{
my_begin(); // begin() not exists!!
}
}
iterator my_begin() { ... }
};
為什麼不直接使用 SFINAE 技術呢?
因為 SFINAE 技術能夠讓我們做出 invoke_begin 的特化版本,但這有可能阻礙了我們的進一步擴充性。SFINAE 僅能在型別不匹配時起作用,但採用 begin_op 的方式我們可以:針對返回型別,針對形參表,針對 const or not,等等。
檢測 emplace(Args &&...)
這要用到我們前面的 bar_t 技法:
template<class T, typename... Arguments>
using emplace_variadic_t = std::conditional_t<
true,
decltype(std::declval<T>().emplace(std::declval<Arguments>()...)),
std::integral_constant<
decltype(std::declval<T>().emplace(std::declval<Arguments>()...)) (T::*)(Arguments...),
&T::emplace>>;
/**
* @brief test member function `emplace()` with variadic params
* @tparam T
* @tparam Arguments
* @details For example:
* @code{c++}
* using C = std::list<int>;
* static_assert(has_emplace_variadic_v<C, C::const_iterator, int &&>);
* @endcode
*/
template<class T, typename... Arguments>
constexpr bool has_emplace_variadic_v = is_detected_v<emplace_variadic_t, T, Arguments...>;
namespace detail {
using C = std::list<int>;
static_assert(has_emplace_variadic_v<C, C::const_iterator, int &&>);
} // namespace detail
More...
在 cmdr-cxx 中有一組 detection traits,是用於檢測標準庫容器函式簽名的。在 undo-cxx 中的 undoable_cmd_system_t<State>
中我們運用了檢測函式名存在性並呼叫它的技法。
關於 _t
和 _v
在標準庫和標準化推行中,_t
和 _v
字尾隱含著直接提供 ::type
或者 ::value
成員的意思。
但這並不影響我(們)慣於使用 _t
表示一個模板類。
在一個模板類被用做基礎類、被當成工具類使用時,我(們)喜歡為其增加 _t
字尾。
Refs
- [WG21 N4436 - Proposing Standard Library Support for the C++ Detection Idiom [pdf]](http://open-std.org/JTC1/SC22...) - Walter E. Brown 計劃在C++標準庫中加入 Detection 慣用法
- WG21 N4502: PDF - Proposing Standard Library Support for the C++ Detection Idiom, v2
- std::experimental::is_detected, std::experimental::detected_t, std::experimental::detected_or - cppreference.com
- std::void_t - cppreference.com
- std::enable_if - cppreference.com
- Detection Idiom - A Stopgap for Concepts
後記
檢測慣用法實在是太大了,本文只是導引,將基本工具提供出來。後續未來應該會繼續就此問題介紹我的經驗。