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

Недавно я много программировал на Java, теперь я возвращаюсь к своим корням С++ (я действительно начал пропускать указатели и ошибки сегментации). Зная, что С++ имеет широкую поддержку шаблонов, мне было интересно, есть ли у него некоторые возможности 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 - интерфейсы. Я знаю, что я мог бы группировать обе эти группы, выведя каждый из них из высшего класса, но это не всегда возможно (без автобоксинга в С++, а некоторые классы не наследуются от общего предка).

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

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

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

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

Ответы

Ответ 1

Вместо <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, и вы можете проверить доступные свойства, предоставленные стандартной библиотекой.

Ответ 2

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

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

#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;

}

Ответ 3

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

#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, используйте это имя для поиска в Интернете для более подробной информации.

Ответ 4

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

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...>;

Теперь мы можем писать в первую очередь и иметь второе место:

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();
  }
}