Đa hình trong C ++


129

AFAIK:

C ++ cung cấp ba loại đa hình khác nhau.

  • Chức năng ảo
  • Quá tải tên hàm
  • Quá tải toán tử

Ngoài ba loại đa hình trên, còn tồn tại các loại đa hình khác:

  • thời gian chạy
  • thời gian biên dịch
  • đa hình ad-hoc
  • đa hình tham số

Tôi biết rằng đa hình thời gian chạy có thể đạt được bằng các hàm ảođa hình tĩnh có thể đạt được bằng các hàm mẫu

Nhưng đối với hai người kia

đa hình ad-hoc:

Nếu phạm vi của các loại thực tế có thể được sử dụng là hữu hạn và các kết hợp phải được chỉ định riêng trước khi sử dụng, đây được gọi là đa hình ad-hoc.

đa hình tham số:

Nếu tất cả các mã được viết mà không đề cập đến bất kỳ loại cụ thể nào và do đó có thể được sử dụng trong suốt với bất kỳ số loại mới nào, nó được gọi là đa hình tham số.

Tôi khó có thể hiểu chúng :(

bất cứ ai có thể giải thích cả hai nếu có thể với một ví dụ? Tôi hy vọng câu trả lời cho câu hỏi này sẽ hữu ích cho nhiều người mới vượt qua từ trường đại học của họ.


30
Trên thực tế, C ++ có bốn loại đa hình: tham số (tính tổng quát thông qua các mẫu trong C ++), bao gồm (phân nhóm thông qua các phương thức ảo trong C ++), quá tải và ép buộc (chuyển đổi ngầm). Về mặt khái niệm, có rất ít sự khác biệt giữa quá tải chức năng và quá tải toán tử.
dòng chảy

Vì vậy, có vẻ như trang web tôi đã đề cập là sai lệch nhiều..tôi có đúng không?
Vijay

@zombie: trang web đó chạm vào rất nhiều khái niệm hay, nhưng không chính xác và nhất quán trong việc sử dụng thuật ngữ của nó (ví dụ: một khi nó bắt đầu nói về đa hình ảo / đa thời gian chạy, nó đưa ra rất nhiều tuyên bố về đa hình sai nói chung nhưng đúng với công văn ảo). Nếu bạn đã hiểu chủ đề, bạn có thể liên quan đến những gì được nói và tinh thần chèn những lời cảnh báo cần thiết, nhưng thật khó để đến đó bằng cách đọc trang web ....
Tony Delroy

Một số thuật ngữ gần như đồng nghĩa, hoặc liên quan nhiều hơn nhưng bị hạn chế hơn các thuật ngữ khác. Ví dụ, thuật ngữ "đa hình ad-hoc" chủ yếu được sử dụng trong Haskell theo kinh nghiệm của tôi, tuy nhiên "các hàm ảo" có liên quan rất chặt chẽ. Sự khác biệt nhỏ là "các hàm ảo" là một thuật ngữ hướng đối tượng đề cập đến các hàm thành viên với "ràng buộc muộn". "Nhiều công văn" cũng là một loại đa hình đặc biệt. Và như FredOverflow nói, cả hai toán tử và chức năng nạp chồng về cơ bản là giống nhau.
Steve314

Tôi đã sửa định dạng của bạn cho bạn. Vui lòng đọc trợ giúp có sẵn ở bên phải của khung chỉnh sửa. Ai đó có> 200 câu hỏi và> 3k nên biết những thứ cơ bản này. Ngoài ra, bạn có thể muốn mua một bàn phím mới. Phím shift này dường như bị lỗi liên tục. Ồ, và: không có thứ gọi là "hàm mẫu" trong C ++. Tuy nhiên, có các mẫu chức năng .
sbi

Câu trả lời:


219

Hiểu biết về / yêu cầu đối với đa hình

Để hiểu tính đa hình - như thuật ngữ được sử dụng trong Khoa học máy tính - nó giúp bắt đầu từ một bài kiểm tra đơn giản và định nghĩa về nó. Xem xét:

    Type1 x;
    Type2 y;

    f(x);
    f(y);

Ở đây, f()là để thực hiện một số hoạt động và đang được đưa ra các giá trị xynhư là đầu vào.

Để thể hiện tính đa hình, f()phải có khả năng hoạt động với các giá trị của ít nhất hai loại riêng biệt (ví dụ intdouble), tìm và thực thi mã phù hợp với loại khác biệt.


Cơ chế C ++ cho đa hình

Đa hình lập trình cụ thể

Bạn có thể viết f()sao cho nó có thể hoạt động trên nhiều loại theo bất kỳ cách nào sau đây:

  • Sơ chế:

    #define f(X) ((X) += 2)
    // (note: in real code, use a longer uppercase name for a macro!)
    
  • Quá tải:

    void f(int& x)    { x += 2; }
    
    void f(double& x) { x += 2; }
    
  • Mẫu:

    template <typename T>
    void f(T& x) { x += 2; }
    
  • Công văn ảo:

    struct Base { virtual Base& operator+=(int) = 0; };
    
    struct X : Base
    {
        X(int n) : n_(n) { }
        X& operator+=(int n) { n_ += n; return *this; }
        int n_;
    };
    
    struct Y : Base
    {
        Y(double n) : n_(n) { }
        Y& operator+=(int n) { n_ += n; return *this; }
        double n_;
    };
    
    void f(Base& x) { x += 2; } // run-time polymorphic dispatch
    

Các cơ chế liên quan khác

Tính đa hình do trình biên dịch cung cấp cho các loại dựng sẵn, chuyển đổi tiêu chuẩn và đúc / ép buộc sẽ được thảo luận sau để hoàn thiện như sau:

  • dù sao họ thường hiểu theo trực giác (đảm bảo phản ứng " oh, điều đó "),
  • chúng tác động đến ngưỡng yêu cầu và tính liền mạch trong việc sử dụng các cơ chế trên và
  • giải thích là một sự phân tâm khó khăn từ các khái niệm quan trọng hơn.

Thuật ngữ

Phân loại thêm

Với các cơ chế đa hình ở trên, chúng ta có thể phân loại chúng theo nhiều cách khác nhau:

  • Khi nào mã đa hình loại cụ thể được chọn?

    • Thời gian chạy có nghĩa là trình biên dịch phải tạo mã cho tất cả các loại chương trình có thể xử lý trong khi chạy và tại thời điểm chạy, mã chính xác được chọn (công văn ảo )
    • Thời gian biên dịch có nghĩa là sự lựa chọn mã loại cụ thể được thực hiện trong quá trình biên dịch. Hậu quả của việc này: giả sử một chương trình chỉ được gọi fở trên với các intđối số - tùy thuộc vào cơ chế đa hình được sử dụng và các lựa chọn nội tuyến, trình biên dịch có thể tránh tạo ra bất kỳ mã nào f(double), hoặc mã được tạo ra có thể bị vứt bỏ tại một số điểm trong quá trình biên dịch hoặc liên kết. ( tất cả các cơ chế trên trừ công văn ảo )

  • Những loại nào được hỗ trợ?

    • Ad-hoc có nghĩa là bạn cung cấp mã rõ ràng để hỗ trợ từng loại (ví dụ: quá tải, chuyên môn hóa mẫu); bạn rõ ràng thêm hỗ trợ "cho loại này" (theo ý nghĩa của ad hoc ), một số "cái này" khác và có thể "cái đó" nữa ;-).
    • Ý nghĩa tham số, bạn chỉ có thể thử sử dụng hàm cho các loại tham số khác nhau mà không cần làm gì cụ thể để hỗ trợ cho chúng (ví dụ: mẫu, macro). Một đối tượng với các hàm / toán tử hoạt động như mẫu / macro mong đợi 1 tất cả các mẫu / macro cần để thực hiện công việc của nó, với loại chính xác là không liên quan. Các "khái niệm" được giới thiệu bởi C ++ 20 thể hiện và thực thi những kỳ vọng như vậy - xem trang cppreference tại đây .

      • Đa hình tham số cung cấp cách gõ vịt - một khái niệm được gán cho James Whitcomb Riley, người dường như đã nói "Khi tôi nhìn thấy một con chim đi như vịt và bơi như vịt và quạ như vịt, tôi gọi con chim đó là vịt". .

        template <typename Duck>
        void do_ducky_stuff(const Duck& x) { x.walk().swim().quack(); }
        
        do_ducky_stuff(Vilified_Cygnet());
        
    • Tính đa hình của tiểu loại (còn gọi là bao gồm) cho phép bạn làm việc trên các loại mới mà không cần cập nhật thuật toán / hàm, nhưng chúng phải được lấy từ cùng một lớp cơ sở (công văn ảo)

