Toán tử thường gặp quá tải
Hầu hết các công việc trong các nhà khai thác quá tải là mã nồi hơi. Đó là một thắc mắc nhỏ, vì các nhà khai thác chỉ là đường cú pháp, công việc thực tế của họ có thể được thực hiện bằng (và thường được chuyển tiếp đến) các hàm đơn giản. Nhưng điều quan trọng là bạn có được mã nồi hơi này ngay. Nếu bạn thất bại, mã của nhà điều hành của bạn sẽ không được biên dịch hoặc mã của người dùng của bạn sẽ không được biên dịch hoặc mã của người dùng của bạn sẽ hoạt động một cách đáng ngạc nhiên.
Toán tử chuyển nhượng
Có rất nhiều điều để nói về sự phân công. Tuy nhiên, hầu hết đã được nói trong Câu hỏi thường gặp về Sao chép và Hoán đổi nổi tiếng của GMan , vì vậy tôi sẽ bỏ qua phần lớn ở đây, chỉ liệt kê toán tử gán hoàn hảo để tham khảo:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Toán tử Bitshift (được sử dụng cho Luồng I / O)
Các toán tử bithift <<
và >>
, mặc dù vẫn được sử dụng trong giao diện phần cứng cho các hàm thao tác bit mà chúng thừa hưởng từ C, đã trở nên phổ biến hơn khi các toán tử đầu vào và đầu ra luồng quá tải trong hầu hết các ứng dụng. Để biết quá tải hướng dẫn dưới dạng toán tử thao tác bit, xem phần bên dưới trên Toán tử số học nhị phân. Để thực hiện định dạng tùy chỉnh và phân tích cú pháp logic của riêng bạn khi đối tượng của bạn được sử dụng với iostreams, hãy tiếp tục.
Các toán tử luồng, trong số các toán tử quá tải phổ biến nhất, là các toán tử infix nhị phân mà cú pháp chỉ định không hạn chế về việc chúng nên là thành viên hay không phải thành viên. Vì họ thay đổi đối số bên trái (họ thay đổi trạng thái của luồng), nên theo quy tắc ngón tay cái, họ nên được thực hiện như là thành viên của loại toán hạng bên trái của họ. Tuy nhiên, các toán hạng bên trái của chúng là các luồng từ thư viện tiêu chuẩn và trong khi hầu hết các toán tử đầu vào và đầu ra luồng được xác định bởi thư viện chuẩn thực sự được xác định là thành viên của các lớp luồng, khi bạn triển khai các hoạt động đầu ra và đầu vào cho các loại của riêng bạn, bạn không thể thay đổi các loại luồng của thư viện chuẩn. Đó là lý do tại sao bạn cần triển khai các toán tử này cho các loại của riêng bạn dưới dạng các hàm không phải là thành viên. Các hình thức kinh điển của hai là:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
Khi triển khai operator>>
, cài đặt thủ công trạng thái của luồng chỉ cần thiết khi việc đọc thành công, nhưng kết quả không như mong đợi.
Toán tử gọi chức năng
Toán tử gọi hàm, được sử dụng để tạo các đối tượng hàm, còn được gọi là functor, phải được định nghĩa là hàm thành viên , vì vậy nó luôn có this
đối số ngầm của các hàm thành viên. Ngoài điều này, nó có thể bị quá tải để lấy bất kỳ số lượng đối số bổ sung nào, bao gồm cả số không.
Đây là một ví dụ về cú pháp:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Sử dụng:
foo f;
int a = f("hello");
Trong toàn bộ thư viện chuẩn C ++, các đối tượng hàm luôn được sao chép. Do đó, các đối tượng chức năng của riêng bạn nên có giá rẻ để sao chép. Nếu một đối tượng chức năng hoàn toàn cần sử dụng dữ liệu đắt tiền để sao chép, tốt hơn là lưu trữ dữ liệu đó ở nơi khác và để đối tượng chức năng tham chiếu đến nó.
Toán tử so sánh
Các toán tử so sánh nhị phân nên, theo các quy tắc của ngón tay cái, được thực hiện như các hàm không phải là thành viên 1 . Việc phủ định tiền tố unary !
nên (theo các quy tắc tương tự) được thực hiện như là một hàm thành viên. (nhưng thường không phải là một ý tưởng tốt để quá tải nó.)
Các thuật toán của thư viện chuẩn (ví dụ std::sort()
) và các loại (ví dụ std::map
) sẽ luôn luôn chỉ operator<
có mặt. Tuy nhiên, những người dùng loại của bạn cũng sẽ có mặt tất cả các toán tử khác , vì vậy nếu bạn xác định operator<
, hãy chắc chắn tuân theo quy tắc cơ bản thứ ba về quá tải toán tử và cũng xác định tất cả các toán tử so sánh boolean khác. Cách thức kinh điển để thực hiện chúng là:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
Điều quan trọng cần lưu ý ở đây là chỉ có hai trong số các toán tử này thực sự làm bất cứ điều gì, những toán tử khác chỉ chuyển tiếp các đối số của chúng cho một trong hai toán tử này để thực hiện công việc thực tế.
Cú pháp nạp chồng các toán tử boolean nhị phân còn lại ( ||
, &&
) tuân theo các quy tắc của các toán tử so sánh. Tuy nhiên, rất khó có khả năng bạn sẽ tìm thấy một trường hợp sử dụng hợp lý cho 2 điều này .
1 Như với tất cả các quy tắc của ngón tay cái, đôi khi cũng có thể có lý do để phá vỡ điều này. Nếu vậy, đừng quên rằng toán hạng bên trái của các toán tử so sánh nhị phân, đối với các hàm thành viên cũng sẽ *this
cần const
. Vì vậy, một toán tử so sánh được triển khai như một hàm thành viên sẽ phải có chữ ký này:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Lưu ý const
ở cuối.)
2 Cần lưu ý rằng phiên bản tích hợp ||
và &&
sử dụng ngữ nghĩa phím tắt. Trong khi người dùng xác định những cái (vì chúng là đường cú pháp cho các cuộc gọi phương thức) không sử dụng ngữ nghĩa phím tắt. Người dùng sẽ mong đợi các toán tử này có ngữ nghĩa phím tắt và mã của họ có thể phụ thuộc vào nó, do đó chúng tôi KHÔNG BAO GIỜ nên xác định chúng.
Toán tử số học
Toán tử số học đơn phương
Các toán tử tăng và giảm đơn nguyên có cả hai tiền tố và hậu tố. Để nói cái này với cái khác, các biến thể postfix có thêm một đối số int giả. Nếu bạn quá tải tăng hoặc giảm, hãy đảm bảo luôn thực hiện cả phiên bản tiền tố và hậu tố. Dưới đây là việc thực hiện chính tắc của tăng, giảm theo các quy tắc tương tự:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Lưu ý rằng biến thể postfix được thực hiện theo thuật ngữ tiền tố. Cũng lưu ý rằng postfix không sao chép thêm. 2
Quá tải trừ unary và cộng không phải là rất phổ biến và có lẽ tốt nhất nên tránh. Nếu cần, có lẽ chúng nên bị quá tải như các hàm thành viên.
2 Cũng lưu ý rằng biến thể postfix thực hiện nhiều công việc hơn và do đó sử dụng ít hiệu quả hơn so với biến thể tiền tố. Đây là một lý do tốt để thường thích tăng tiền tố hơn tăng tiền tố. Mặc dù các trình biên dịch thường có thể tối ưu hóa công việc bổ sung tăng hậu tố cho các kiểu dựng sẵn, nhưng chúng có thể không thể làm tương tự đối với các kiểu do người dùng định nghĩa (có thể là thứ gì đó trông giống như một trình lặp danh sách). Khi bạn đã quen với việc này i++
, sẽ rất khó để nhớ ++i
thay vào đó khi i
không phải là loại tích hợp (cộng với bạn phải thay đổi mã khi thay đổi một loại), vì vậy tốt hơn là nên tạo thói quen luôn luôn sử dụng gia tăng tiền tố, trừ khi hậu tố rõ ràng là cần thiết.
Toán tử số học nhị phân
Đối với các toán tử số học nhị phân, đừng quên tuân theo nạp chồng toán tử quy tắc cơ bản thứ ba: Nếu bạn cung cấp +
, cũng cung cấp +=
, nếu bạn cung cấp -
, đừng bỏ qua -=
, v.v. Andrew Koenig được cho là người đầu tiên quan sát rằng phép gán ghép toán tử có thể được sử dụng làm cơ sở cho các đối tác không phải là hợp chất của chúng. Đó là, nhà điều hành +
được thực hiện về mặt +=
, -
được thực hiện trong điều khoản của -=
vv
Theo quy tắc ngón tay cái của chúng tôi, +
và những người bạn đồng hành của nó phải là những người không phải là thành viên, trong khi các đối tác chuyển nhượng ghép của họ ( +=
v.v.), thay đổi đối số bên trái của họ, nên là một thành viên. Đây là mã mẫu cho +=
và +
; các toán tử số học nhị phân khác nên được thực hiện theo cùng một cách:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
trả về kết quả của nó trên mỗi tham chiếu, trong khi operator+
trả về một bản sao kết quả của nó. Tất nhiên, trả về một tham chiếu thường hiệu quả hơn trả về một bản sao, nhưng trong trường hợp operator+
, không có cách nào xung quanh việc sao chép. Khi bạn viết a + b
, bạn mong đợi kết quả là một giá trị mới, đó là lý do tại sao operator+
phải trả về một giá trị mới. 3
Cũng lưu ý rằng operator+
có toán hạng bên trái của nó bằng bản sao chứ không phải bằng tham chiếu const. Lý do cho điều này cũng giống như lý do đưa ra operator=
đối số của nó trên mỗi bản sao.
Các toán tử thao tác bit ~
&
|
^
<<
>>
nên được thực hiện theo cách tương tự như các toán tử số học. Tuy nhiên, (ngoại trừ quá tải <<
và >>
cho đầu ra và đầu vào) có rất ít trường hợp sử dụng hợp lý để quá tải những trường hợp này.
3 Một lần nữa, bài học rút ra từ điều này a += b
là, nói chung, hiệu quả hơn a + b
và nên được ưu tiên nếu có thể.
Đăng ký mảng
Toán tử mảng con là toán tử nhị phân phải được thực hiện như một thành viên lớp. Nó được sử dụng cho các loại giống như container cho phép truy cập vào các thành phần dữ liệu của chúng bằng một khóa. Các hình thức kinh điển của việc cung cấp này là:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Trừ khi bạn không muốn người dùng của lớp bạn có thể thay đổi các thành phần dữ liệu được trả về operator[]
(trong trường hợp đó bạn có thể bỏ qua biến thể không phải là const), bạn nên luôn cung cấp cả hai biến thể của toán tử.
Nếu value_type được biết là tham chiếu đến loại tích hợp, biến thể const của toán tử sẽ trả về một bản sao thay vì tham chiếu const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Toán tử cho các loại giống như con trỏ
Để xác định các trình lặp hoặc con trỏ thông minh của riêng bạn, bạn phải quá tải toán tử quy ước tiền tố unary *
và toán tử truy cập thành viên con trỏ nhị phân ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Lưu ý rằng những điều này cũng vậy, hầu như sẽ luôn cần cả phiên bản const và phiên bản không const. Đối với ->
toán tử, nếu value_type
thuộc loại class
(hoặc struct
hoặc union
), một toán tử khác operator->()
được gọi là đệ quy, cho đến khi operator->()
trả về một giá trị của loại không phải là lớp.
Địa chỉ đơn nguyên của toán tử không bao giờ bị quá tải.
Để operator->*()
xem câu hỏi này . Nó hiếm khi được sử dụng và do đó hiếm khi quá tải. Trong thực tế, ngay cả các trình vòng lặp không quá tải nó.
Tiếp tục với các nhà khai thác chuyển đổi