Ошибки при использовании decltype() и SFINAE

В ответ на ... какой-то другой вопрос где-то я написал этот код.

struct no_type{};
template<typename T> struct has_apply {
    static decltype(T().apply<0u>(double())) func( T* ptr );
    static no_type func( ... );
    static const bool result = !std::is_same<no_type, decltype(func(nullptr))>::value;
};

class A {
public:
    template< unsigned n >
    void apply( const double& );

};
class B {
};

int main()
{
  std::cout << std::boolalpha << has_apply< A >::result << '\n';
  std::cout << std::boolalpha << has_apply< B >::result << '\n';
  std::cin.get();
  return( 0 );
}

Теперь мне кажется, что результат должен быть истинным, если T предлагает нестатическую функцию-член «применить», которая принимает двойное значение rvalue и литерал параметра шаблона, и ложь в противном случае. Однако приведенный пример фактически не компилируется для класса B при компиляции has_apply<B>. Не должен ли тот факт, что подстановка T в операторе decltype завершилась неудачно, означать, что он просто вызывает другую функцию? Разве не в этом суть SFINAE?

Решается самым нелепым и бессмысленным способом:

struct no_type{};
template<typename T> struct has_apply {
    template<typename U> static decltype(U().apply<0u>(double())) func( U* );
    template<typename U> static no_type func( ... );
    static const bool result = !std::is_same<no_type, decltype(func<T>(nullptr))>::value;
};

class A {
public:
    template< unsigned n >
    void apply( const double& );

};
class B {
};

int main()
{
  std::cout << std::boolalpha << has_apply< A >::result << '\n';
  std::cout << std::boolalpha << has_apply< B >::result << '\n';
  std::cin.get();
  return( 0 );
}

person Puppy    schedule 09.11.2010    source источник


Ответы (1)


SFINAE применяется, когда подстановка не удалась для параметра шаблона шаблона функции, а не для параметра шаблона шаблона класса, который имеет (не шаблонную) рассматриваемую функцию в качестве члена, как в вашем случае.

После исправления этого вы должны хотя бы изменить decltype(T().apply<0u>(double())) на decltype(T().template apply<0u>(double())), потому что выражение T() имеет зависимый тип. Причина этого такова: когда компилятор впервые видит T().apply<0u>, он еще ничего не знает о T, так как же ему анализировать токены apply и < после .? apply может быть шаблоном элемента, а затем < запустит для него список аргументов. Вместо этого OTOH apply может быть элементом, не входящим в шаблон (например, элементом данных), и тогда < будет проанализирован как оператор «меньше». Существует двусмысленность, и компилятору еще рано ее разрешать. Существует необходимость в механизме устранения неоднозначности, который программист мог бы использовать, чтобы сообщить компилятору, что apply ожидается: шаблон или нет. И здесь на помощь приходит конструкция .template (или ->template, или ::template): если она присутствует, компилятор знает, что она должна быть членом шаблона, в противном случае, если она отсутствует, компилятор знает, что член не должен быть шаблоном.

Наконец, вот пример, который я создал, который работает правильно и дает желаемые результаты на g++ 4.5.0 с -std=c++0x:

#include <iostream>

template < class T >
decltype( T().template apply< 0u >( double() ) ) f( T &t )
{
    return t.template apply< 0u >( 5. );
}

const char *f( ... )
{
    return "no apply<>";
}

class A {
public:
    template < unsigned >
    int apply( double d )
    {
        return d + 10.;
    }
};

class B {};

int main()
{
    A a;
    std::cout << f( a ) << std::endl;
    B b;
    std::cout << f( b ) << std::endl;
}

Выход:

15
no apply<>

Теперь, если вы удалите оба .template из первого определения f(), результат будет таким:

no apply<>
no apply<>

Это указывает на ошибку замены для class A, так как в нем нет элемента не шаблонного с именем apply. СФИНАЭ в действии!

person usta    schedule 09.11.2010
comment
Вы также должны использовать declval‹T›() вместо T(), если у T нет конструктора по умолчанию. В VS2010 его нет, но его можно сделать: stackoverflow.com/questions/2638843/ - person Edward Strange; 09.11.2010
comment
@Ноа Робертс: Верно! Это часть того, почему я сказал «по крайней мере» выше;) - person usta; 09.11.2010
comment
@usta: я до сих пор не понимаю. T не является зависимым типом, это тип шаблона. Если вы создаете переменную-член типа T, вы не говорите typename T t;, что вы делаете для зависимых типов. - person Puppy; 09.11.2010
comment
@DeadMG: обратите внимание, что необходимость добавлять template после . здесь не самая большая проблема. Главное, что SFINAE занимается только шаблонами функций и их параметрами. В любом случае, объект типа T (например, T()) является объектом зависимого типа (типа, который зависит от параметра шаблона), поэтому нужно произнести .template, чтобы получить доступ к шаблону члена этого объекта. - person usta; 09.11.2010
comment
@DeadMG: Пожалуйста, посмотрите, понятен ли мой отредактированный ответ в этом вопросе. - person usta; 09.11.2010
comment
@usta: Visual Studio даже не примет такой синтаксис. Я понимаю, что вы имеете в виду, когда говорите о проблемах синтаксического анализа, но это не так. - person Puppy; 09.11.2010
comment
@DeadMG: Тогда это ошибка. GCC 4.5 правильно его принимает (-std=c++0x). И он не принимает без .template (ну, в примере, который я создал, он компилируется из-за SFINAE, но во время выполнения работает по-другому :) ). Я собираюсь опубликовать свой пример в ближайшее время. - person usta; 09.11.2010
comment
@usta: Visual Studio выдает ошибку EOF в этом фрагменте, но если я удалю ключевое слово шаблона, он отлично скомпилируется и выдаст 15, no apply<> в качестве вывода. - person Puppy; 10.11.2010
comment
@DeadMG: Спасибо за информацию. Я думаю, было бы хорошо сообщить об этом MS; Я посмотрю, найду ли я на это смелость... - person usta; 10.11.2010
comment
@usta: Хотя я считаю, что тот факт, что он не будет компилироваться с правильным синтаксисом Стандарта, невелик, тот факт, что он будет компилироваться с более простым синтаксисом, вероятно, хорош. - person Puppy; 10.11.2010
comment
@DeadMG: я не могу с этим согласиться, более простой синтаксис предназначен только для не-шаблонов. Представьте, что у нас в миксе есть еще один класс: class C { public: int apply; };, а в main() у нас есть C c; cout << f( c );, что он должен печатать в этом случае? Если у нас есть .template, стандарт говорит, что он не должен печатать no apply‹›, GCC согласится, и vc++ тоже должен, но сейчас это не так. Если у нас нет .template, стандарт говорит, что он должен печатать 0 (независимо от значения c.apply или даже от того, инициализирован он или нет). Почему? Это хороший вопрос, который можно оставить в качестве упражнения для читателя ;) - person usta; 10.11.2010
comment
@usta: Не заставляйте меня начинать с того, насколько невероятно плохо разработана грамматика C ++. - person Puppy; 10.11.2010
comment
@DeadMG: Тогда, я думаю, мы согласимся не согласиться с этим вопросом. - person usta; 10.11.2010