2016-08-28 18 views
14

Gần đây tôi đã lập trình rất nhiều trong Java, bây giờ tôi đang quay trở lại với gốc C++ của mình (tôi thực sự bắt đầu thiếu các lỗi con trỏ và phân đoạn). Biết rằng C++ có sự hỗ trợ rộng rãi cho các khuôn mẫu, tôi đã tự hỏi liệu nó có một số khả năng của Java có thể hữu ích cho việc viết mã tổng quát hay không. Giả sử tôi có hai nhóm lớp học. Một trong số họ có phương thức first(), phương thức còn lại có phương thức second(). Có cách nào chuyên về các mẫu được trình biên dịch chọn tùy thuộc vào các phương thức mà một lớp sở hữu không? Tôi đang hướng tới hành vi đó là tương tự như một trong Java:Chuyên mẫu dựa trên phương pháp

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

đâu FirstSecond là giao diện. Tôi biết tôi có thể nhóm cả hai nhóm này bằng cách bắt nguồn từ mỗi nhóm từ một tầng lớp thượng lưu, nhưng không phải lúc nào cũng có thể (không có autoboxing trong C++ và một số lớp không kế thừa từ một tổ tiên chung).

Một ví dụ điển hình về nhu cầu của tôi là thư viện STL, trong đó một số lớp học có các phương pháp như push() và một số khác có insert() hoặc push_back(). Cho phép nói rằng tôi muốn tạo một hàm mà phải chèn nhiều giá trị vào một vùng chứa bằng cách sử dụng hàm variadic. Trong Java, nó dễ thực hiện bởi vì các bộ sưu tập có một tổ tiên chung. Mặt khác, trong C++ không phải lúc nào cũng vậy. Tôi đã thử nó bởi vịt đánh máy, nhưng trình biên dịch mang lại một thông báo lỗi:

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

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

Vì vậy, câu hỏi của tôi là: Sản phẩm thực hiện hành vi như vậy càng tốt mà không tạo mã boileplate không cần thiết bởi chuyên mọi trường hợp duy nhất?

+1

Bạn có thể sử dụng một lớp chính sách như một mẫu tham số, và kế thừa từ nó. Sau đó, bạn có triển khai chính sách cho các loại vùng chứa khác nhau. Xem thêm [Thiết kế dựa trên chính sách] (https://en.wikipedia.org/wiki/Policy-based_design) –

+0

Bạn có thể chỉ cho tôi một ví dụ về cách điều này có thể khắc phục được sự cố không? Bởi vì có vẻ như nó đáng để thử. –

+0

Có một mẫu tại bài viết wikipedia, và nếu bạn tìm kiếm những từ này bạn sẽ tìm thấy nhiều hơn nữa. –

Trả lời

16

Thay vì <T extends First>, bạn sẽ sử dụng thứ gì đó mà chúng tôi gọi là sfinae. Đây là một kỹ thuật về việc thêm các constaints vào một hàm dựa trên các kiểu tham số.

Đây là cách bạn muốn làm điều đó trong 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(); 
} 

Đối với các chức năng để tồn tại, trình biên dịch sẽ cần loại argument.second() hoặc loại argument.first(). Nếu biểu thức không mang lại một loại (ví dụ: T không có chức năng first()), trình biên dịch sẽ thử một quá tải khác.

void_t được thực hiện như sau:

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

Một điều tuyệt vời là nếu bạn có lớp học như:

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

Sau đó trình biên dịch sẽ có hiệu quả cho bạn biết rằng cuộc gọi dễ nhầm lẫn vì các loại phù hợp với cả hai ràng buộc.


Nếu bạn thực sự muốn kiểm tra nếu một kiểu mở rộng khác (hoặc thực hiện, trong C++ nó là điều tương tự), bạn có thể sử dụng các loại đặc điểm 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(); 
} 

Để đọc thêm về chủ đề này, kiểm tra sfinae trên cpprefence và bạn có thể kiểm tra available traits do thư viện chuẩn cung cấp.

4

Bạn có thể gửi các cuộc gọi vì nó sau:

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

Phương pháp first được ưa thích hơn second nếu một lớp học có cả trong số họ.
Nếu không, func sử dụng quá tải hàm để kiểm tra hai phương pháp và chọn phương thức phù hợp.
Kỹ thuật này được gọi là sfinae, sử dụng tên này để tìm kiếm trên web để biết thêm chi tiết.

5

rất nhiều tùy chọn có sẵn trong C++.

Tùy chọn của tôi là ưu tiên các chức năng miễn phí và trả lại bất kỳ loại kết quả nào chính xá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; 

} 
3

Đây là thư viện nhỏ giúp bạn xác định xem thành viên có tồn tại không.

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

Bây giờ chúng ta có thể viết có đầu tiên và có thứ hai một cách dễ dàng:

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

và tương tự cho second.

Bây giờ chúng tôi có phương pháp của chúng tôi. Chúng tôi muốn gọi đầu tiên hoặc thứ hai.

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

điều này được gọi là gửi thẻ.

method gọi số method_first được xác định nếu T& có thể được gọi với .first(). Nếu có, điện thoại gọi số gọi đến .first().

Nếu không, điện thoại sẽ gọi số chuyển tiếp đến method_second và kiểm tra xem điện thoại có .second() hay không.

Nếu không có, nó gọi hàm =delete, tạo ra thông báo lỗi tại thời gian biên dịch.

Có rất nhiều, nhiều, nhiều cách để thực hiện việc này. Cá nhân tôi thích gửi thẻ vì bạn có thể nhận được thông báo lỗi tốt hơn do không khớp với SFIANE.

Trong C++ 17 bạn có thể trực tiếp hơn:

template<class T> 
void method(T & t) { 
    if constexpr (has_first<T&>{}) { 
    t.first(); 
    } 
    if constexpr (has_second<T&>{}) { 
    t.second(); 
    } 
} 
Các vấn đề liên quan