Почему этот SFINAE-фрагмент не работает в g ++, но работает в MSVC?
В MSVC2017 это работает нормально, оба static_asserts НЕ запускаются, как ожидалось:
template <typename T>
struct do_have_size {
template <typename = decltype(std::declval<T>().size())>
static std::true_type check(T);
static std::false_type check(...);
using type = decltype(check(std::declval<T>()));
};
int main() {
using TR = typename do_have_size<std::vector<int>>::type;
using FL = typename do_have_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}
Однако, если я скомпилирую в g++ 7.1 или clang 4.0, я получаю следующую ошибку компилятора:
In instantiation of 'struct do_have_size<int>':
20:39: required from here
9:24: error: request for member 'size' in 'declval<do_have_size<int>::TP>()', which is of non-class type 'int'
По моему пониманию SFINAE, подстановка функции возврата true_type
должна прерываться для параметра int
, и следующая функция должна быть выбрана, как это сделано в MSVC. Почему clang и g++ вообще не компилируют его?
Я скомпилирован только с -std=c++17
, возможно, что-то еще нужно?
Ответы
Ответ 1
Это абсолютно не имеет никакого отношения к тому, являются ли аргументы шаблона по умолчанию частью сигнатуры шаблона функции.
Реальная проблема заключается в том, что T
является параметром шаблона класса, и когда вы создаете экземпляр определения шаблона класса, реализация может немедленно заменить его на аргумент шаблона по умолчанию, decltype(std::declval<T>().size())
вне вывода аргумента шаблона, что вызывает жесткая ошибка, если size
нет.
Исправление прост; просто заставьте его зависеть от параметра шаблона функции.
template <typename U, typename = decltype(std::declval<U>().size())>
static std::true_type check(U);
(Существуют и другие проблемы с вашей реализацией, например, для этого требуется конструктивный конструктор, не содержащий абстрактных T
, и не требует, чтобы size()
был const-callable, но они не являются причиной ошибки вы видите.)
Ответ 2
SFINAE здесь не работает, поскольку класс уже создан с T = int
в do_have_size<int>::type
. SFINAE работает только для списка кандидатов в функции шаблона, в вашем случае вы получите жесткую ошибку, поскольку в экземпляре
do_have_size<int>::type
функция-член
template <typename = decltype(std::declval<int>().size())>
static std::true_type check(T);
безусловно, плохо сформирован для int
.
static std::false_type check(...);
никогда не будет рассмотрен. Таким образом, gcc находится прямо здесь, отклоняя ваш код, и MSVC2017 не должен принимать код.
Связано: std:: enable_if: параметр vs параметр шаблона и SFINAE работает в обратном типе, но не как параметр шаблона
Одним из решений является использование магии void_t
(начиная с С++ 17, но вы может определить ваш собственный в С++ 11/14), который отображает список всех типов на void
и позволяет использовать сумасшедшие простые, простые методы SFINAE, например
#include <utility>
#include <vector>
template<typename...>
using void_t = void; // that how void_t is defined in C++17
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, void_t<decltype(std::declval<T>().size())>>
: std::true_type {};
int main() {
using TR = typename has_size<std::vector<int>>::type;
using FL = typename has_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}
Live on Wandbox
Здесь - это видео Cppcon от Уолтера Брауна, в котором подробно описываются методы void_t
, я очень рекомендую!
Ответ 3
@vsoftco ответил: "gcc прав, чтобы отклонить ваш код". Я согласен.
Чтобы исправить это, я говорю:
namespace details {
template<template<class...>class Z, class, class...Ts>
struct can_apply:std::false_type{};
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t = typename voider<Ts...>::type;
template<template<class...>class Z, class...Ts>
struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z,void,Ts...>;
Это библиотека can_apply
, которая делает этот вид SFINAE простым.
Теперь написать одну из этих черт так же просто, как:
template<class T>
using dot_size_r = decltype( std::declval<T>().size() );
template<class T>
using has_dot_size = can_apply< dot_size_r, T >;
тестовый код:
int main() {
static_assert( has_dot_size<std::vector<int>>{}, "TRUE" );
static_assert( !has_dot_size<int>{}, "FALSE" );
}
Живой пример.
В С++ 17 вы можете перейти к меньшему количеству объявленных значений.
#define RETURNS(...) \
noexcept(noexcept(__VA_ARGS__)) \
-> decltype(__VA_ARGS__) \
{ return __VA_ARGS__; }
template<class F>
constexpr auto can_invoke(F&&) {
return [](auto&&...args) {
return std::is_invocable< F(decltype(args)...) >{};
};
}
can_invoke
принимает функцию f
и возвращает "тестер-запрос". Тестер invokation принимает аргументы, а затем возвращает true_type
, если эти аргументы будут действительны для перехода на f
и false_type
в противном случае.
RETURNS
позволяет легко сделать однозадачную лямбда-SFINAE дружественной. И в С++ 17, лямбда-операции являются constexpr, если это возможно (вот почему нам нужен С++ 17 здесь).
Тогда это дает нам:
template<class T>
constexpr auto can_dot_size(T&& t) {
return can_invoke([](auto&& x) RETURNS(x.size()))(t);
}
Теперь мы часто это делаем, потому что хотим по возможности называть .size()
и в противном случае возвращать 0.
template<class T, class A, class...Bs>
decltype(auto) call_first_valid(T&& t, A&& a, Bs&&...bs) {
if constexpr( can_invoke(std::forward<A>(a))(std::forward<T>(t)) ) {
return std::forward<A>(a)(std::forward<T>(t));
else
return call_first_valid(std::forward<T>(t), std::forward<Bs>(bs)...);
}
теперь мы можем
template<class T>
std::size_t size_at_least( T&& t ) {
return call_first_valid( std::forward<T>(t),
[](auto&& t) RETURNS(t.size()),
[](auto&&)->std::size_t { return 0; }
);
}
Как это бывает, @Barry предложил функцию в С++ 20, которая заменяет [](auto&& f) RETURNS(f.size())
на [](auto&& f)=>f.size()
(и более).
Ответ 4
Я получил его для работы с помощью std:: enable_if для SFINAE пропустить версию проверки шаблона на основе либо параметра, либо возврата тип. Условием, которое я использовал, было std:: is_fundamental, чтобы исключить использование экземпляров int, float и других неклассов при создании экземпляра шаблона. Я использовал флаг -std=c++1z
для clang и gcc. Я ожидаю, что -std=c++14
тоже будет работать.
#include <type_traits>
#include <utility>
#include <vector>
template <typename T>
struct do_have_size {
static std::false_type check(...);
template <typename U = T, typename = decltype(std::declval<U>().size())>
static std::true_type check(std::enable_if_t<!std::is_fundamental<U>::value, U>);
// OR
//template <typename U = T, typename = decltype(std::declval<U>().size())>
//static auto check(U)
// -> std::enable_if_t<!std::is_fundamental<U>::value, std::true_type>;
using type = decltype(check(std::declval<T>()));
};
int main() {
using TR = typename do_have_size<std::vector<int>>::type;
using FL = typename do_have_size<int>::type;
static_assert(std::is_same<TR, std::true_type>::value, "TRUE");
static_assert(std::is_same<FL, std::false_type>::value, "FALSE");
}