Tại sao tôi nên tránh std :: enable_if trong chữ ký hàm


165

Scott Meyers đã đăng nội dung và trạng thái của cuốn sách tiếp theo EC ++ 11. Ông đã viết rằng một mục trong cuốn sách có thể là "Tránh std::enable_iftrong chữ ký chức năng" .

std::enable_if có thể được sử dụng làm đối số hàm, làm kiểu trả về hoặc làm mẫu lớp hoặc tham số mẫu hàm để loại bỏ có điều kiện các hàm hoặc lớp khỏi độ phân giải quá tải.

Trong câu hỏi này cả ba giải pháp được hiển thị.

Như tham số hàm:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

Như tham số mẫu:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

Như kiểu trả về:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • Giải pháp nào nên được ưu tiên và tại sao tôi nên tránh những người khác?
  • Trong trường hợp "Tránh std::enable_iftrong chữ ký hàm" liên quan đến việc sử dụng như kiểu trả về (không phải là một phần của chữ ký hàm thông thường mà là các chuyên môn mẫu)?
  • Có sự khác biệt nào đối với các mẫu hàm thành viên và không phải thành viên không?

Bởi vì quá tải chỉ là tốt đẹp, thường. Nếu có bất cứ điều gì, hãy ủy thác cho một triển khai sử dụng các mẫu lớp (chuyên biệt).
sehe

Các hàm thành viên khác nhau ở chỗ bộ quá tải bao gồm quá tải được khai báo sau khi quá tải hiện tại. Điều này đặc biệt quan trọng khi thực hiện kiểu biến đổi bị trì hoãn kiểu trả về (trong đó kiểu trả về sẽ được suy ra từ một tình trạng quá tải khác)
sehe

1
Chà, chỉ đơn thuần là chủ quan tôi phải nói rằng mặc dù thường khá hữu ích nhưng tôi không muốn std::enable_iflàm lộn xộn chữ ký chức năng của mình (đặc biệt là nullptrphiên bản đối số chức năng bổ sung xấu xí ) bởi vì nó luôn trông giống như một bản hack lạ (đối với một thứ gì đó static ifcó thể làm đẹp và sạch sẽ hơn nhiều) bằng cách sử dụng ma thuật đen mẫu để khai thác tính năng ngôn ngữ xen kẽ. Đây là lý do tại sao tôi thích gửi thẻ bất cứ khi nào có thể (tốt, bạn vẫn có thêm các đối số lạ, nhưng không có trong giao diện công cộng và cũng ít xấu xí và khó hiểu hơn nhiều ).
Christian Rau

2
Tôi muốn hỏi những gì làm =0trong typename std::enable_if<std::is_same<U, int>::value, int>::type = 0thành tựu? Tôi không thể tìm thấy tài nguyên chính xác để hiểu nó. Tôi biết phần đầu tiên trước đây =0có loại thành viên intnếu Uintgiống nhau. Cảm ơn nhiều!
astroboylrx

4
@astroboylrx Buồn cười, tôi vừa định bình luận về điều này. Về cơ bản, that = 0 chỉ ra rằng đây là tham số mẫu không mặc định, kiểu . Nó được thực hiện theo cách này vì các tham số mẫu loại mặc định không phải là một phần của chữ ký, vì vậy bạn không thể quá tải chúng.
Nir Friedman

Câu trả lời:


107

Đặt hack trong các tham số mẫu .

Cách enable_iftiếp cận tham số mẫu có ít nhất hai ưu điểm so với các cách khác:

  • khả năng đọc : việc sử dụng enable_if và các kiểu trả về / đối số không được hợp nhất với nhau thành một khối lộn xộn của bộ định dạng kiểu chữ và các truy cập kiểu lồng nhau; mặc dù sự lộn xộn của bộ phân tán và kiểu lồng nhau có thể được giảm thiểu bằng các mẫu bí danh, nhưng vẫn sẽ hợp nhất hai thứ không liên quan lại với nhau. Việc sử dụng enable_if có liên quan đến các tham số mẫu không liên quan đến các kiểu trả về. Có chúng trong các tham số mẫu có nghĩa là chúng gần với những gì quan trọng hơn;

  • khả năng áp dụng phổ biến : các nhà xây dựng không có kiểu trả về và một số toán tử không thể có thêm đối số, vì vậy cả hai tùy chọn khác đều không thể được áp dụng ở mọi nơi. Đặt enable_if trong một tham số mẫu hoạt động ở mọi nơi vì bạn chỉ có thể sử dụng SFINAE trên các mẫu.

