Подтвердить что ты не робот

Почему этот 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, возможно, что-то еще нужно?

4b9b3361

Ответ 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");
}