元编程:函数定义失败定义一个单独的函数
Metaprograming: Failure of Function Definition Defines a Separate Function
在this answer中我根据类型的is_arithmetic
定义了一个模板属性:
template<typename T> enable_if_t<is_arithmetic<T>::value, string> stringify(T t){
return to_string(t);
}
template<typename T> enable_if_t<!is_arithmetic<T>::value, string> stringify(T t){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
dyp suggests,而不是 is_arithmetic
属性 的类型,是否 to_string
被定义为模板选择标准的类型。这显然是可取的,但我不知道怎么说:
If std::to_string
is not defined then use the ostringstream
overload.
声明 to_string
条件很简单:
template<typename T> decltype(to_string(T{})) stringify(T t){
return to_string(t);
}
这与我不知道如何构建的标准相反。这显然行不通,但希望它传达了我要构建的内容:
template<typename T> enable_if_t<!decltype(to_string(T{})::value, string> (T t){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
您可以使用表达式 SFINAE 为此编写辅助特征:
namespace detail
{
//base case, to_string is invalid
template <typename T>
auto has_to_string_helper (...) //... to disambiguate call
-> false_type;
//true case, to_string valid for T
template <typename T>
auto has_to_string_helper (int) //int to disambiguate call
-> decltype(std::to_string(std::declval<T>()), true_type{});
}
//alias to make it nice to use
template <typename T>
using has_to_string = decltype(detail::has_to_string_helper<T>(0));
然后使用std::enable_if_t<has_to_string<T>::value>
template <typename...>
using void_t = void;
制作这样的类型特征非常容易:
template<typename T, typename = void>
struct has_to_string
: std::false_type { };
template<typename T>
struct has_to_string<T,
void_t<decltype(std::to_string(std::declval<T>()))>
>
: std::true_type { };
我认为有两个问题:1) 找到给定类型的所有可行算法。 2) Select 最好的一个。
例如,我们可以手动指定一组重载算法的顺序:
namespace detail
{
template<typename T, REQUIRES(helper::has_to_string(T))>
std::string stringify(choice<0>, T&& t)
{
using std::to_string;
return to_string(std::forward<T>(t));
}
template<std::size_t N>
std::string stringify(choice<1>, char const(&arr)[N])
{
return std::string(arr, N);
}
template<typename T, REQUIRES(helper::has_output_operator(T))>
std::string stringify(choice<2>, T&& t)
{
std::ostringstream o;
o << std::forward<T>(t);
return std::move(o).str();
}
}
第一个函数参数指定这些算法之间的顺序("first choice"、"second choice"、..)。为了select一个算法,我们简单地分配给最佳可行匹配:
template<typename T>
auto stringify(T&& t)
-> decltype( detail::stringify(choice<0>{}, std::forward<T>(t)) )
{
return detail::stringify(choice<0>{}, std::forward<T>(t));
}
这是如何实现的?我们从 Xeo @ Flaming Dangerzone and Paul @ void_t
"can implement concepts"? 中窃取了一点(使用简化的实现):
constexpr static std::size_t choice_max = 10;
template<std::size_t N> struct choice : choice<N+1>
{
static_assert(N < choice_max, "");
};
template<> struct choice<choice_max> {};
#include <type_traits>
template<typename T, typename = void> struct models : std::false_type {};
template<typename MF, typename... Args>
struct models<MF(Args...),
decltype(MF{}.requires_(std::declval<Args>()...),
void())>
: std::true_type {};
#define REQUIRES(...) std::enable_if_t<models<__VA_ARGS__>::value>* = nullptr
选择 类 继承自更差的选择:choice<0>
继承自 choice<1>
。因此,对于choice<0>
类型的参数,choice<0>
类型的函数参数比choice<1>
更好匹配,比choice<2>
更好匹配等等[over.ics.rank]p4.4
请注意,更专业 决胜局仅适用于两个函数都不是更好的情况。由于 choice
的总顺序,我们永远不会陷入那种情况。这可以防止调用出现歧义,即使多种算法都可行。
我们定义类型特征:
#include <string>
#include <sstream>
namespace helper
{
using std::to_string;
struct has_to_string
{
template<typename T>
auto requires_(T&& t) -> decltype( to_string(std::forward<T>(t)) );
};
struct has_output_operator
{
std::ostream& ostream();
template<typename T>
auto requires_(T&& t) -> decltype(ostream() << std::forward<T>(t));
};
}
宏可以通过使用 an idea from R. Martinho Fernandes:
来避免
template<typename T>
using requires = std::enable_if_t<models<T>::value, int>;
// exemplary application:
template<typename T, requires<helper::has_to_string(T)> = 0>
std::string stringify(choice<0>, T&& t)
{
using std::to_string;
return to_string(std::forward<T>(t));
}
首先,我认为 SFINAE 通常应该从界面中隐藏起来。它使界面混乱。将 SFINAE 远离表面,并使用标签调度来选择过载。
其次,我什至隐藏了 SFINAE 的特征 class。根据我的经验,编写 "can I do X" 代码很常见,因此我不想编写凌乱的 SFINAE 代码来完成它。所以我写了一个通用的 can_apply
特征,并且有一个特征,如果使用 decltype
.
传递了错误的类型,SFINAE 就会失败
然后我们将 SFIANE 失败的 decltype
特征提供给 can_apply
,并根据应用程序是否失败得到 true/false 类型。
这将每个 "can I do X" 特性的工作量减少到最低限度,并将有点棘手和脆弱的 SFINAE 代码从日常工作中移开。
我使用 C++1z 的 void_t
。自己实现它很容易(在这个答案的底部)。
一个类似于 can_apply
的元函数被提议用于 C++1z 中的标准化,但它不如 void_t
稳定,所以我没有使用它。
首先,一个 details
命名空间来隐藏 can_apply
的实现不被意外发现:
namespace details {
template<template<class...>class Z, class, class...>
struct can_apply:std::false_type{};
template<template<class...>class Z, class...Ts>
struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
std::true_type{};
}
然后我们可以根据 details::can_apply
编写 can_apply
,它有一个更好的接口(它不需要传递额外的 void
):
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;
以上是通用的辅助元编程代码。一旦我们有了它,我们就可以非常干净地写一个 can_to_string
traits class:
template<class T>
using to_string_t = decltype( std::to_string( std::declval<T>() ) );
template<class T>
using can_to_string = can_apply< to_string_t, T >;
我们有一个特征 can_to_string<T>
是真的当且仅当我们可以 to_string
一个 T
.
编写这样一个新特征的工作现在是 2-4 行简单代码——只需创建一个 decltype
using
别名,然后执行 can_apply
测试一下。
一旦我们有了它,我们就使用标签分派到正确的实现:
template<typename T>
std::string stringify(T t, std::true_type /*can to string*/){
return std::to_string(t);
}
template<typename T>
std::string stringify(T t, std::false_type /*cannot to string*/){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
template<typename T>
std::string stringify(T t){
return stringify(t, can_to_string<T>{});
}
所有丑陋的代码都隐藏在 details
命名空间中。
如果您需要 void_t
,请使用:
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
适用于大多数主要 C++11 编译器。
请注意,更简单的 template<class...>using void_t=void;
无法在某些较旧的 C++11 编译器中工作(标准中存在歧义)。
在上周的委员会会议上新投票给图书馆基础知识 TS:
template<class T>
using to_string_t = decltype(std::to_string(std::declval<T>()));
template<class T>
using has_to_string = std::experimental::is_detected<to_string_t, T>;
然后在 has_to_string
上标记 dispatch and/or SFINAE,随心所欲。
is_detected
如何实现可以咨询the current working draft of the TS和朋友们。它与@Yakk 的回答中的 can_apply
非常相似。
好吧,您可以跳过所有元编程魔术并使用 fit::conditional
adaptor from the Fit 库:
FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
[](auto x) -> decltype(to_string(x))
{
return to_string(x);
},
[](auto x) -> decltype(static_cast<ostringstream&>(ostringstream() << x).str())
{
return static_cast<ostringstream&>(ostringstream() << x).str();
}
);
或者更紧凑,如果你不介意宏:
FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
[](auto x) FIT_RETURNS(to_string(x)),
[](auto x) FIT_RETURNS(static_cast<ostringstream&>(ostringstream() << x).str())
);
注意,我也限制了第二个函数,所以如果类型不能用 to_string
调用也不能流式传输到 ostringstream
那么函数就不能被调用。这有助于在检查类型要求时提供更好的错误消息和更好的可组合性。
我的看法:在不为每个人设置冗长的类型特征,或使用实验性功能或长代码的情况下,普遍确定某事是否可调用:
template<typename Callable, typename... Args, typename = decltype(declval<Callable>()(declval<Args>()...))>
std::true_type isCallableImpl(Callable, Args...) { return {}; }
std::false_type isCallableImpl(...) { return {}; }
template<typename... Args, typename Callable>
constexpr bool isCallable(Callable callable) {
return decltype(isCallableImpl(callable, declval<Args>()...)){};
}
用法:
constexpr auto TO_STRING_TEST = [](auto in) -> decltype(std::to_string(in)) { return {}; };
constexpr bool TO_STRING_WORKS = isCallable<Input>(TO_STRING_TEST);
我发现 C++20 的 concepts
易于阅读。我们可以这样写:
#include<concepts>
template<typename T>
concept has_to_string = requires (T a){ std::to_string(a);};
template<typename T>
auto stringify(T a){
return "Doesn't have to_string";
}
template<has_to_string T>
auto stringify(T a){
return "Has to_string";
}
我们可以这样测试它:
int main()
{
int a;
int b[2];
std::cout<<stringify(a); // Has to_string
std::cout<<stringify(b); // Doesn't have to_string
}
编译器 GCC 10.2 标志 -std=c++20
。
在this answer中我根据类型的is_arithmetic
定义了一个模板属性:
template<typename T> enable_if_t<is_arithmetic<T>::value, string> stringify(T t){
return to_string(t);
}
template<typename T> enable_if_t<!is_arithmetic<T>::value, string> stringify(T t){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
dyp suggests,而不是 is_arithmetic
属性 的类型,是否 to_string
被定义为模板选择标准的类型。这显然是可取的,但我不知道怎么说:
If
std::to_string
is not defined then use theostringstream
overload.
声明 to_string
条件很简单:
template<typename T> decltype(to_string(T{})) stringify(T t){
return to_string(t);
}
这与我不知道如何构建的标准相反。这显然行不通,但希望它传达了我要构建的内容:
template<typename T> enable_if_t<!decltype(to_string(T{})::value, string> (T t){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
您可以使用表达式 SFINAE 为此编写辅助特征:
namespace detail
{
//base case, to_string is invalid
template <typename T>
auto has_to_string_helper (...) //... to disambiguate call
-> false_type;
//true case, to_string valid for T
template <typename T>
auto has_to_string_helper (int) //int to disambiguate call
-> decltype(std::to_string(std::declval<T>()), true_type{});
}
//alias to make it nice to use
template <typename T>
using has_to_string = decltype(detail::has_to_string_helper<T>(0));
然后使用std::enable_if_t<has_to_string<T>::value>
template <typename...>
using void_t = void;
制作这样的类型特征非常容易:
template<typename T, typename = void>
struct has_to_string
: std::false_type { };
template<typename T>
struct has_to_string<T,
void_t<decltype(std::to_string(std::declval<T>()))>
>
: std::true_type { };
我认为有两个问题:1) 找到给定类型的所有可行算法。 2) Select 最好的一个。
例如,我们可以手动指定一组重载算法的顺序:
namespace detail
{
template<typename T, REQUIRES(helper::has_to_string(T))>
std::string stringify(choice<0>, T&& t)
{
using std::to_string;
return to_string(std::forward<T>(t));
}
template<std::size_t N>
std::string stringify(choice<1>, char const(&arr)[N])
{
return std::string(arr, N);
}
template<typename T, REQUIRES(helper::has_output_operator(T))>
std::string stringify(choice<2>, T&& t)
{
std::ostringstream o;
o << std::forward<T>(t);
return std::move(o).str();
}
}
第一个函数参数指定这些算法之间的顺序("first choice"、"second choice"、..)。为了select一个算法,我们简单地分配给最佳可行匹配:
template<typename T>
auto stringify(T&& t)
-> decltype( detail::stringify(choice<0>{}, std::forward<T>(t)) )
{
return detail::stringify(choice<0>{}, std::forward<T>(t));
}
这是如何实现的?我们从 Xeo @ Flaming Dangerzone and Paul @ void_t
"can implement concepts"? 中窃取了一点(使用简化的实现):
constexpr static std::size_t choice_max = 10;
template<std::size_t N> struct choice : choice<N+1>
{
static_assert(N < choice_max, "");
};
template<> struct choice<choice_max> {};
#include <type_traits>
template<typename T, typename = void> struct models : std::false_type {};
template<typename MF, typename... Args>
struct models<MF(Args...),
decltype(MF{}.requires_(std::declval<Args>()...),
void())>
: std::true_type {};
#define REQUIRES(...) std::enable_if_t<models<__VA_ARGS__>::value>* = nullptr
选择 类 继承自更差的选择:choice<0>
继承自 choice<1>
。因此,对于choice<0>
类型的参数,choice<0>
类型的函数参数比choice<1>
更好匹配,比choice<2>
更好匹配等等[over.ics.rank]p4.4
请注意,更专业 决胜局仅适用于两个函数都不是更好的情况。由于 choice
的总顺序,我们永远不会陷入那种情况。这可以防止调用出现歧义,即使多种算法都可行。
我们定义类型特征:
#include <string>
#include <sstream>
namespace helper
{
using std::to_string;
struct has_to_string
{
template<typename T>
auto requires_(T&& t) -> decltype( to_string(std::forward<T>(t)) );
};
struct has_output_operator
{
std::ostream& ostream();
template<typename T>
auto requires_(T&& t) -> decltype(ostream() << std::forward<T>(t));
};
}
宏可以通过使用 an idea from R. Martinho Fernandes:
来避免template<typename T>
using requires = std::enable_if_t<models<T>::value, int>;
// exemplary application:
template<typename T, requires<helper::has_to_string(T)> = 0>
std::string stringify(choice<0>, T&& t)
{
using std::to_string;
return to_string(std::forward<T>(t));
}
首先,我认为 SFINAE 通常应该从界面中隐藏起来。它使界面混乱。将 SFINAE 远离表面,并使用标签调度来选择过载。
其次,我什至隐藏了 SFINAE 的特征 class。根据我的经验,编写 "can I do X" 代码很常见,因此我不想编写凌乱的 SFINAE 代码来完成它。所以我写了一个通用的 can_apply
特征,并且有一个特征,如果使用 decltype
.
然后我们将 SFIANE 失败的 decltype
特征提供给 can_apply
,并根据应用程序是否失败得到 true/false 类型。
这将每个 "can I do X" 特性的工作量减少到最低限度,并将有点棘手和脆弱的 SFINAE 代码从日常工作中移开。
我使用 C++1z 的 void_t
。自己实现它很容易(在这个答案的底部)。
一个类似于 can_apply
的元函数被提议用于 C++1z 中的标准化,但它不如 void_t
稳定,所以我没有使用它。
首先,一个 details
命名空间来隐藏 can_apply
的实现不被意外发现:
namespace details {
template<template<class...>class Z, class, class...>
struct can_apply:std::false_type{};
template<template<class...>class Z, class...Ts>
struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:
std::true_type{};
}
然后我们可以根据 details::can_apply
编写 can_apply
,它有一个更好的接口(它不需要传递额外的 void
):
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;
以上是通用的辅助元编程代码。一旦我们有了它,我们就可以非常干净地写一个 can_to_string
traits class:
template<class T>
using to_string_t = decltype( std::to_string( std::declval<T>() ) );
template<class T>
using can_to_string = can_apply< to_string_t, T >;
我们有一个特征 can_to_string<T>
是真的当且仅当我们可以 to_string
一个 T
.
编写这样一个新特征的工作现在是 2-4 行简单代码——只需创建一个 decltype
using
别名,然后执行 can_apply
测试一下。
一旦我们有了它,我们就使用标签分派到正确的实现:
template<typename T>
std::string stringify(T t, std::true_type /*can to string*/){
return std::to_string(t);
}
template<typename T>
std::string stringify(T t, std::false_type /*cannot to string*/){
return static_cast<ostringstream&>(ostringstream() << t).str();
}
template<typename T>
std::string stringify(T t){
return stringify(t, can_to_string<T>{});
}
所有丑陋的代码都隐藏在 details
命名空间中。
如果您需要 void_t
,请使用:
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
适用于大多数主要 C++11 编译器。
请注意,更简单的 template<class...>using void_t=void;
无法在某些较旧的 C++11 编译器中工作(标准中存在歧义)。
在上周的委员会会议上新投票给图书馆基础知识 TS:
template<class T>
using to_string_t = decltype(std::to_string(std::declval<T>()));
template<class T>
using has_to_string = std::experimental::is_detected<to_string_t, T>;
然后在 has_to_string
上标记 dispatch and/or SFINAE,随心所欲。
is_detected
如何实现可以咨询the current working draft of the TS和朋友们。它与@Yakk 的回答中的 can_apply
非常相似。
好吧,您可以跳过所有元编程魔术并使用 fit::conditional
adaptor from the Fit 库:
FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
[](auto x) -> decltype(to_string(x))
{
return to_string(x);
},
[](auto x) -> decltype(static_cast<ostringstream&>(ostringstream() << x).str())
{
return static_cast<ostringstream&>(ostringstream() << x).str();
}
);
或者更紧凑,如果你不介意宏:
FIT_STATIC_LAMBDA_FUNCTION(stringify) = fit::conditional(
[](auto x) FIT_RETURNS(to_string(x)),
[](auto x) FIT_RETURNS(static_cast<ostringstream&>(ostringstream() << x).str())
);
注意,我也限制了第二个函数,所以如果类型不能用 to_string
调用也不能流式传输到 ostringstream
那么函数就不能被调用。这有助于在检查类型要求时提供更好的错误消息和更好的可组合性。
我的看法:在不为每个人设置冗长的类型特征,或使用实验性功能或长代码的情况下,普遍确定某事是否可调用:
template<typename Callable, typename... Args, typename = decltype(declval<Callable>()(declval<Args>()...))>
std::true_type isCallableImpl(Callable, Args...) { return {}; }
std::false_type isCallableImpl(...) { return {}; }
template<typename... Args, typename Callable>
constexpr bool isCallable(Callable callable) {
return decltype(isCallableImpl(callable, declval<Args>()...)){};
}
用法:
constexpr auto TO_STRING_TEST = [](auto in) -> decltype(std::to_string(in)) { return {}; };
constexpr bool TO_STRING_WORKS = isCallable<Input>(TO_STRING_TEST);
我发现 C++20 的 concepts
易于阅读。我们可以这样写:
#include<concepts>
template<typename T>
concept has_to_string = requires (T a){ std::to_string(a);};
template<typename T>
auto stringify(T a){
return "Doesn't have to_string";
}
template<has_to_string T>
auto stringify(T a){
return "Has to_string";
}
我们可以这样测试它:
int main()
{
int a;
int b[2];
std::cout<<stringify(a); // Has to_string
std::cout<<stringify(b); // Doesn't have to_string
}
编译器 GCC 10.2 标志 -std=c++20
。