Специализированные шаблоны на основе методов

В последнее время я много программировал на Java, сейчас возвращаюсь к своим корням в C++ (мне реально стало не хватать указателей и ошибок сегментации). Зная, что C++ имеет широкую поддержку шаблонов, мне было интересно, есть ли в нем некоторые возможности Java, которые могли бы быть полезны для написания обобщенного кода. Допустим, у меня есть две группы классов. У одного из них метод first(), у другого метод second(). Есть ли способ специализации шаблонов, которые будут выбираться компилятором, в зависимости от методов, которыми обладает один класс? Я стремлюсь к поведению, похожему на поведение Java:

public class Main {
    public static void main(String[] args) {
        First first = () -> System.out.println("first");
        Second second = () -> System.out.println("second");
        method(first);
        method(second);
    }

    static <T extends First> void method(T argument) {
        argument.first();   
    }

    static <T extends Second> void method(T argument) {
        argument.second();
    }
}

Где First и Second — это интерфейсы. Я знаю, что могу сгруппировать обе эти группы, производя каждую из них от высшего класса, но это не всегда возможно (автобоксинг в C++ отсутствует, а некоторые классы не наследуются от общего предка).

Хорошим примером моих потребностей является библиотека STL, в которой некоторые классы имеют такие методы, как push(), а некоторые другие — insert() или push_back(). Допустим, я хочу создать функцию, которая должна вставлять несколько значений в контейнер, используя функцию с переменным числом аргументов. В Java это легко сделать, потому что у коллекций есть общий предок. С другой стороны, в C++ это не всегда так. Я попробовал это с помощью утиного набора, но компилятор выдает сообщение об ошибке:

template <typename T>
void generic_fcn(T argument) {
    argument.first();
}

template <typename T>
void generic_fcn(T argument) {
    argument.second();
}

Итак, мой вопрос: возможна ли реализация такого поведения без создания ненужного шаблонного кода путем специализации каждого отдельного случая?


person Adrian Jałoszewski    schedule 28.08.2016    source источник
comment
Вы можете использовать класс политики в качестве другого параметра шаблона и наследовать от него. Затем у вас есть реализации политики для различных типов контейнеров. См. также Проектирование на основе политик   -  person πάντα ῥεῖ    schedule 28.08.2016
comment
Не могли бы вы показать мне пример, как это может решить проблему? Потому что кажется, что стоит попробовать.   -  person Adrian Jałoszewski    schedule 28.08.2016
comment
В статье в Википедии есть образец, и если вы будете искать эти слова, вы найдете гораздо больше.   -  person πάντα ῥεῖ    schedule 28.08.2016
comment
Хорошо, я попробую. Спасибо.   -  person Adrian Jałoszewski    schedule 28.08.2016


Ответы (4)


Вместо <T extends First> вы будете использовать то, что мы называем sfinae. Это метод добавления констант к функции на основе типов параметров.

Вот как вы это сделаете на С++:

template <typename T>
auto generic_fcn(T argument) -> void_t<decltype(argument.first())> {
    argument.first();
}

template <typename T>
auto generic_fcn(T argument) -> void_t<decltype(argument.second())> {
    argument.second();
}

Чтобы функция существовала, компилятору потребуется тип argument.second() или тип argument.first(). Если выражение не дает тип (т. е. T не имеет функции first()), компилятор попытается использовать другую перегрузку.

void_t реализовано следующим образом:

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

Еще одна замечательная вещь заключается в том, что если у вас есть такой класс:

struct Bummer {
    void first() {}
    void second() {}
};

Тогда компилятор фактически сообщит вам, что вызов неоднозначен, потому что тип соответствует обоим ограничениям.


Если вы действительно хотите проверить, расширяет ли тип другой (или реализует, в С++ это одно и то же), вы можете использовать черту типа std::is_base_of

template <typename T>
auto generic_fcn(T argument) -> std::enable_if_t<std::is_base_of<First, T>::value> {
    argument.first();
}

template <typename T>
auto generic_fcn(T argument) -> std::enable_if_t<std::is_base_of<Second, T>::value> {
    argument.second();
}

Чтобы узнать больше об этой теме, проверьте sfinae на cpprefence, и вы можете проверить < href="http://en.cppreference.com/w/cpp/header/type_traits" rel="nofollow">доступные признаки, предоставляемые стандартной библиотекой.

person Guillaume Racicot    schedule 28.08.2016

так много вариантов, доступных в С++.

Я предпочитаю бесплатные функции и корректно возвращаю любой тип результата.

#include <utility>
#include <type_traits>
#include <iostream>

struct X
{
  int first() { return 1; }
};

struct Y
{
  double second() { return 2.2; }
};