1 - Mẫu cực kỳ linh hoạt. SFINAE (xem thêm std::enable_if) có hiệu quả cho phép một số bộ kỳ vọng cho đa hình tham số. Ví dụ: bạn có thể mã hóa rằng khi loại dữ liệu bạn đang xử lý có .size()thành viên, bạn sẽ sử dụng một chức năng, nếu không, một chức năng khác không cần .size()(nhưng có lẽ bị ảnh hưởng theo một cách nào đó - ví dụ: sử dụng chậm hơn strlen()hoặc không in như hữu ích một thông điệp trong nhật ký). Bạn cũng có thể chỉ định các hành vi đặc biệt khi mẫu được khởi tạo với các tham số cụ thể, để lại một số tham số tham số ( chuyên môn mẫu một phần ) hoặc không ( chuyên môn hóa đầy đủ ).

"Đa hình"

Alf Steinbach nhận xét rằng trong đa hình chuẩn C ++ chỉ đề cập đến đa hình thời gian chạy bằng cách sử dụng công văn ảo. Tổng hợp Khoa học. nghĩa là bao gồm nhiều hơn, theo thuật ngữ của người sáng tạo C ++ Bjarne Stroustrup '( http://www.stroustrup.com/glossary.html ):

đa hình - cung cấp một giao diện duy nhất cho các thực thể thuộc các loại khác nhau. Các hàm ảo cung cấp tính đa hình động (thời gian chạy) thông qua một giao diện được cung cấp bởi một lớp cơ sở. Các hàm và mẫu quá tải cung cấp đa hình tĩnh (thời gian biên dịch). TC ++ PL 12.2.6, 13.6.1, D & E 2.9.

Câu trả lời này - giống như câu hỏi - liên quan đến các tính năng của C ++ với Comp. Khoa học. thuật ngữ.

Thảo luận

Với Tiêu chuẩn C ++, sử dụng định nghĩa "đa hình" hẹp hơn so với Comp. Khoa học. cộng đồng, để đảm bảo sự hiểu biết lẫn nhau cho khán giả của bạn xem xét ...

  • sử dụng thuật ngữ không rõ ràng ("chúng ta có thể sử dụng mã này cho các loại khác không?" hoặc "chúng ta có thể sử dụng công văn ảo không?" thay vì "chúng ta có thể tạo mã này đa hình không?") và / hoặc
  • xác định rõ thuật ngữ của bạn.

Tuy nhiên, điều cốt yếu để trở thành một lập trình viên C ++ tuyệt vời là hiểu được những gì đa hình thực sự làm cho bạn ...

    cho phép bạn viết mã "thuật toán" một lần và sau đó áp dụng nó cho nhiều loại dữ liệu

... Và sau đó hãy nhận thức được các cơ chế đa hình khác nhau phù hợp với nhu cầu thực tế của bạn như thế nào.

Phù hợp với đa hình thời gian chạy:

  • đầu vào được xử lý bằng các phương thức của nhà máy và phun ra như một bộ sưu tập đối tượng không đồng nhất được xử lý thông qua Base*s,
  • triển khai được chọn trong thời gian chạy dựa trên các tệp cấu hình, chuyển đổi dòng lệnh, cài đặt giao diện người dùng, v.v.
  • thực hiện khác nhau trong thời gian chạy, chẳng hạn như cho một mẫu máy trạng thái.