Đối với tôi, khía cạnh dễ đọc là yếu tố thúc đẩy lớn trong sự lựa chọn này.


4
Sử dụng FUNCTION_REQUIRESmacro ở đây , giúp đọc dễ dàng hơn nhiều và nó cũng hoạt động trong trình biên dịch C ++ 03, và nó phụ thuộc vào việc sử dụng enable_iftrong kiểu trả về. Ngoài ra, việc sử dụng enable_iftrong các tham số mẫu hàm gây ra sự cố cho quá tải, vì hiện tại chữ ký hàm không phải là duy nhất gây ra lỗi quá tải mơ hồ.
Paul Fultz II

3
Đây là một câu hỏi cũ, nhưng đối với bất kỳ ai vẫn đọc: giải pháp cho vấn đề được nêu ra bởi @Paul là sử dụng enable_ifvới tham số mẫu không loại mặc định, cho phép quá tải. Tức là enable_if_t<condition, int> = 0thay vì typename = enable_if_t<condition>.
Nir Friedman

Wayback liên kết với hầu hết tĩnh-nếu: web.archive.org/web/20150726012736/http://flamingdangerzone.com/...
davidbak

@ R.MartinhoFernand flamingdangerzoneliên kết trong bình luận của bạn dường như dẫn đến một trang cài đặt phần mềm gián điệp bây giờ. Tôi đánh dấu nó cho sự chú ý của người điều hành.
nispio

58

std::enable_ifdựa vào nguyên tắc " Lỗi thay thế không phải là lỗi " (hay còn gọi là SFINAE) trong quá trình khấu trừ đối số mẫu . Đây là một tính năng ngôn ngữ rất mong manh và bạn cần phải rất cẩn thận để làm cho đúng.

  1. nếu điều kiện của bạn bên trong enable_ifchứa một mẫu hoặc định nghĩa kiểu lồng nhau (gợi ý: tìm kiếm ::mã thông báo), thì độ phân giải của các loại hoặc các tempat lồng nhau này thường là một bối cảnh không suy diễn . Bất kỳ thất bại thay thế trên một bối cảnh không suy diễn như vậy là một lỗi .
  2. các điều kiện khác nhau trong nhiều enable_iftình trạng quá tải không thể có bất kỳ sự chồng chéo nào vì độ phân giải quá tải sẽ không rõ ràng. Đây là điều mà bạn với tư cách là một tác giả cần tự kiểm tra, mặc dù bạn sẽ nhận được các cảnh báo trình biên dịch tốt.
  3. enable_ifthao tác tập hợp các hàm khả thi trong quá trình phân giải quá tải, có thể có các tương tác đáng ngạc nhiên tùy thuộc vào sự hiện diện của các chức năng khác được đưa vào từ các phạm vi khác (ví dụ thông qua ADL). Điều này làm cho nó không mạnh mẽ lắm.

Nói tóm lại, khi nó hoạt động, nó hoạt động, nhưng khi nó không hoạt động thì có thể rất khó gỡ lỗi. Một cách khác rất tốt là sử dụng việc gửi thẻ , tức là ủy quyền cho một hàm thực hiện (thường là trong một detailkhông gian tên hoặc trong một lớp trợ giúp) nhận được một đối số giả dựa trên cùng một điều kiện thời gian biên dịch mà bạn sử dụng trong enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

Gửi thẻ không thao tác tập quá tải, nhưng giúp bạn chọn chính xác chức năng bạn muốn bằng cách cung cấp các đối số phù hợp thông qua biểu thức thời gian biên dịch (ví dụ: trong một đặc điểm loại). Theo kinh nghiệm của tôi, điều này dễ dàng hơn nhiều để gỡ lỗi và làm cho đúng. Nếu bạn là một nhà văn thư viện khao khát những đặc điểm kiểu tinh vi, bạn có thể cần enable_ifbằng cách nào đó, nhưng đối với việc sử dụng thường xuyên các điều kiện thời gian biên dịch thì không nên.


22
Việc gửi thẻ có một nhược điểm: nếu bạn có một số đặc điểm phát hiện sự hiện diện của chức năng và chức năng đó được thực hiện theo phương pháp gửi thẻ, nó luôn báo cáo thành viên đó như hiện tại và dẫn đến lỗi thay vì lỗi thay thế tiềm năng . SFINAE chủ yếu là một kỹ thuật để loại bỏ quá tải khỏi các bộ ứng cử viên và gửi thẻ là một kỹ thuật để chọn giữa hai (hoặc nhiều hơn) quá tải. Có một số chồng chéo trong chức năng, nhưng chúng không tương đương.
R. Martinho Fernandes

