SFINAE: компилятор не выбирает специализированный класс шаблона

У меня проблема SFINAE:

В следующем коде я хочу, чтобы компилятор C++ выбрал специализированный функтор и напечатал «специальный», но вместо этого напечатал «общий».

#include <iostream>
#include <vector>

template<class T, class V = void>
struct Functor {
  void operator()() const {
    std::cerr << "general" << std::endl;
  }
};

template<class T>
struct Functor<T, typename T::Vec> {
  void operator()() const {
    std::cerr << "special" << std::endl;
  }
};

struct Foo {
  typedef std::vector<int> Vec;
};

int main() {
  Functor<Foo> ac;
  ac();
}

Как я могу исправить это, чтобы специализированная структура использовалась автоматически? Примечание. Я не хочу напрямую специализировать структуру Functor на Foo, но я хочу специализировать ее на всех типах, имеющих тип Vec.

P.S.: я использую g++ 4.4.4


person Frank    schedule 04.07.2012    source источник
comment
Удален тег compiler, он обычно используется для вопроса о самом процессе компиляции, тогда как этот вопрос касается языка C++.   -  person Matthieu M.    schedule 08.07.2012


Ответы (3)


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

Черта

Простой подход, не обязательно лучший, но простой для написания:

template <typename T>
struct has_nested_Vec {
    typedef char yes;
    typedef char (&no)[2];
    template <typename U>
    static yes test( typename U::Vec* p );
    template <typename U>
    static no test( ... );

    static const bool value = sizeof( test<T>(0) ) == sizeof(yes);
};

Подход прост, предоставьте две шаблонные функции, которые возвращают типы разных размеров. Один из них принимает вложенный тип Vec, а другой — многоточие. Для всех тех типов, которые имеют вложенный Vec, первая перегрузка является лучшим соответствием (многоточие является худшим соответствием для любого типа). Для тех типов, которые не имеют вложенного Vec SFINAE, эта перегрузка будет отброшена, и единственным оставшимся вариантом будет многоточие. Итак, теперь у нас есть трейт, который спрашивает, есть ли у какого-либо типа вложенный тип Vec.

Включить, если

Вы можете использовать это из любой библиотеки, или вы можете накатить свою собственную, это довольно просто:

template <bool state, typename T = void>
struct enable_if {};

template <typename T>
struct enable_if<true,T> {
    typedef T type;
};

Когда первым аргументом является false, единственным вариантом является базовый шаблон, который не имеет вложенного type, если условие равно true, то enable_if имеет вложенный type, который мы можем использовать с SFINAE.

Реализация

Теперь нам нужно предоставить шаблон и специализацию, которая будет использовать SFINAE только для тех типов с вложенным Vec:

template<class T, class V = void>
struct Functor {
    void operator()() const {
        std::cerr << "general" << std::endl;
    }
};
template<class T>
struct Functor<T, typename enable_if<has_nested_Vec<T>::value>::type > {
    void operator()() const {
        std::cerr << "special" << std::endl;
    }
};

Всякий раз, когда мы создаем экземпляр Functor с типом, компилятор попытается использовать специализацию, которая, в свою очередь, создаст экземпляр has_nested_Vec и получит истинное значение, переданное в enable_if. Для тех типов, для которых значение равно false, enable_if не имеет вложенного типа type, поэтому специализация будет отброшена в SFINAE и будет использоваться базовый шаблон.

Ваш конкретный случай

В вашем конкретном случае, когда кажется, что вам действительно не нужно специализировать весь тип, а только оператор, вы можете смешать три элемента в один: Functor, который отправляет одну из двух внутренних шаблонных функций на основе наличие Vec, устраняя необходимость в enable_if и классе признаков:

template <typename T>
class Functor {
   template <typename U>
   void op_impl( typename U::Vec* p ) const {
      std::cout << "specialized";
   }
   template <typename U>
   void op_impl( ... ) const {
      std::cout << "general";
   }
public:
   void operator()() const {
      op_impl<T>(0);
   }
};
person David Rodríguez - dribeas    schedule 04.07.2012

Несмотря на то, что это старый вопрос, я думаю, что все же стоит предоставить еще пару альтернатив для быстрого исправления исходного кода.

