Специализированные шаблоны на основе методов
Недавно я много программировал на 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();
}
}