Khi không có trình điều khiển rõ ràng cho đa hình thời gian chạy, các tùy chọn biên dịch thời gian thường được ưa thích hơn. Xem xét:

  • khía cạnh biên dịch-cái được gọi là của các lớp templated thích hợp hơn cho các giao diện chất béo bị lỗi khi chạy
  • SỮA
  • CRTP
  • tối ưu hóa (nhiều phần bao gồm nội tuyến và loại bỏ mã chết, không kiểm soát vòng lặp, mảng dựa trên ngăn xếp tĩnh so với heap)
  • __FILE__,, __LINE__nối chuỗi theo nghĩa đen và các khả năng độc đáo khác của macro (vẫn còn xấu ;-))
  • các mẫu và macro kiểm tra sử dụng ngữ nghĩa được hỗ trợ, nhưng không hạn chế một cách giả tạo cách cung cấp hỗ trợ đó (vì công văn ảo có xu hướng bằng cách yêu cầu ghi đè chính xác chức năng thành viên)

Các cơ chế khác hỗ trợ đa hình

Như đã hứa, để hoàn thiện, một số chủ đề ngoại vi được đề cập:

  • quá tải do trình biên dịch cung cấp
  • chuyển đổi
  • diễn viên / ép buộc

Câu trả lời này kết thúc với một cuộc thảo luận về cách kết hợp ở trên để trao quyền và đơn giản hóa mã đa hình - đặc biệt là đa hình tham số (mẫu và macro).

Các cơ chế ánh xạ tới các hoạt động cụ thể của loại

> Quá tải do trình biên dịch cung cấp

Về mặt khái niệm, trình biên dịch quá tải nhiều toán tử cho các kiểu dựng sẵn. Nó không khác biệt về mặt khái niệm với quá tải do người dùng chỉ định, nhưng được liệt kê vì nó dễ bị bỏ qua. Ví dụ: bạn có thể thêm vào ints và doubles bằng cách sử dụng cùng một ký hiệu x += 2và trình biên dịch tạo ra:

  • hướng dẫn CPU dành riêng cho từng loại
  • một kết quả cùng loại.

Quá tải sau đó mở rộng liên tục đến các loại do người dùng xác định:

std::string x;
int y = 0;

x += 'c';
y += 'c';

Quá tải do trình biên dịch cung cấp cho các loại cơ bản là phổ biến trong các ngôn ngữ máy tính cấp cao (3GL +) và thảo luận rõ ràng về đa hình nói chung ngụ ý điều gì đó nhiều hơn. (2GL - ngôn ngữ lắp ráp - thường yêu cầu lập trình viên sử dụng rõ ràng việc ghi nhớ khác nhau cho các loại khác nhau.)

> Chuyển đổi tiêu chuẩn

Phần thứ tư của Tiêu chuẩn C ++ mô tả các chuyển đổi Tiêu chuẩn.

Điểm đầu tiên tóm tắt độc đáo (từ một bản nháp cũ - hy vọng vẫn còn chính xác):

-1- Chuyển đổi tiêu chuẩn là các chuyển đổi ngầm định nghĩa cho các loại tích hợp. Điều khoản liệt kê toàn bộ các chuyển đổi như vậy. Trình tự chuyển đổi tiêu chuẩn là một chuỗi các chuyển đổi tiêu chuẩn theo thứ tự sau:

  • Không hoặc một chuyển đổi từ tập hợp sau: chuyển đổi lvalue-to-rvalue, chuyển đổi mảng thành con trỏ và chuyển đổi hàm thành con trỏ.

  • Không hoặc một chuyển đổi từ bộ sau: quảng cáo tích hợp, quảng cáo điểm nổi, chuyển đổi tích phân, chuyển đổi điểm nổi, chuyển đổi tích phân trôi nổi, chuyển đổi con trỏ, chuyển đổi con trỏ sang chuyển đổi thành viên và chuyển đổi boolean.

  • Không hoặc một chuyển đổi trình độ.

[Lưu ý: một chuỗi chuyển đổi tiêu chuẩn có thể trống, nghĩa là, nó có thể bao gồm không có chuyển đổi. ] Trình tự chuyển đổi tiêu chuẩn sẽ được áp dụng cho một biểu thức nếu cần thiết để chuyển đổi nó thành loại đích cần thiết.

Các chuyển đổi này cho phép mã như:

double a(double x) { return x + 2; }

a(3.14);
a(42);

Áp dụng thử nghiệm trước đó:

Để được đa hình, [ a()] phải có khả năng hoạt động với các giá trị của ít nhất hai loại riêng biệt (ví dụ intdouble), tìm và thực thi mã phù hợp với loại .

a()chính nó chạy mã đặc biệt cho doublevà do đó không phải là đa hình.

Nhưng, trong lệnh gọi thứ hai a(), trình biên dịch biết tạo mã phù hợp với kiểu cho "quảng cáo điểm nổi" (Tiêu chuẩn §4) để chuyển đổi 42sang 42.0. Mã bổ sung đó là trong chức năng gọi . Chúng ta sẽ thảo luận về ý nghĩa của điều này trong kết luận.

> Ép buộc, diễn viên, nhà xây dựng ngầm

Các cơ chế này cho phép các lớp do người dùng định nghĩa chỉ định các hành vi gần giống với các chuyển đổi Tiêu chuẩn của các kiểu dựng sẵn. Chúng ta hãy có một cái nhìn:

int a, b;

if (std::cin >> a >> b)
    f(a, b);

Ở đây, đối tượng std::cinđược đánh giá trong bối cảnh boolean, với sự trợ giúp của toán tử chuyển đổi. Điều này có thể được nhóm theo khái niệm với "quảng cáo tích hợp" et al từ các chuyển đổi Tiêu chuẩn trong chủ đề ở trên.

Các nhà xây dựng tiềm ẩn thực hiện một cách hiệu quả điều tương tự, nhưng được điều khiển bởi kiểu cast-to:

f(const std::string& x);
f("hello");  // invokes `std::string::string(const char*)`

Ý nghĩa của quá tải, chuyển đổi và ép buộc do trình biên dịch cung cấp