По сути, проблема заключается не в использовании SFINAE (на самом деле эта часть в порядке), а в сопоставлении параметра по умолчанию в основном шаблоне (void) с аргументом, предоставленным в частичной специализации (typename T::Vec). Из-за параметра по умолчанию в основном шаблоне Functor<Foo> на самом деле означает Functor<Foo, void>. Когда компилятор пытается реализовать это с помощью специализации, он пытается сопоставить два аргумента с аргументами в специализации и терпит неудачу, поскольку void нельзя заменить на std::vector<int>. Затем он возвращается к созданию экземпляра с использованием основного шаблона.

Итак, самое быстрое исправление, которое предполагает, что все ваши Vecs являются std::vector<int>s, состоит в том, чтобы заменить строку

template<class T, class V = void>

с этим

template<class T, class E = std::vector<int>>

Теперь будет использоваться специализация, потому что аргументы будут совпадать. Простой, но слишком ограниченный. Очевидно, нам нужно лучше контролировать тип аргумента в специализации, чтобы он соответствовал тому, что мы можем указать в качестве параметра по умолчанию в основном шаблоне. Вот одно из быстрых решений, не требующих определения новых трейтов:

#include <iostream>
#include <vector>
#include <type_traits>

template<class T, class E = std::true_type>
struct Functor {
  void operator()() const {
    std::cerr << "general" << std::endl;
  }
};

template<class T>
struct Functor<T, typename std::is_reference<typename T::Vec&>::type> {
  void operator()() const {
    std::cerr << "special" << std::endl;
  }
};

struct Foo {
  typedef std::vector<int> Vec;
};

int main() {
  Functor<Foo> ac;
  ac();
}

Это будет работать для любого типа Vec, который здесь может иметь смысл, включая, например, фундаментальные типы и массивы, а также ссылки или указатели на них.

person bogdan    schedule 04.12.2014

Другой альтернативой для обнаружения существования типа члена является использование void_t. Поскольку действительные частичные специализации предпочтительнее общей реализации, если они соответствуют параметрам по умолчанию, нам нужен тип, который оценивается как void, когда он действителен, и действителен только тогда, когда указанный член существует; этот тип обычно (и, начиная с C++17, канонически) известен как void_t.

template<class...>
using void_t = void;

Если ваш компилятор не поддерживает его должным образом (в ранних компиляторах C++14 неиспользуемые параметры в шаблонах псевдонимов не гарантированно обеспечивали SFINAE, нарушая приведенный выше void_t), доступен обходной путь.

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

Начиная с C++17, void_t доступен в библиотеке утилит, в type_traits.

#include <iostream>
#include <vector>
#include <type_traits> // For void_t.

template<class T, class V = void>
struct Functor {
  void operator()() const {
    std::cerr << "general" << std::endl;
  }
};

// Use void_t here.
template<class T>
struct Functor<T, std::void_t<typename T::Vec>> {
  void operator()() const {
    std::cerr << "special" << std::endl;
  }
};

struct Foo {
  typedef std::vector<int> Vec;
};

int main() {
  Functor<Foo> ac;
  ac();
}

При этом выход равен special, как и предполагалось.


В этом случае, поскольку мы проверяем наличие типа члена, процесс очень прост; это можно сделать без использования выражения SFINAE или библиотеки type_traits, что позволяет нам при необходимости переписать проверку для использования средств C++03.

// void_t:
// Place above Functor's definition.
template<typename T> struct void_t { typedef void type; };

// ...

template<class T>
struct Functor<T, typename void_t<typename T::Vec>::type> {
  void operator()() const {
    std::cerr << "special" << std::endl;
  }
};

Насколько мне известно, это должно работать на большинстве, если не на всех, совместимых с SFINAE компиляторах C++03-, C++11-, C++14- или C++1z. Это может быть полезно при работе с компиляторами, которые немного отстают от стандарта, или при компиляции для платформ, на которых еще нет компиляторов, совместимых с C++11.


Для получения дополнительной информации о void_t см. cppreference.

person Justin Time - Reinstate Monica    schedule 27.11.2016