2016-08-28 20 views
14

Ostatnio dużo programowałem w Javie, teraz wracam do moich korzeni C++ (naprawdę zacząłem pomijać wskaźniki i błędy segmentacji). Wiedząc, że C++ ma szerokie wsparcie dla szablonów, zastanawiałem się, czy ma pewne możliwości Javy, które mogłyby być przydatne do pisania uogólnionego kodu. Powiedzmy, że mam dwie grupy klas. Jedna z nich ma metodę first(), druga ma metodę second(). Czy istnieje sposób na wyspecjalizowanie szablonów do wyboru przez kompilator w zależności od metod, które posiada jedna klasa? Jestem zmierzające do zachowania, który jest podobny do jednego z Java:Specjalizujące szablony oparte na metodach

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

Gdzie First i Second interfejsy. Wiem, że mógłbym pogrupować obie te grupy, wyprowadzając każdą z nich z wyższej klasy, ale nie zawsze jest to możliwe (nie ma autoboxingu w C++ i niektóre klasy nie dziedziczą po wspólnym przodku).

Dobrym przykładem moich potrzeb jest biblioteka STL, w której niektóre klasy mają metody takie jak push(), a niektóre inne mają insert() lub push_back(). Powiedzmy, że chcę utworzyć funkcję, która musi wstawiać wiele wartości do kontenera przy użyciu funkcji variadic. W Javie jest to łatwe do wykonania, ponieważ kolekcje mają wspólnego przodka. Z drugiej strony w C++ nie zawsze tak jest. Próbowałem go przez kaczki-pisania, ale kompilator daje komunikat o błędzie:

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

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

Więc moje pytanie brzmi: czy wdrożenia takiego zachowania możliwe bez tworzenia niepotrzebnego kodu boileplate przez specjalizującą się każdy pojedynczy przypadek?

+1

Można użyć klasę polityczną jako kolejny parametr szablonu i dziedziczą z niego. Następnie masz implementacje strategii dla różnych typów kontenerów. Zobacz także [Projektowanie oparte na zasadach] (https://en.wikipedia.org/wiki/Policy-based_design) –

+0

Czy możesz pokazać mi przykład, jak to rozwiązać problem? Bo wygląda na to, że warto spróbować. –

+0

Jest przykład w artykule wikipedia, a jeśli szukasz tych słów, znajdziesz o wiele więcej. –

Odpowiedz

16

Zamiast <T extends First> użyjesz czegoś, co nazywamy sfinae. Jest to technika polegająca na dodawaniu stałych do funkcji opartej na typach parametrów.

Oto jak chcesz to zrobić w 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(); 
} 

Dla funkcja istnieje, kompilator będzie potrzebować typ argument.second() lub rodzaj argument.first(). Jeśli wyrażenie nie daje typu (np. T nie ma funkcji first()), kompilator spróbuje innego przeciążenia.

void_t realizowany jest następująco:

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

Kolejną wielką zaletą jest to, że jeśli masz taką klasę:

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

Następnie kompilator będzie skutecznie powiedzieć, że połączenie jest niejednoznaczny, ponieważ typ dopasuj oba ograniczenia.


Jeśli naprawdę chcesz, aby sprawdzić, czy typ rozszerza innego (lub zaimplementować w C++ to samo) można użyć typu cechę 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(); 
} 

Aby dowiedzieć się więcej na ten temat, sprawdź sfinae na cpprefence, a możesz sprawdzić available traits dostarczoną przez bibliotekę standardową.

4

Można wysyła wywołanie jak następuje:

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

Metoda first jest preferowane nad second jeśli klasa ma oboje.
W przeciwnym razie, func wykorzystuje funkcję przeciążania, aby przetestować obie metody i wybrać właściwą.
Ta technika nazywa się sfinae, użyj tej nazwy do wyszukiwania w Internecie w celu uzyskania dalszych szczegółów.

5

tak wiele opcji dostępnych w języku C++.

Preferuję preferowanie darmowych funkcji i zwracanie poprawnych wyników.

#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

Oto mała biblioteka, która pomaga określić, czy członek istnieje.

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

Teraz możemy napisać ma pierwszy i drugi ma łatwo:

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

i podobnie dla second.

Teraz mamy naszą metodę. Chcemy zadzwonić pierwszy lub drugi.

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&>{}); 
} 

nazywane jest to wysyłaniem tagów.

method dzwoni pod numer method_first, który określa, czy T& można wywołać za pomocą .first(). Jeśli to możliwe, dzwoni do tego, który dzwoni pod numer .first().. Jeśli nie może, wywołuje ten, który przekazuje numer method_second i sprawdza, czy ma on .second().

Jeśli nie ma żadnej, wywołuje funkcję =delete, która generuje komunikat o błędzie podczas kompilacji.

Istnieje wiele, wiele, wiele sposobów, aby to zrobić. Osobiście lubię wysyłanie tagów, ponieważ można uzyskać lepsze komunikaty o błędach z powodu braku dopasowania niż SFIANE generuje.

w C++ 17 może być bardziej bezpośredni:

template<class T> 
void method(T & t) { 
    if constexpr (has_first<T&>{}) { 
    t.first(); 
    } 
    if constexpr (has_second<T&>{}) { 
    t.second(); 
    } 
}