Xem xét:

void f()
{
    typedef int Amount;
    Amount x = 13;
    x /= 2;
    std::cout << x * 1.1;
}

Nếu chúng ta muốn số tiền xđược coi là số thực trong quá trình chia (tức là 6,5 thay vì làm tròn xuống 6), chúng ta chỉ cần thay đổi thành typedef double Amount.

Điều đó thật tuyệt, nhưng sẽ không có quá nhiều công việc để làm cho mã rõ ràng "gõ đúng":

void f()                               void f()
{                                      {
    typedef int Amount;                    typedef double Amount;
    Amount x = 13;                         Amount x = 13.0;
    x /= 2;                                x /= 2.0;
    std::cout << double(x) * 1.1;          std::cout << x * 1.1;
}                                      }

Nhưng, hãy xem xét rằng chúng ta có thể chuyển đổi phiên bản đầu tiên thành template:

template <typename Amount>
void f()
{
    Amount x = 13;
    x /= 2;
    std::cout << x * 1.1;
}

Đó là do những "tính năng tiện lợi" nhỏ đó có thể dễ dàng được tạo ngay lập tức cho hoặc inthoặc doublehoạt động như dự định. Nếu không có các tính năng này, chúng tôi sẽ cần các diễn viên rõ ràng, các đặc điểm loại và / hoặc các lớp chính sách, một số lỗi dài dòng, dễ bị lỗi như:

template <typename Amount, typename Policy>
void f()
{
    Amount x = Policy::thirteen;
    x /= static_cast<Amount>(2);
    std::cout << traits<Amount>::to_double(x) * 1.1;
}

Vì vậy, quá tải toán tử do trình biên dịch cung cấp cho các kiểu dựng sẵn, Chuyển đổi tiêu chuẩn, đúc / ép buộc / hàm tạo ẩn - tất cả đều đóng góp hỗ trợ tinh tế cho đa hình. Từ định nghĩa ở đầu câu trả lời này, họ giải quyết "tìm và thực thi mã phù hợp với loại" bằng cách ánh xạ:

  • "đi" khỏi các loại tham số

    • từ nhiều loại dữ liệu xử lý mã thuật toán đa hình

    • để mã bằng văn bản cho một (có khả năng thấp hơn) số (cùng hoặc khác) các loại.

  • "đến" các loại tham số từ các giá trị của loại không đổi

Họ không tự thiết lập bối cảnh đa hình, nhưng giúp trao quyền / đơn giản hóa mã bên trong các bối cảnh đó.

Bạn có thể cảm thấy bị lừa dối ... có vẻ như không nhiều. Điều quan trọng là trong các bối cảnh đa hình tham số (ví dụ bên trong các mẫu hoặc macro), chúng tôi đang cố gắng hỗ trợ một loạt các loại lớn tùy ý nhưng thường muốn thể hiện các hoạt động trên chúng theo các chức năng, nghĩa đen và hoạt động khác được thiết kế cho bộ nhỏ các loại. Nó giảm nhu cầu tạo các hàm hoặc dữ liệu gần giống nhau trên cơ sở mỗi loại khi hoạt động / giá trị giống nhau về mặt logic. Các tính năng này hợp tác để thêm thái độ "nỗ lực tốt nhất", thực hiện những gì được mong đợi bằng trực giác bằng cách sử dụng các chức năng và dữ liệu hạn chế và chỉ dừng lại với một lỗi khi có sự mơ hồ thực sự.

Điều này giúp hạn chế sự cần thiết của mã đa hình hỗ trợ mã đa hình, tạo ra một mạng lưới chặt chẽ hơn xung quanh việc sử dụng đa hình để sử dụng cục bộ không bắt buộc sử dụng rộng rãi và làm cho lợi ích của đa hình có sẵn khi cần mà không phải chịu chi phí thực hiện biên dịch thời gian, có nhiều bản sao của cùng một hàm logic trong mã đối tượng để hỗ trợ các loại đã sử dụng và thực hiện công văn ảo trái ngược với nội tuyến hoặc ít nhất là các cuộc gọi được giải quyết trong thời gian biên dịch. Như điển hình trong C ++, lập trình viên được trao rất nhiều quyền tự do để kiểm soát các ranh giới trong đó đa hình được sử dụng.


1
-1 Câu trả lời tuyệt vời ngoại trừ các cuộc thảo luận về thuật ngữ. Tiêu chuẩn C ++ định nghĩa thuật ngữ "đa hình" trong §1.8 / 1, trong đó đề cập đến phần 10.3 về các chức năng ảo. Vì vậy, không có phòng ngọ nguậy, không có chỗ để thảo luận, không có chỗ cho ý kiến ​​cá nhân: trong bối cảnh của C ++ tiêu chuẩn, thuật ngữ này được xác định một lần và mãi mãi. Và nó chơi một rô-bốt trong thực tế. Ví dụ, §5.2.7 / 6 about dynamic_castyêu cầu "con trỏ tới hoặc giá trị của loại đa hình". Chúc mừng & hth.,
Chúc mừng và hth. - Alf

@Alf: tài liệu tham khảo tuyệt vời - mặc dù tôi nghĩ rằng quan điểm của bạn quá hẹp. Rất rõ ràng từ quá tải danh sách câu hỏi, đa hình ad-hoc và đa hình tham số, vv rằng câu trả lời sẽ liên quan đến khả năng của C ++ với Comp chung. Khoa học. ý nghĩa của các điều khoản. Thật vậy, thuật ngữ của Stroustrup nói "đa hình - cung cấp một giao diện duy nhất cho các thực thể thuộc các loại khác nhau. Các hàm ảo cung cấp đa hình động (thời gian chạy) thông qua một giao diện được cung cấp bởi một lớp cơ sở. Các hàm và mẫu quá tải cung cấp đa hình tĩnh (thời gian biên dịch). TC ++ PL 12.2.6, 13.6.1, D & E 2.9. "
Tony Delroy

