Выражение SFINAE: как выбрать версию шаблона в зависимости от того, содержит ли тип функцию с одним или несколькими аргументами - PullRequest
0 голосов
/ 18 октября 2018

Я пытаюсь выбрать во время компиляции между различными реализациями шаблона в зависимости от того, реализует ли аргумент конкретную функцию.Это общий вопрос (см. этот вопрос SO и этот пример , на который ссылается эта статья . Общий ответ "use expression SFINAE".

В большинстве примеров показано, как выражение SFINAE можно использовать для выбора в зависимости от наличия функции с нулевым аргументом. Я пытался адаптировать их к моему сценарию использования с 1 аргументом, используя declval (свободно основано на этот пример ), но я не могу заставить его работать.

Я уверен, что я делаю что-то не так в примере ниже, но я не могу понять, что этоis. Пример пытается определить две версии шаблона bool Util::Container::Contains(container, value), который будет использовать встроенный в контейнер метод find(value), если он существует, и в противном случае использовать линейный поиск, используя std::find(...)

Обратите внимание: Я знаю, что могу сделать эту работу, просто перегрузив Contains () для unordered_map, unordered_set и т. Д., Но вместо этого я хотел бы выяснить этот подход на основе шаблонов, чтобы он автоматически делегироваллюбомуfind(value) контейнера без добавления перегрузки.

#include <unordered_set>
#include <unordered_map>
#include <vector>
#include <string>

namespace Util::Container {

    namespace Detail
    {
        template <typename T>
        class HasFindMethod
        {
        private:
            typedef char YesType[1];
            typedef char NoType[2];

            // This is how the examples show it being done for a 0-arg function
            //template <typename C> static YesType& Test(decltype(&C::find));

            // Here's my attempt to make it match a 1-arg function
            template <typename C> static YesType& 
                Test(decltype(std::declval<C>().find(std::declval<const C::value_type&>())));

            template <typename C> static NoType& Test(...);

        public:
            enum { value = sizeof(Test<T>(0)) == sizeof(YesType) };
        };
    }

    // Fallback: uses std::find() to do the lookup if no type-specific T::find(value) exists
    template<typename T>
    bool Contains(const T& in_container, const typename T::value_type& in_item)
    {
        const auto& result = std::find(in_container.cbegin(), in_container.cend(), in_item);
        return (result != in_container.cend());
    }

    // Preferred: use T::find() to do the lookup if possible
    template<typename T>
    inline typename std::enable_if<Detail::HasFindMethod<T>::value, bool>::type 
        Contains(const T& in_container, const typename T::value_type& in_item)
    {
        return (in_container.find(in_item) != in_container.end());
    }
}

int main()
{
    const std::vector<int> v { 1, 2, 3 };
    const std::unordered_map<int, std::string> m { {1,"1" }, {2,"2"} };
    const std::unordered_set<std::string> s { "1" , "2" };

    // These should use the std::find()-based version of Contains() since vector and unordered_map
    // have no find(value_type) method. And they do.
    const bool r_v = Util::Container::Contains(v, 2);
    const bool r_m = Util::Container::Contains(m, { 2, "2" });

    // !!!!!! 
    // 
    // This should use the T::find(value_type)-based version of Contains() since
    // unordered_set has a find(value_type) method.
    //
    // But it doesn't --- that's the issue I'm trying to solve.
    // 
    const bool r_s = Util::Container::Contains(s, "2");
}

Если кто-нибудь может показать мне, как это исправить, я был бы очень признателен.

FWIW, япытаясь реализовать это в Visual Studio 2017 v15.8

Ответы [ 3 ]

0 голосов
/ 18 октября 2018

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

Например, Detail::HasFindMethod<std::unordered_set<int>> приведет к следующим двум Test сигнатурам (потому что find вернет iterator):

        static YesType& Test(std::unordered_set<int>::iterator);

        static NoType& Test(...);

Вы пытаетесь вызвать Test с аргументом 0, который не конвертируется в iterator.Следовательно, выбирается второй.

В качестве решения используйте указатель:

        template <typename C> static YesType& 
            Test(decltype(std::declval<C>().find(std::declval<const C::value_type&>()))*);
        //                                                                             ^

Затем выполните проверку с аргументом nullptr:

        enum { value = sizeof(Test<T>(nullptr)) == sizeof(YesType) };

Теперь у нас будет неоднозначность (Test(...) также будет соответствовать), поэтому мы можем сделать это еще хуже:

        template <typename C, class ... Args> static NoType& Test(void*, Args...);

Как показано в других ответах, это все еще сравнительно запутанное решение (и есть еще проблемы, которые мешают ему работать в вашем случае, такие как неоднозначность между перегрузками, когда enable_if работает).Просто объясню конкретную пробку в вашей попытке здесь.

0 голосов
/ 18 октября 2018

Более простое (на мой взгляд) и более читаемое решение может быть достигнуто с помощью утилиты void_t :

template <typename T, typename Dummy = void>
struct has_member_find : std::false_type { };

template <typename T>
struct has_member_find<T,
    std::void_t<decltype(std::declval<T>().find(std::declval<typename T::value_type &>()))>>
    : std::true_type { };

template<typename T>
std::enable_if_t<!has_member_find<T>::value, bool>
Contains(const T& in_container, const typename T::value_type& in_item)
{
    const auto& result = std::find(in_container.cbegin(), in_container.cend(), in_item);
    return (result != in_container.cend());
}

template<typename T>
std::enable_if_t<has_member_find<T>::value, bool>
Contains(const T& in_container, const typename T::value_type& in_item)
{
    return (in_container.find(in_item) != in_container.end());
}

Обратите внимание, что void_t доступно только с C ++ 17,однако, если у вас нет полной поддержки C ++ 17, вы можете определить ее самостоятельно, поскольку ее определение смехотворно просто:

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

Вы можете узнать больше об этой утилите и шаблоне, который она вводит в этот документ .

0 голосов
/ 18 октября 2018

Простой способ с decltype - это

template<typename C, typename V>
auto Contains(const C& c, const V& value)
-> decltype(std::find(c.cbegin(), c.cend(), value) != c.cend())
{
    return std::find(c.cbegin(), c.cend(), value) != c.cend();
}

template <typename C, typename Key>
auto Contains(const C& c, const Key& key)
-> decltype(c.find(key) != c.end())
{
    return c.find(key) != c.end();
}

, но тогда, когда обе функции возможны, вы получите неоднозначный вызов.

Так что просто добавьте дополнительный параметр, чтобы расставить приоритеты перегрузки:

struct low_priority {};
struct high_priority : low_priority {};


template<typename C, typename V>
auto ContainsImpl(low_priority, const C& c, const V& value)
-> decltype(std::find(c.cbegin(), c.cend(), value) != c.cend())
{
    return std::find(c.cbegin(), c.cend(), value) != c.cend();
}

template <typename C, typename Key>
auto ContainsImpl(high_priority, const C& c, const Key& key)
-> decltype(c.find(key) != c.end())
{
    return c.find(key) != c.end();
}

template <typename C, typename T>
auto Contains(const C& c, const T& t)
-> decltype(ContainsImpl(high_priority{}, c, t))
{
    return ContainsImpl(high_priority{}, c, t);
}

Теперь о вашей версии у вас есть несколько проблем

Последняя:

// Expected Fallback: uses std::find() to do the lookup if no type-specific T::find(value) exists
template<typename T>
bool Contains(const T&, const typename T::value_type&);

// Expected Preferred: use T::find() to do the lookup if possible
template<typename T>
typename std::enable_if<Detail::HasFindMethod<T>::value, bool>::type 
Contains(const T&, const typename T::value_type&);

SFINAE позволяет отбрасывать перегрузки, но не расставлять приоритеты.Вы должны использовать приоритет, как показано выше, или создать исключительный набор перегрузки:

template<typename T>
typename std::enable_if<!Detail::HasFindMethod<T>::value, bool>::type 
Contains(const T&, const typename T::value_type&);

template<typename T>
typename std::enable_if<Detail::HasFindMethod<T>::value, bool>::type 
Contains(const T&, const typename T::value_type&);

В дополнение к этому, как упомянуто в комментарии, map семейство будет использовать key_type, а не value_type.

Тогда ваш код обнаружения содержит ошибки,

// Вот как примеры показывают, что это делается для функции 0-arg // шаблон static YesType & Test (decltype (& C:: find));

Нет, это обнаруживает, если C имеет метод find (без перегрузки).

template <typename C> static YesType& 
Test(decltype(std::declval<C>().find(std::declval<const C::value_type&>())));

Здесь,вы используете SFINAE, но конечным типом будет (const_) iterator, и Test<C>(0) не будет принимать эту перегрузку (если только итератор не может быть собран из 0, что не является обычным случаем).Возможно добавление дополнительных *, тогда у вас есть указатель на итератор, который можно инициализировать с помощью 0.

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

namespace detail{
  template<class T, typename ... Args>
  static auto test_find(int)
      -> sfinae_true<decltype(std::declval<T>().find(std::declval<const Arg&>()...))>;
  template<class, class ...>
  static auto test_find(long) -> std::false_type;
} // detail::

template<class C, typename ... Args>
struct has_find : decltype(detail::test_find<T, Args...>(0)){};
// int has higher priority than long for overload resolution

а затем используйте свои черты с std::enable_if has_find<Container, Key>::value.

...