@ R.MartinhoFernandes bạn có thể đưa ra một ví dụ ngắn, và minh họa làm thế nào enable_ifđể làm cho đúng?
TemplateRex

1
@ R.MartinhoFernandes Tôi nghĩ rằng một câu trả lời riêng biệt giải thích những điểm này có thể tăng thêm giá trị cho OP. :-) BTW, viết các đặc điểm như is_f_ablelà điều mà tôi coi là nhiệm vụ cho các nhà văn thư viện, những người tất nhiên có thể sử dụng SFINAE khi điều đó mang lại lợi thế cho họ, nhưng đối với người dùng "thông thường" và đưa ra một đặc điểm is_f_able, tôi nghĩ việc gửi thẻ dễ dàng hơn.
TemplateRex

1
@hansmaad Tôi đã đăng một câu trả lời ngắn giải quyết câu hỏi của bạn và sẽ giải quyết vấn đề "gửi SFINAE hoặc không SFINAE" trong một bài đăng trên blog thay vào đó (nó hơi lạc đề về câu hỏi này). Ngay khi tôi có thời gian để hoàn thành nó, ý tôi là.
R. Martinho Fernandes

8
SFINAE có "mong manh"? Gì?
Các cuộc đua nhẹ nhàng trong quỹ đạo

5

Giải pháp nào nên được ưu tiên và tại sao tôi nên tránh những người khác?

  • Tham số mẫu

    • Nó có thể sử dụng trong Con constructor.
    • Nó có thể sử dụng trong toán tử chuyển đổi do người dùng định nghĩa.
    • Nó đòi hỏi C ++ 11 trở lên.
    • Đó là IMO, càng dễ đọc hơn.
    • Nó có thể dễ dàng được sử dụng sai và tạo ra lỗi với quá tải:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()
      

    Thông báo typename = std::enable_if_t<cond>thay vì đúngstd::enable_if_t<cond, int>::type = 0

  • loại trả về:

    • Nó không thể được sử dụng trong constructor. (không có loại trả lại)
    • Nó không thể được sử dụng trong toán tử chuyển đổi do người dùng định nghĩa. (không được khấu trừ)
    • Nó có thể được sử dụng trước C ++ 11.
    • IMO thứ hai dễ đọc hơn.
  • Cuối cùng, trong tham số chức năng:

    • Nó có thể được sử dụng trước C ++ 11.
    • Nó có thể sử dụng trong Con constructor.
    • Nó không thể được sử dụng trong toán tử chuyển đổi do người dùng định nghĩa. (không có tham số)
    • Nó không thể được sử dụng trong phương pháp với số lượng cố định của đối số (unary / khai thác nhị phân +, -, *, ...)
    • Nó có thể được sử dụng một cách an toàn trong thừa kế (xem bên dưới).
    • Thay đổi chữ ký hàm (về cơ bản bạn có thêm một đối số cuối cùng void* = nullptr) (vì vậy con trỏ hàm sẽ khác nhau, v.v.)

Có sự khác biệt nào đối với các mẫu hàm thành viên và không phải thành viên không?

Có sự khác biệt tinh tế với thừa kế và using:

Theo using-declarator(nhấn mạnh của tôi):

không gian tên.udecl

Tập hợp các khai báo được giới thiệu bởi người khai báo sử dụng được tìm thấy bằng cách thực hiện tra cứu tên đủ điều kiện ([basic.lookup.qual], [class.member.lookup]) cho tên trong trình khai báo sử dụng, ngoại trừ các hàm được ẩn như mô tả phía dưới.

...

Khi một người khai báo sử dụng đưa các khai báo từ một lớp cơ sở vào một lớp dẫn xuất, các hàm thành viên và các mẫu hàm thành viên trong lớp dẫn xuất ghi đè và / hoặc ẩn các hàm thành viên và các mẫu hàm thành viên có cùng tên, danh sách kiểu tham số, cv- trình độ chuyên môn và trình độ tham khảo (nếu có) trong một lớp cơ sở (thay vì xung đột). Các khai báo ẩn hoặc ghi đè như vậy được loại trừ khỏi tập hợp các khai báo được giới thiệu bởi người khai báo sử dụng.

Vì vậy, đối với cả đối số khuôn mẫu và kiểu trả về, các phương thức được ẩn theo kịch bản sau:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Demo (gcc tìm sai hàm cơ sở).

Trong khi với đối số, kịch bản tương tự hoạt động:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

Bản giới thiệu

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.