Недавно я много программировал на 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 ++, с другой стороны, это не всегда так. Я попробовал это с помощью duck-typing, но компилятор выдает сообщение об ошибке:
template <typename T>
void generic_fcn(T argument) {
argument.first();
}
template <typename T>
void generic_fcn(T argument) {
argument.second();
}
Поэтому мой вопрос: возможно ли реализовать такое поведение, не создавая ненужный код для разбивки, специализируя каждый отдельный случай?
Вместо <T extends First>
, вы будете использовать то, что мы называем sfinae. Это техника добавления компонентов в функцию на основе типов параметров.
Вот как бы вы сделали это в C ++:
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() {}
};
Тогда компилятор фактически скажет вам, что вызов неоднозначен, потому что тип соответствует обоим ограничениям.
Если вы действительно хотите проверить, расширяет ли тип другой (или реализует то же самое в C ++), вы можете использовать черту типа 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, и вы можете проверить доступные черты предоставляется стандартной библиотекой.
так много вариантов, доступных в C ++.
Я предпочитаю отдавать предпочтение свободным функциям и правильно возвращать любой тип результата.
#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;
}
Вы можете отправить звонок следующим образом:
#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, используйте это имя для поиска в Интернете для получения дополнительной информации.
Вот небольшая библиотека, которая поможет вам определить, существует ли член.
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.
В C ++ 17 вы можете быть более прямым:
template<class T>
void method(T & t) {
if constexpr (has_first<T&>{}) {
t.first();
}
if constexpr (has_second<T&>{}) {
t.second();
}
}