@Tony: không phải câu trả lời chính của bạn là sai. không sao đâu, nó thật tuyệt chỉ là vậy thôi. thuật ngữ bạn hiểu nó ngược: thuật ngữ học thuật chính thức là thuật ngữ hẹp được định nghĩa bởi Tiêu chuẩn quốc tế Holy, và thuật ngữ thô không chính thức trong đó mọi người có thể có nghĩa hơi khác nhau, là thuật ngữ chủ yếu được sử dụng trong câu hỏi và câu trả lời này. Chúc mừng & hth.,
Chúc mừng và hth. - Alf

@Alf: Tôi ước câu trả lời thật tuyệt - "Các cơ chế khác" cần được viết lại ở một phần năm dòng, và tôi đang suy nghĩ / phác thảo một tính năng cụ thể hơn - và - ngụ ý tương phản của các cơ chế đa hình. Dù sao, sự hiểu biết của tôi là học thuật chính thức dành riêng cho C ++ - ý nghĩa tập trung có thể hẹp, nhưng tổng quát về học thuật chính thức. Khoa học. ý nghĩa là không, bằng chứng là thuật ngữ của Stroustrup. Chúng ta cần một cái gì đó dứt khoát - ví dụ như định nghĩa từ Knuth - không có may mắn nào được googling. Tôi đánh giá cao bạn là một bậc thầy về C ++, nhưng bạn có thể chỉ ra bằng chứng thích hợp về điều này một cách cụ thể không?
Tony Delroy

1
@Alf: thứ hai, tôi tự tin rằng tính đa hình được định nghĩa chính thức trong bất kỳ Comp chung nào. Khoa học. cuốn sách theo cách (vượt thời gian, ổn định) tương thích với cách sử dụng của tôi (và của Stroustrup). Bài viết Wikipedia liên kết một số ấn phẩm học thuật định nghĩa theo cách đó: "Hàm đa hình là các hàm có toán hạng (tham số thực tế) có thể có nhiều hơn một loại. Kiểu đa hình là loại có hoạt động áp dụng cho các giá trị nhiều hơn một loại." (từ lucacardelli.name/Papers/OnUnder Hiểu.A4.pdf ). Vì vậy, câu hỏi là "ai nói cho Comp. Sci" ...?
Tony Delroy

15

Trong C ++, sự khác biệt quan trọng là thời gian chạy so với ràng buộc thời gian biên dịch. Ad-hoc so với parametric không thực sự có ích, như tôi sẽ giải thích sau.

|----------------------+--------------|
| Form                 | Resolved at  |
|----------------------+--------------|
| function overloading | compile-time |
| operator overloading | compile-time |
| templates            | compile-time |
| virtual methods      | run-time     |
|----------------------+--------------|

Lưu ý - tính đa hình trong thời gian chạy vẫn có thể được giải quyết tại thời gian biên dịch, nhưng đó chỉ là tối ưu hóa. Cần hỗ trợ giải quyết thời gian hiệu quả và chống lại các vấn đề khác, là một phần của những gì dẫn đến các chức năng ảo là những gì chúng là. Và đó thực sự là chìa khóa cho tất cả các dạng đa hình trong C ++ - mỗi dạng phát sinh từ các bộ đánh đổi khác nhau được thực hiện trong một bối cảnh khác nhau.

Quá tải chức năng và quá tải toán tử là điều tương tự trong mọi cách quan trọng. Tên và cú pháp sử dụng chúng không ảnh hưởng đến đa hình.

Mẫu cho phép bạn chỉ định nhiều quá tải chức năng cùng một lúc.

Có một bộ tên khác cho cùng một ý tưởng thời gian giải quyết ...

|---------------+--------------|
| early binding | compile-time |
| late binding  | run-time     |
|---------------+--------------|

Các tên này được liên kết nhiều hơn với OOP, do đó, hơi lạ khi nói rằng một mẫu hoặc hàm không phải thành viên khác sử dụng liên kết sớm.

Để hiểu rõ hơn về mối quan hệ giữa các chức năng ảo và quá tải chức năng, cũng rất hữu ích để hiểu sự khác biệt giữa "một công văn" và "nhiều công văn". Ý tưởng có thể được hiểu là một sự tiến bộ ...

  • Đầu tiên, có các chức năng đơn hình. Việc thực hiện chức năng được xác định duy nhất bởi tên chức năng. Không có thông số nào là đặc biệt.
  • Sau đó, có công văn duy nhất. Một trong các tham số được coi là đặc biệt và được sử dụng (cùng với tên) để xác định nên sử dụng triển khai nào. Trong OOP, chúng ta có xu hướng nghĩ tham số này là "đối tượng", liệt kê nó trước tên hàm, v.v.
  • Sau đó, có nhiều công văn. Bất kỳ / tất cả các tham số góp phần xác định việc thực hiện sẽ sử dụng. Do đó, một lần nữa, không có tham số nào cần phải đặc biệt.

Rõ ràng có nhiều điều để OOP hơn là một cái cớ để chỉ định một tham số là đặc biệt, nhưng đó là một phần của nó. Và liên quan đến những gì tôi đã nói về sự đánh đổi - công văn đơn lẻ khá dễ thực hiện một cách hiệu quả (cách thực hiện thông thường được gọi là "bảng ảo"). Nhiều công văn khó xử hơn, không chỉ về hiệu quả, mà còn cho việc biên dịch riêng biệt. Nếu bạn tò mò, bạn có thể tra cứu "vấn đề biểu hiện".

Cũng giống như việc sử dụng thuật ngữ "liên kết sớm" đối với các hàm không phải là thành viên hơi kỳ quặc, sẽ hơi kỳ quặc khi sử dụng thuật ngữ "một lần gửi" và "nhiều công văn" trong đó tính đa hình được giải quyết tại thời điểm biên dịch. Thông thường, C ++ được coi là không có nhiều công văn, được coi là một loại độ phân giải thời gian chạy cụ thể. Tuy nhiên, quá tải chức năng có thể được xem là nhiều công việc được thực hiện tại thời gian biên dịch.