//
// option 1 - specific overloads
//

decltype(auto) generic_function(X& x) { return x.first(); }
decltype(auto) generic_function(Y& y) { return y.second(); }

//
// option 2 - enable_if
//

namespace detail {
  template<class T> struct has_member_first
  {
    template<class U> static auto test(U*p) -> decltype(p->first(), void(), std::true_type());
    static auto test(...) -> decltype(std::false_type());
    using type = decltype(test(static_cast<T*>(nullptr)));
  };
}
template<class T> using has_member_first = typename detail::has_member_first<T>::type;

namespace detail {
  template<class T> struct has_member_second
  {
    template<class U> static auto test(U*p) -> decltype(p->second(), void(), std::true_type());
    static auto test(...) -> decltype(std::false_type());
    using type = decltype(test(static_cast<T*>(nullptr)));
  };
}
template<class T> using has_member_second = typename detail::has_member_second<T>::type;

template<class T, std::enable_if_t<has_member_first<T>::value>* =nullptr> 
decltype(auto) generic_func2(T& t)
{
  return t.first();
}

template<class T, std::enable_if_t<has_member_second<T>::value>* =nullptr> 
decltype(auto) generic_func2(T& t)
{
  return t.second();
}

//
// option 3 - SFNAE with simple decltype
//

template<class T>
auto generic_func3(T&t) -> decltype(t.first())
{
  return t.first();
}

template<class T>
auto generic_func3(T&t) -> decltype(t.second())
{
  return t.second();
}


int main()
{
  X x;
  Y y;

  std::cout << generic_function(x) << std::endl;
  std::cout << generic_function(y) << std::endl;

  std::cout << generic_func2(x) << std::endl;
  std::cout << generic_func2(y) << std::endl;

  std::cout << generic_func3(x) << std::endl;
  std::cout << generic_func3(y) << std::endl;

}
person Richard Hodges    schedule 28.08.2016

Вы можете отправить вызов следующим образом:

#include<utility>
#include<iostream>

struct S {
    template<typename T>
    auto func(int) -> decltype(std::declval<T>().first(), void())
    { std::cout << "first" << std::endl; }

    template<typename T>
    auto func(char) -> decltype(std::declval<T>().second(), void())
    { std::cout << "second" << std::endl; }

    template<typename T>
    auto func() { return func<T>(0); }
};

struct First {
    void first() {}
};

struct Second {
    void second() {}
};

int main() {
    S s;
    s.func<First>();
    s.func<Second>();
}

Метод first предпочтительнее метода second, если в классе есть оба метода.
В противном случае func использует перегрузку функций для проверки двух методов и выбора правильного.
Этот метод называется sfinae, используйте это имя для поиска в Интернете дополнительных сведений.

person skypjack    schedule 28.08.2016

Вот небольшая библиотека, которая поможет вам определить, существует ли член.

namespace details {
  template<template<class...>class Z, class always_void, class...>
  struct can_apply:std::false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply=details::can_apply<Z, void, Ts...>;

Теперь мы можем легко написать has first и has second:

template<class T>
using first_result = decltype(std::declval<T>().first());
template<class T>
using has_first = can_apply<first_result, T>;

и аналогично для second.

Теперь у нас есть наш метод. Мы хотим позвонить либо первому, либо второму.

template<class T>
void method_second( T& t, std::true_type has_second ) {
  t.second();
}
template<class T>
void method_first( T& t, std::false_type has_first ) = delete; // error message
template<class T>
void method_first( T& t, std::true_type has_first ) {
  t.first();
}
template<class T>
void method_first( T& t, std::false_type has_first ) {
  method_second( t, has_second<T&>{} );
}
template<class T>
void method( T& t ) {
  method_first( t, has_first<T&>{} );
}

это известно как диспетчеризация тегов.

method вызывает method_first, который определяется, можно ли вызвать T& с помощью .first(). Если это возможно, он звонит тому, кто звонит .first().

Если это невозможно, он вызывает тот, который пересылает на method_second, и проверяет, есть ли у него .second().

Если нет ни того, ни другого, он вызывает функцию =delete, которая генерирует сообщение об ошибке во время компиляции.

Есть много, много, много способов сделать это. Мне лично нравится диспетчеризация тегов, потому что вы можете получить более качественные сообщения об ошибках из-за несоответствия, чем генерирует SFIANE.

В С++ 17 вы можете быть более прямым:

template<class T>
void method(T & t) {
  if constexpr (has_first<T&>{}) {
    t.first();
  }
  if constexpr (has_second<T&>{}) {
    t.second();
  }
}
person Yakk - Adam Nevraumont    schedule 29.08.2016