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ị x
và y
như 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ụ int
và double
), 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:
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 int
s và double
s bằng cách sử dụng cùng một ký hiệu x += 2
và 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ụ int
và double
), 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 double
và 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 42
sang 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 int
hoặc double
hoạ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ạ:
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.