Quay trở lại đa hình tham số so với ad-hoc, các thuật ngữ này phổ biến hơn trong lập trình chức năng và chúng không hoạt động hoàn toàn trong C ++. Ngay cả như vậy...

Đa hình tham số có nghĩa là bạn có các loại làm tham số và mã chính xác được sử dụng bất kể loại nào bạn sử dụng cho các tham số đó.

Đa hình ad-hoc là ad-hoc theo nghĩa là bạn cung cấp mã khác nhau tùy thuộc vào các loại cụ thể.

Quá tải và các hàm ảo là cả hai ví dụ về đa hình ad-hoc.

Một lần nữa, có một số từ đồng nghĩa ...

|------------+---------------|
| parametric | unconstrained |
| ad-hoc     | constrained   |
|------------+---------------|

Ngoại trừ những từ này không hoàn toàn đồng nghĩa, mặc dù chúng thường được đối xử như thể chúng là như vậy và đó là nơi mà sự nhầm lẫn có thể xuất hiện trong C ++.

Lý do đằng sau coi những điều này là từ đồng nghĩa là bằng cách hạn chế tính đa hình đối với các loại loại cụ thể, có thể sử dụng các hoạt động cụ thể cho các loại loại đó. Từ "các lớp" ở đây có thể được hiểu theo nghĩa OOP, nhưng thực sự chỉ đề cập đến (thường được đặt tên) các loại chia sẻ các hoạt động nhất định.

Vì vậy, đa hình tham số thường được lấy (ít nhất là theo mặc định) để ngụ ý đa hình không ràng buộc. Bởi vì cùng một mã được sử dụng bất kể các tham số loại, các hoạt động có thể hỗ trợ duy nhất là các hoạt động cho tất cả các loại. Bằng cách để tập hợp các loại không bị giới hạn, bạn hạn chế nghiêm ngặt tập hợp các hoạt động bạn có thể áp dụng cho các loại đó.

Ví dụ như Haskell, bạn có thể có ...

myfunc1 :: Bool -> a -> a -> a
myfunc1 c x y = if c then x else y

Đây alà một loại đa hình không ràng buộc. Nó có thể là bất cứ điều gì, vì vậy chúng ta không thể làm gì nhiều với các giá trị của loại đó.

myfunc2 :: Num a => a -> a
myfunc2 x = x + 3

Ở đây, abị hạn chế là thành viên của Numlớp - các loại hoạt động như số. Ràng buộc đó cho phép bạn thực hiện những điều tương tự với các giá trị đó, chẳng hạn như thêm chúng. Ngay cả 3số liệu suy luận đa hình - loại cũng chỉ ra rằng bạn có nghĩa là 3loại a.

Tôi nghĩ về điều này như đa hình tham số bị hạn chế. Chỉ có một triển khai, nhưng nó chỉ có thể được áp dụng trong các trường hợp bị ràng buộc. Khía cạnh đặc biệt là sự lựa chọn trong đó +3sử dụng. Mỗi "ví dụ" của Numnó có cách thực hiện riêng biệt của những điều này. Vì vậy, ngay cả trong Haskell "tham số" và "không ràng buộc" không thực sự đồng nghĩa - đừng đổ lỗi cho tôi, đó không phải là lỗi của tôi!

Trong C ++, cả hai chức năng quá tải và ảo đều là đa hình ad-hoc. Định nghĩa của đa hình ad-hoc không quan tâm đến việc triển khai được chọn vào thời gian chạy hay thời gian biên dịch.

C ++ rất gần với đa hình tham số với các mẫu nếu mỗi tham số mẫu có loại typename. Có các tham số loại và có một triển khai duy nhất cho dù loại nào được sử dụng. Tuy nhiên, quy tắc "Lỗi thay thế không phải là lỗi" có nghĩa là các ràng buộc ngầm phát sinh do sử dụng các hoạt động trong mẫu. Các biến chứng bổ sung bao gồm chuyên môn hóa mẫu để cung cấp các mẫu thay thế - triển khai (đặc biệt) khác nhau.

Vì vậy, theo một cách nào đó, C ++ có tính đa hình tham số, nhưng nó hoàn toàn bị hạn chế và có thể bị ghi đè bởi các lựa chọn thay thế đặc biệt - tức là cách phân loại này không thực sự hiệu quả với C ++.


+1 Rất nhiều điểm thú vị và hiểu biết. Tôi mới chỉ dành vài giờ để đọc về Haskell nên " ađây là loại đa hình không bị ràng buộc [...] vì vậy chúng ta không thể làm gì nhiều với các giá trị của loại đó." được quan tâm - trong C ++ sans Các khái niệm bạn không bị hạn chế chỉ thử một bộ hoạt động cụ thể trên một đối số của loại được chỉ định làm tham số mẫu ... các thư viện như khái niệm boost hoạt động theo cách khác - đảm bảo loại hỗ trợ các hoạt động bạn chỉ định, thay vì bảo vệ chống lại việc sử dụng ngẫu nhiên các hoạt động bổ sung.
Tony Delroy

@Tony - Các khái niệm là một cách để ràng buộc rõ ràng tính đa hình của các mẫu. Các ràng buộc ngầm rõ ràng sẽ không biến mất vì tính tương thích, nhưng các ràng buộc rõ ràng sẽ cải thiện đáng kể mọi thứ. Tôi khá chắc chắn rằng một số kế hoạch trước đây cho các khái niệm có phần nào đó liên quan đến các kiểu chữ Haskell, mặc dù tôi đã không nhìn sâu vào chúng và khi tôi nhìn "nông cạn", tôi không biết nhiều về Haskell.
Steve314

"Các ràng buộc ngầm rõ ràng sẽ không biến mất vì tính tương thích" - từ bộ nhớ, các khái niệm C ++ 0x đã làm (hứa với: - /) ngăn chặn "các ràng buộc ngầm" - bạn chỉ có thể sử dụng loại theo cách mà các khái niệm đã hứa.
Tony Delroy

2

Đối với đa hình ad-hoc, nó có nghĩa là quá tải chức năng hoặc quá tải toán tử. Kiểm tra tại đây:

http://en.wikipedia.org/wiki/Ad-hoc_polymorphism

Đối với đa hình tham số, các hàm mẫu cũng có thể được tính vì chúng không nhất thiết phải lấy tham số của các kiểu CỐ ĐỊNH. Ví dụ, một hàm có thể sắp xếp mảng các số nguyên và nó cũng có thể sắp xếp mảng các chuỗi, v.v.

http://en.wikipedia.org/wiki/Parametric_polymorphism


1
Thật không may, mặc dù chính xác, điều này là sai lệch. Các hàm mẫu có thể có các ràng buộc ngầm định do quy tắc SFINAE - sử dụng một thao tác trong khuôn mẫu sẽ ràng buộc đa hình - và chuyên môn hóa mẫu có thể cung cấp các mẫu thay thế đặc biệt ghi đè lên các mẫu chung hơn. Vì vậy, một mẫu (theo mặc định) cung cấp đa hình tham số không bị ràng buộc, nhưng không có sự thực thi nào về điều đó - có ít nhất hai cách nó có thể bị hạn chế hoặc đột xuất.
Steve314

Trong thực tế, ví dụ của bạn - sắp xếp - ngụ ý một ràng buộc. Sắp xếp chỉ hoạt động cho các loại được đặt hàng (tức là cung cấp các <toán tử tương tự và tương tự). Trong Haskell, bạn thể hiện rõ yêu cầu đó bằng cách sử dụng lớp Ord. Việc bạn nhận được một loại khác nhau <tùy thuộc vào loại cụ thể (như được cung cấp bởi ví dụ Ord) sẽ được coi là đa hình ad-hoc.
Steve314

2

Điều này có thể không giúp ích được gì, nhưng tôi đã thực hiện điều này để giới thiệu cho bạn bè của mình về lập trình bằng cách đưa ra các chức năng được xác định, như STARTENDcho chức năng chính để nó không quá khó khăn (họ chỉ sử dụng tệp main.cpp ). Nó chứa các lớp và cấu trúc đa hình, mẫu, vectơ, mảng, chỉ thị tiền xử lý, tình bạn, toán tử và con trỏ (tất cả những gì bạn có thể nên biết trước khi thử đa hình):

Lưu ý: Nó chưa hoàn thành, nhưng bạn có thể có được ý tưởng

main.cpp

#include "main.h"
#define ON_ERROR_CLEAR_SCREEN false
START
    Library MyLibrary;
    Book MyBook("My Book", "Me");
    MyBook.Summarize();
    MyBook += "Hello World";
    MyBook += "HI";
    MyBook.EditAuthor("Joe");
    MyBook.EditName("Hello Book");
    MyBook.Summarize();
    FixedBookCollection<FairyTale> FBooks("Fairytale Books");
    FairyTale MyTale("Tale", "Joe");
    FBooks += MyTale;
    BookCollection E("E");
    MyLibrary += E;
    MyLibrary += FBooks;
    MyLibrary.Summarize();
    MyLibrary -= FBooks;
    MyLibrary.Summarize();
    FixedSizeBookCollection<5> Collection("My Fixed Size Collection");
    /* Extension Work */ Book* Duplicate = MyLibrary.DuplicateBook(&MyBook);
    /* Extension Work */ Duplicate->Summarize();
END

chính

