анти-SFINAE, разрешающий перегрузку, если данное выражение *не* правильно сформировано

SFINAE легко использовать, чтобы скрыть перегрузку определенной функции, если конкретное выражение имеет неправильный формат. Но я хочу сделать обратное, скрыв перегрузку тогда и только тогда, когда заданное выражение является правильным, и сделать это в очень общем виде. У меня есть решение, которое работает в clang 3.5.0 и gcc 5.2.0, но меня интересуют любые комментарии и альтернативы.

В идеале должна быть встроенная constexpr bool функция/макрос, которая сообщала бы нам во время компиляции, правильно ли построено конкретное выражение.

IS_WELL_FORMED(  declval<T>() < declval<T>()  )  // I want this as bool

который можно использовать с enable_if для включения или отключения перегрузок.

Я нашел решение, но я столкнулся с каким-то странным поведением в g++ 5.2.0 и clang 3.5.0, и мне интересно, есть ли ошибки.


Предлагаемое решение

Во-первых, самое надежное решение, которое я нашел до сих пор, которое работает на обоих компиляторах. Например, я хочу проверить, есть ли у T метод .length(). Это требует «скрытия» выражения внутри другого шаблона. Кроме того, функция под названием well_formed_default, о которой я расскажу позже.

    // Define a template to contain our expression
    template<typename T2=T, typename = 
        decltype( declval<T2>().length() ) // This line is the expression to test
    > struct test_for_length_method { };

и вот как его можно использовать в содержащем классе:

template<typename T>
struct Demo { // the main struct I'm working on

    // Define a template to "hide" our expression
    template<typename T2=T, typename = 
        decltype( declval<T2>().length() ) // This line is the expression to test
    > struct test_for_length_method { };

    // test if the above "test" succeeds
    constexpr bool T_has_length =
        well_formed_default< test_for_length_method >();

    // demonstrate the bool in an enable_if
    template<bool b = T_has_length>
    static
    auto demo_enable_if() -> typename std::enable_if<  b >::type { 
        cout << "T has length" << endl;
    }
    template<bool b = T_has_length>
    static
    auto demo_enable_if() -> typename std::enable_if< !b >::type {
        cout << "T doesn't" << endl;
    }
}

Вышеприведенное работает, как и ожидалось, с Demo<int>::demo_enable_if() и Demo<std::string>::demo_enable_if().

Я не могу использовать T_has_length непосредственно внутри enable_if, так как это приведет к серьезным ошибкам, потому что это не замена шаблона. Поэтому я делаю вид, что это параметр шаблона, делая копию if в другом параметре шаблона bool b = T_has_length. Это похоже на то, как мы должны использовать typename T2=T внутри тестовой структуры. Немного раздражает, но я думаю, что это имеет смысл.

Теперь я определяю well_formed_default. Он принимает шаблон (и, возможно, некоторые типы) и возвращает true или false в зависимости от того, может ли он построить шаблон с этими конкретными аргументами. Я бы включил его автоматически во все свои проекты. Возможно, что-то подобное уже есть в стандарте?

template<template<class...> class Template, class ...Args>
constexpr auto well_formed_default_impl(int) 
    -> typename std::conditional<false,Template<Args...>, bool>::type {
        return true;
}
template<template<class...> class Template, class ...Args>
constexpr auto well_formed_default_impl(...)
    -> bool {
        return false; 
}   
template<template<class...> class Template, class ...Args>
constexpr bool well_formed_default() {
    return well_formed_default_impl<Template,Args...>(0);
}

--

Мой (первый) вопрос

Это работает как в g++ 5.2.0, так и в clang 3.5.0. Но должно ли? Это полностью стандарт, или я слишком далеко захожу со стандартом? Я думаю, что самое странное для меня - это использование Template<Args...> внутри well_formed_default_impl - гарантированно ли это будет ошибкой замены в том виде, в котором я его использую? например с test_for_pushback_method_struct<>, когда соответствующий decltype неправильно сформирован?

--

Почему я не могу использовать вместо этого псевдоним?

(Что касается оставшейся части этого вопроса, может быть полезно взглянуть на вывод этого кода на Coliru так как в нем есть все тесты и результаты, которые я обсуждаю ниже.)

Я начал этот проект с псевдонима вместо указанной выше структуры. Я думал, что это будет эквивалентно. Но вместо этого оба компилятора считают, что string нет метода длины.

template<typename T2=T>
    using test_for_length_method_alias = decltype( declval<T2>().length() );

Наконец, я попробовал и структуру, и псевдоним, но там, где я явно определяю первый параметр типа (T) вместо того, чтобы полагаться на значение по умолчанию T2=T. Это ничего не должно изменить, потому что я передаю тип по умолчанию, но это меняет поведение! Использование структуры с явным первым параметром

well_formed_default<test_for_length_method_struct , T>

корректно работает на обоих компиляторах. Но alias-with-explicit-first-type корректно работает только с clang:

well_formed_default<test_for_length_method_alias , T>