#include <iostream>
#include <sstream>
#include <vector>
#include <string>
#include <type_traits>
#include <array>
#ifndef __cplusplus
#error Not C++
#endif
#define START int main(void)try{
#define END GET_ENTER_EXIT return(0);}catch(const std::exception& e){if(ON_ERROR_CLEAR_SCREEN){system("cls");}std::cerr << "Error: " << e.what() << std::endl; GET_ENTER_EXIT return (1);}
#define GET_ENTER_EXIT std::cout << "Press enter to exit" << std::endl; getchar();
class Book;
class Library;
typedef std::vector<const Book*> Books;
bool sContains(const std::string s, const char c){
    return (s.find(c) != std::string::npos);
}
bool approve(std::string s){
    return (!sContains(s, '#') && !sContains(s, '%') && !sContains(s, '~'));
}
template <class C> bool isBook(){
    return (typeid(C) == typeid(Book) || std::is_base_of<Book, C>());
}
template<class ClassToDuplicate> class DuplicatableClass{ 
public:
    ClassToDuplicate* Duplicate(ClassToDuplicate ToDuplicate){
        return new ClassToDuplicate(ToDuplicate);
    }
};
class Book : private DuplicatableClass<Book>{
friend class Library;
friend struct BookCollection;
public:
    Book(const char* Name, const char* Author) : name_(Name), author_(Author){}
    void operator+=(const char* Page){
        pages_.push_back(Page);
    }
    void EditAuthor(const char* AuthorName){
        if(approve(AuthorName)){
            author_ = AuthorName;
        }
        else{
            std::ostringstream errorMessage;
            errorMessage << "The author of the book " << name_ << " could not be changed as it was not approved";
            throw std::exception(errorMessage.str().c_str());
        }
    }
    void EditName(const char* Name){
        if(approve(Name)){
            name_ = Name;
        }
        else{
            std::ostringstream errorMessage;
            errorMessage << "The name of the book " << name_ << " could not be changed as it was not approved";
            throw std::exception(errorMessage.str().c_str());
        }
    }
    virtual void Summarize(){
        std::cout << "Book called " << name_ << "; written by " << author_ << ". Contains "
            << pages_.size() << ((pages_.size() == 1) ? " page:" : ((pages_.size() > 0) ? " pages:" : " pages")) << std::endl;
        if(pages_.size() > 0){
            ListPages(std::cout);
        }
    }
private:
    std::vector<const char*> pages_;
    const char* name_;
    const char* author_;
    void ListPages(std::ostream& output){
        for(int i = 0; i < pages_.size(); ++i){
            output << pages_[i] << std::endl;
        }
    }
};
class FairyTale : public Book{
public:
    FairyTale(const char* Name, const char* Author) : Book(Name, Author){}
};
struct BookCollection{
friend class Library;
    BookCollection(const char* Name) : name_(Name){}
    virtual void operator+=(const Book& Book)try{
        Collection.push_back(&Book); 
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
    virtual void operator-=(const Book& Book){
        for(int i = 0; i < Collection.size(); ++i){
            if(Collection[i] == &Book){
                Collection.erase(Collection.begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The Book " << Book.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
private:
    const char* name_;
    Books Collection;
};
template<class FixedType> struct FixedBookCollection : public BookCollection{
    FixedBookCollection(const char* Name) : BookCollection(Name){
        if(!isBook<FixedType>()){
            std::ostringstream errorMessage;
            errorMessage << "The type " << typeid(FixedType).name() << " cannot be initialized as a FixedBookCollection";
            throw std::exception(errorMessage.str().c_str());
            delete this;
        }
    }
    void operator+=(const FixedType& Book)try{
        Collection.push_back(&Book); 
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
    void operator-=(const FixedType& Book){
        for(int i = 0; i < Collection.size(); ++i){
            if(Collection[i] == &Book){
                Collection.erase(Collection.begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The Book " << Book.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
private:
    std::vector<const FixedType*> Collection;
};
template<size_t Size> struct FixedSizeBookCollection : private std::array<const Book*, Size>{
    FixedSizeBookCollection(const char* Name) : name_(Name){ if(Size < 1){ throw std::exception("A fixed size book collection cannot be smaller than 1"); currentPos = 0; } }
    void operator+=(const Book& Book)try{
        if(currentPos + 1 > Size){
            std::ostringstream errorMessage;
            errorMessage << "The FixedSizeBookCollection " << name_ << "'s size capacity has been overfilled";
            throw std::exception(errorMessage.str().c_str());
        }
        this->at(currentPos++) = &Book;
    }catch(const std::exception& e){
        std::ostringstream errorMessage;
        errorMessage << e.what() << " - on line (approx.) " << (__LINE__ -3);
        throw std::exception(errorMessage.str().c_str());
    }
private:
    const char* name_;
    int currentPos;
};
class Library : private std::vector<const BookCollection*>{
public:
    void operator+=(const BookCollection& Collection){
        for(int i = 0; i < size(); ++i){
            if((*this)[i] == &Collection){
                std::ostringstream errorMessage;
                errorMessage << "The BookCollection " << Collection.name_ << " was already in the library, and therefore cannot be added";
                throw std::exception(errorMessage.str().c_str());
            }
        }
        push_back(&Collection);
    }
    void operator-=(const BookCollection& Collection){
        for(int i = 0; i < size(); ++i){
            if((*this)[i] == &Collection){
                erase(begin() + i);
                return;
            }
        }
        std::ostringstream errorMessage;
        errorMessage << "The BookCollection " << Collection.name_ << " was not found, and therefore cannot be erased";
        throw std::exception(errorMessage.str().c_str());
    }
    Book* DuplicateBook(Book* Book)const{
        return (Book->Duplicate(*Book));
    }
    void Summarize(){
        std::cout << "Library, containing " << size() << ((size() == 1) ? " book collection:" : ((size() > 0) ? " book collections:" : " book collections")) << std::endl;
        if(size() > 0){
            for(int i = 0; i < size(); ++i){
                std::cout << (*this)[i]->name_ << std::endl;
            }
        }
    }
};

1

Dưới đây là một ví dụ cơ bản sử dụng các lớp Đa hình

#include <iostream>

class Animal{
public:
   Animal(const char* Name) : name_(Name){/* Add any method you would like to perform here*/
    virtual void Speak(){
        std::cout << "I am an animal called " << name_ << std::endl;
    }
    const char* name_;
};

class Dog : public Animal{
public:
    Dog(const char* Name) : Animal(Name) {/*...*/}
    void Speak(){
        std::cout << "I am a dog called " << name_ << std::endl;
    }
};

int main(void){
    Animal Bob("Bob");
    Dog Steve("Steve");
    Bob.Speak();
    Steve.Speak();
    //return (0);
}

0

Đa hình có nghĩa là nhiều dạng như vậy nó được sử dụng cho một toán tử để hành động khác nhau trong các trường hợp khác nhau. Đa hình được sử dụng để thực hiện kế thừa. Đối với ex, chúng ta đã định nghĩa fn draw () cho hình dạng lớp sau đó vẽ fn có thể được thực hiện để vẽ hình tròn, hình hộp, hình tam giác và các hình dạng khác. (là các đối tượng của hình dạng lớp)


-3

Nếu có ai nói CẮT với những người này

The Surgeon
The Hair Stylist
The Actor

Chuyện gì sẽ xảy ra?

The Surgeon would begin to make an incision.
The Hair Stylist would begin to cut someone's hair.
The Actor would abruptly stop acting out of the current scene, awaiting directorial guidance.

Vì vậy, đại diện trên cho thấy tính đa hình (cùng tên, hành vi khác nhau) trong OOP là gì.

Nếu bạn đang đi phỏng vấn và người phỏng vấn yêu cầu bạn kể / đưa ra một ví dụ trực tiếp cho sự đa hình trong cùng một phòng chúng ta đang ngồi, hãy nói-

Trả lời - Cửa / Windows

Tự hỏi thế nào?

Thông qua Cửa / Cửa sổ - một người có thể đến, không khí có thể đến, ánh sáng có thể đến, mưa có thể đến, v.v.

tức là Một dạng hành vi khác nhau (Đa hình).

Để hiểu rõ hơn và theo cách đơn giản, tôi đã sử dụng ví dụ trên .. Nếu bạn cần tham khảo mã theo các câu trả lời ở trên.


Như tôi đã đề cập để hiểu rõ hơn về Đa hình trong c ++, tôi đã sử dụng ví dụ trên. Điều này có thể giúp một người mới thực sự hiểu và liên quan đến ý nghĩa hoặc những gì xảy ra đằng sau mã trong khi thực hiện tại cuộc phỏng vấn. Cảm ơn bạn!
Sanchit

op hỏi "đa hình trong c ++". Câu trả lời của bạn quá trừu tượng.
StahlRat
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.