person Aaron McDaid    schedule 14.08.2015    source источник
comment
Знаете ли вы о en.cppreference.com/w/cpp/experimental/is_detected ?   -  person dyp    schedule 14.08.2015
comment
Похоже, это связано с тем, как работает вывод: melpon.org/wandbox/permlink/Q1ObwKmGatRZ4jDJ Либо параметр template<class...> class неправильно соответствует шаблону псевдонима, либо замена не удалась.   -  person dyp    schedule 14.08.2015
comment
Может быть, это wg21.cmeerw.net/cwg/issue1430?   -  person dyp    schedule 14.08.2015
comment
stackoverflow.com/a/30195655/1774667 решить вашу проблему? В основном это одна часть is_detected выше (та часть, которая вам нужна).   -  person Yakk - Adam Nevraumont    schedule 14.08.2015
comment
Спасибо за отзыв и ссылки. Мне придется изучить это больше, когда у меня будет время в эти выходные. Я думаю, я попытаюсь понять, есть ли основные языковые функции, которые появятся в C++ 17, чтобы упростить это, или это просто расширения библиотеки. Потому что, если все эти методы могут быть реализованы на С++ 11, как я сделал в этом вопросе, то почему это не использовалось в течение последних нескольких лет в качестве стандарта, зачем делать проверку концепций?! Хотя я предполагаю, что могут быть некоторые дополнительные проблемы и детали, которые мне не хватает.   -  person Aaron McDaid    schedule 15.08.2015
comment
@dyp, я только сейчас вижу актуальность проблемы расширения пакета. Потребовалось много экспериментов, прежде чем я смог найти больше примеров, которые помогли мне. По сути, при вычислении возвращаемого типа gcc отклоняет преобразование в псевдонимы. Но он принимает его и вычисляет правильный тип в других контекстах, таких как указание типа по умолчанию для параметра типа шаблона. Все это кажется немного нелогичным. Во всяком случае, стандарт требует, чтобы шаблоны можно было расширять в шаблоны? Как clang делает? И gcc принимает их в некоторых местах, но есть еще какие-то пробелы (например, возвращаемый тип) в gcc?   -  person Aaron McDaid    schedule 15.08.2015
comment
(после моего комментария всего несколько минут назад). После еще нескольких экспериментов я вдруг снова запутался. @dyp, теперь clang отклоняет расширение пакетов в псевдонимы. Есть ли вообще хороший ресурс по шаблонам псевдонимов? Я предполагал, что понял их, поскольку они кажутся довольно простыми, но должен признать, что это не так. Кажется, у них есть какие-то странные ограничения, которых нет у шаблона structs.   -  person Aaron McDaid    schedule 15.08.2015
comment
Я действительно не понимаю, куда движется CWG с шаблонами псевдонимов. Они частично прозрачны, но я не совсем понимаю, насколько они прозрачны. Например, wg21.cmeerw.net/cwg/issue1286 Кроме того, CWG 1430 упоминает искажение имен. Поскольку существует открытая проблема (1430) и различия в реализации, я бы предложил сократить код до этой проблемы и задать вопрос в std-discussion, или разработчикам gcc, или снова на SO.   -  person dyp    schedule 15.08.2015


Ответы (2)


Несколько маловероятно, что в язык будут внесены какие-либо изменения для прямой поддержки такого рода использования (за исключением того, какие концепции принесут). Понятия облегчают написание взаимоисключающих перегрузок, а отрицательные понятия позволяют писать перегрузки, которые включаются, если выражение неправильно сформировано. (И да, я знаю, что это не поможет вам написать такой код для C++11 или C++14.) Пример:

#include <iostream>

template <class T>
requires !requires(T t) {t.f();}
void f()
{
    std::cout << "does not support f()!" << std::endl;    
}

template <class T>
void f()
{
    std::cout << "supports f()!" << std::endl;    
}

struct A {};
struct B {void f() {}};
int main()
{
    f<A>();
    f<B>();
}

Я добавил неограниченную перегрузку только для иллюстрации. Его отсутствие сделало бы вызов f‹B›() некорректным. Код работает сегодня на транке gcc, и вы можете попробовать его на melpon.org/wandbox.

person Ville Voutilainen    schedule 14.08.2015
comment
Думаю, мне следовало упомянуть понятия в моем вопросе, поскольку они актуальны. Спасибо, что упомянули о них, так как я мало что о них знаю, кроме того, что смотрел отличный доклад Страуструпа о них! - person Aaron McDaid; 15.08.2015

Вот can_apply шаблон:

template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;

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, void_t<Z<Ts...>>, Ts...>:
    std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;

Для конкретной проблемы, если x<y его использование выглядит так:

template<class Lhs, class Rhs>
using less_r = decltype( std::declval<Lhs>() < std::declval<Rhs>() );

template<class Lhs, class Rhs>
using can_less = can_apply< less_r, Lhs, Rhs >;

А затем с помощью can_less:

struct Foo {};
struct Bar {};

void operator<( Foo, Bar ) {}

int main() {
  std::cout << can_less<Foo, Bar>{} << can_less<Bar,Foo>{} << can_less<int, int>{} << can_less<char*, int>{} << '\n';
}

вывод 1010 как в clang, так и в gcc.

Это работает по стандарту и примерно соответствует интерфейсу std::experimental::is_detected (с меньше функций, но проще в реализации).

person Yakk - Adam Nevraumont    schedule 14.08.2015
comment
На первый взгляд, это очень похоже на один из неудачных экспериментов с g++ (иногда псевдонимы вели себя неправильно). Так что мне придется провести еще несколько тестов — постепенно превратить ваш код в мой или наоборот, чтобы посмотреть, что не сработает. - person Aaron McDaid; 15.08.2015