Các quy tắc và thành ngữ cơ bản cho quá tải toán tử là gì?


2143

Lưu ý: Các câu trả lời đã được đưa ra theo một thứ tự cụ thể , nhưng vì nhiều người dùng sắp xếp câu trả lời theo phiếu bầu, thay vì thời gian được đưa ra, đây là một chỉ mục các câu trả lời theo thứ tự có ý nghĩa nhất:

(Lưu ý: Đây có nghĩa là một mục trong Câu hỏi thường gặp về C ++ của Stack Overflow . Nếu bạn muốn phê bình ý tưởng cung cấp Câu hỏi thường gặp trong biểu mẫu này, thì bài đăng trên meta bắt đầu tất cả điều này sẽ là nơi để thực hiện điều đó. câu hỏi đó được theo dõi trong phòng chat C ++ , nơi ý tưởng FAQ bắt đầu ngay từ đầu, vì vậy câu trả lời của bạn rất có thể được đọc bởi những người nghĩ ra ý tưởng.)


63
Nếu chúng ta sẽ tiếp tục với thẻ C ++ - FAQ, đây là cách các mục nên được định dạng.
John Dibling

Tôi đã viết một loạt bài viết ngắn cho cộng đồng C ++ của Đức về quá tải toán tử: Phần 1: quá tải toán tử trong C ++ bao gồm ngữ nghĩa, cách sử dụng điển hình và đặc sản cho tất cả các nhà khai thác. Nó có một số chồng chéo với câu trả lời của bạn ở đây, tuy nhiên có một số thông tin bổ sung. Phần 2 và 3 tạo một hướng dẫn sử dụng Boost.Operators. Bạn có muốn tôi dịch chúng và thêm chúng dưới dạng câu trả lời không?
Arne Mertz

Ồ, và một bản dịch tiếng Anh cũng có sẵn: những điều cơ bảnthông lệ
Arne Mertz

Câu trả lời:


1043

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 <<>>, 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ẽ *thiscầ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 ||&&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ớ ++ithay vào đó khi ikhô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 +=+; 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 <<>>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 += blà, nói chung, hiệu quả hơn a + bvà 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_typethuộc loại class(hoặc structhoặ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


89
operator->()thực sự là vô cùng kỳ lạ. Không bắt buộc phải trả về value_type*- trên thực tế, nó có thể trả về một loại lớp khác, với điều kiện loại lớp đó có mộtoperator->() , sau đó sẽ được gọi sau đó. Cuộc gọi đệ quy này của operator->()s tiến hành cho đến khi value_type*kiểu trả về xảy ra. Điên cuồng! :)
j_random_hacker

2
Đó không phải là chính xác về hiệu quả. Đó là về chúng ta không thể làm điều đó theo cách thành ngữ truyền thống trong một vài trường hợp (rất): khi định nghĩa của cả hai toán hạng cần giữ nguyên trong khi chúng ta tính kết quả. Và như tôi đã nói, có hai ví dụ cổ điển: phép nhân ma trận và phép nhân đa thức. Chúng tôi có thể định nghĩa *theo *=nhưng sẽ rất khó xử vì một trong những thao tác đầu tiên *=sẽ tạo ra một đối tượng mới, kết quả của việc tính toán. Sau đó, sau vòng lặp for-ijk, chúng ta sẽ trao đổi đối tượng tạm thời này với *this. I E. 1.copy, 2.operator *, 3.swap
Luc Hermitte

6
Tôi không đồng ý với các phiên bản const / non-const của các toán tử giống như con trỏ của bạn, ví dụ `const value_type & toán tử * () const;` - điều này sẽ giống như T* consttrả lại một cuộc hội thảo const T&, không phải là trường hợp. Hay nói cách khác: một con trỏ const không bao hàm một con trỏ const. Trong thực tế, nó không tầm thường để bắt chước T const *- đó là lý do cho toàn bộ const_iteratornội dung trong thư viện tiêu chuẩn. Kết luận: chữ ký phải làreference_type operator*() const; pointer_type operator->() const
Arne Mertz

6
Một nhận xét: Việc triển khai các toán tử số học nhị phân được đề xuất là không hiệu quả như nó có thể. Các nhà khai thác Se Boost ghi chú simmetry: boost.org/doc/libs/1_54_0/libs/utility/operators.htmlm#symmetry Một bản sao khác có thể tránh được nếu bạn sử dụng bản sao cục bộ của tham số đầu tiên, do + = và trả về bản sao địa phương. Điều này cho phép tối ưu hóa NRVO.
Manu343726

3
Như tôi đã đề cập trong cuộc trò chuyện, L <= Rcũng có thể được thể hiện !(R < L)thay vì !(L > R). Có thể lưu thêm một lớp nội tuyến trong các biểu thức khó tối ưu hóa (và đó cũng là cách Boost.Operators thực hiện nó).
TemplateRex

494

Ba quy tắc cơ bản của quá tải toán tử trong C ++

Khi nói đến quá tải toán tử trong C ++, có ba quy tắc cơ bản bạn nên tuân theo . Như với tất cả các quy tắc như vậy, thực sự có ngoại lệ. Đôi khi mọi người đã đi chệch khỏi họ và kết quả không phải là mã xấu, nhưng những sai lệch tích cực như vậy là rất ít. Ít nhất, 99 trong số 100 sai lệch như vậy tôi đã thấy là không chính đáng. Tuy nhiên, nó cũng có thể là 999 trên 1000. Vì vậy, tốt hơn hết bạn nên tuân thủ các quy tắc sau.

  1. Bất cứ khi nào ý nghĩa của một toán tử không rõ ràng và không thể tranh cãi, nó không nên bị quá tải. Thay vào đó, cung cấp một chức năng với một tên được chọn tốt.
    Về cơ bản, quy tắc đầu tiên và quan trọng nhất đối với các nhà khai thác quá tải, tại trung tâm của nó, nói: Đừng làm điều đó . Điều đó có vẻ lạ, bởi vì có rất nhiều điều được biết về quá tải toán tử và rất nhiều bài báo, chương sách và các văn bản khác giải quyết tất cả điều này. Nhưng mặc dù bằng chứng dường như rõ ràng này, chỉ có một vài trường hợp đáng ngạc nhiên là quá tải toán tử là phù hợp. Lý do là thực sự khó hiểu về ngữ nghĩa đằng sau ứng dụng của một toán tử trừ khi việc sử dụng toán tử trong miền ứng dụng được biết đến và không thể tranh cãi. Trái với niềm tin phổ biến, điều này hầu như không bao giờ xảy ra.

  2. Luôn luôn bám sát ngữ nghĩa nổi tiếng của nhà điều hành.
    C ++ đặt ra không có giới hạn về ngữ nghĩa của các toán tử quá tải. Trình biên dịch của bạn sẽ vui vẻ chấp nhận mã thực hiện+toán tửnhị phânđể trừ khỏi toán hạng bên phải của nó. Tuy nhiên, những người sử dụng của một nhà điều hành như vậy sẽ không bao giờ nghi ngờ sự biểu hiệna + btrừatừb. Tất nhiên, điều này cho rằng ngữ nghĩa của toán tử trong miền ứng dụng là không cần bàn cãi.

  3. Luôn cung cấp tất cả các bộ hoạt động liên quan.
    Các nhà khai thác có liên quan với nhau và các hoạt động khác. Nếu loại của bạn hỗ trợa + b, người dùng cũng sẽ có thể gọia += b. Nếu nó hỗ trợ tăng tiền tố++a, họ cũng sẽa++làm việc như vậy. Nếu họ có thể kiểm tra xema < b, chắc chắn họ cũng sẽ mong đợi để có thể kiểm tra xema > b. Nếu họ có thể sao chép-xây dựng kiểu của bạn, họ mong đợi sự phân công cũng hoạt động.


Tiếp tục Quyết định giữa Thành viên và Không thành viên .


16
Điều duy nhất tôi biết là vi phạm bất kỳ thứ gì trong số này là boost::spiritlol.
Billy ONeal

66
@Billy: Theo một số người, lạm dụng +để nối chuỗi là vi phạm, nhưng đến nay nó đã trở thành những lời khen ngợi được thiết lập tốt, do đó nó có vẻ tự nhiên. Mặc dù tôi nhớ một lớp chuỗi nhà pha chế mà tôi đã thấy trong những năm 90 đã sử dụng nhị phân &cho mục đích này (tham khảo BASIC cho các Praxis đã thiết lập). Nhưng, yeah, đặt nó vào lib std về cơ bản đặt điều này trong đá. Điều tương tự cũng xảy ra đối với việc lạm dụng <<>>đối với IO, BTW. Tại sao dịch chuyển trái là hoạt động đầu ra rõ ràng? Bởi vì tất cả chúng ta đã học về nó khi chúng ta thấy "Xin chào, thế giới!" Đầu tiên của chúng tôi ứng dụng. Và không vì lý do nào khác.
sbi

5
@cquilguy: Nếu bạn phải giải thích nó, điều đó rõ ràng không rõ ràng và không thể tranh cãi. Tương tự như vậy nếu bạn cần thảo luận hoặc bảo vệ quá tải.
sbi

5
@sbi: "đánh giá ngang hàng" luôn là một ý kiến ​​hay. Đối với tôi, một toán tử được chọn xấu không khác với một tên hàm được chọn kém (tôi đã thấy nhiều). Toán tử chỉ là chức năng. Không nhiều không ít. Các quy tắc là như nhau. Và để hiểu nếu một ý tưởng là tốt, cách tốt nhất là hiểu nó mất bao lâu để được hiểu. (Do đó, đánh giá ngang hàng là điều bắt buộc, nhưng đồng nghiệp phải được lựa chọn giữa những người không bị giáo điều và thành kiến.)
Emilio Garavaglia

5
@sbi Với tôi, sự thật hoàn toàn rõ ràng và không thể chối cãi operator==là nó phải là một mối quan hệ tương đương (IOW, bạn không nên sử dụng NaN không báo hiệu). Có nhiều quan hệ tương đương hữu ích trên container. Bình đẳng có nghĩa là gì? " aBằng b" có nghĩa rằng abcó giá trị toán học tương tự. Khái niệm giá trị toán học của một (không phải NaN) floatlà rõ ràng, nhưng giá trị toán học của một container có thể có nhiều định nghĩa hữu ích riêng biệt (kiểu đệ quy). Định nghĩa mạnh nhất về sự bình đẳng là "chúng là cùng một đối tượng" và nó vô dụng.
tò mò

265

Cú pháp chung của quá tải toán tử trong C ++

Bạn không thể thay đổi ý nghĩa của các toán tử cho các loại tích hợp trong C ++, các toán tử chỉ có thể bị quá tải cho các loại 1 do người dùng định nghĩa . Đó là, ít nhất một trong các toán hạng phải thuộc loại do người dùng định nghĩa. Cũng như các hàm quá tải khác, các toán tử có thể bị quá tải cho một bộ tham số nhất định chỉ một lần.

Không phải tất cả các toán tử có thể bị quá tải trong C ++. Trong số các toán tử không thể bị quá tải là: . :: sizeof typeid .*và toán tử ternary duy nhất trong C ++,?:

Trong số các toán tử có thể bị quá tải trong C ++ là:

  • toán tử số học: + - * / %+= -= *= /= %=(tất cả các phần tử nhị phân); + -(tiền tố đơn nguyên); ++ --(tiền tố unary và hậu tố)
  • thao tác bit: & | ^ << >>&= |= ^= <<= >>=(tất cả các phần tử nhị phân); ~(tiền tố đơn nguyên)
  • đại số boolean: == != < > <= >= || &&(tất cả các nhị phân); !(tiền tố đơn nguyên)
  • quản lý bộ nhớ: new new[] delete delete[]
  • toán tử chuyển đổi ngầm
  • miscellany: = [] -> ->* , (tất cả các phần tử nhị phân); * &(tất cả tiền tố unary) ()(hàm gọi, n-ary infix)

Tuy nhiên, thực tế là bạn có thể quá tải tất cả những điều này không có nghĩa là bạn nên làm như vậy. Xem các quy tắc cơ bản của quá tải toán tử.

Trong C ++, các toán tử bị quá tải dưới dạng các hàm với các tên đặc biệt . Cũng như các hàm khác, các toán tử quá tải thường có thể được triển khai như là một hàm thành viên thuộc loại toán hạng bên trái của chúng hoặc là các hàm không phải là thành viên . Việc bạn có thể tự do lựa chọn hay ràng buộc sử dụng một trong hai tùy thuộc vào một số tiêu chí. 2 Một toán tử đơn nguyên @3 , được áp dụng cho một đối tượng x, được gọi là as operator@(x)hoặc as x.operator@(). Một toán tử infix nhị phân @, được áp dụng cho các đối tượng xy, được gọi là as operator@(x,y)hoặc as x.operator@(y). 4

Các toán tử được triển khai như các hàm không phải thành viên đôi khi là bạn của kiểu toán hạng của chúng.

1 Thuật ngữ người dùng do người dùng định nghĩa có thể có một chút sai lệch. C ++ tạo ra sự khác biệt giữa các loại tích hợp và các loại do người dùng định nghĩa. Đối với trước đây thuộc về int, char và double; sau này thuộc về tất cả các kiểu struct, class, union và enum, bao gồm cả các kiểu từ thư viện chuẩn, mặc dù chúng không được xác định bởi người dùng.

2 Điều này được đề cập trong phần sau của Câu hỏi thường gặp này.

3 Đây @không phải là toán tử hợp lệ trong C ++, đó là lý do tại sao tôi sử dụng nó như một trình giữ chỗ.

4 Toán tử ternary duy nhất trong C ++ không thể bị quá tải và toán tử n-ary duy nhất phải luôn được thực hiện như một hàm thành viên.


Tiếp tục với Ba quy tắc cơ bản của quá tải toán tử trong C ++ .


~là tiền tố unary, không phải là nhị phân.
mrkj

1
.*bị thiếu trong danh sách các toán tử không quá tải.
celticminstrel 04/07/2015

1
@Mateen Tôi muốn sử dụng một trình giữ chỗ thay vì một nhà điều hành thực sự để làm rõ rằng đây không phải là về một nhà điều hành đặc biệt, nhưng áp dụng cho tất cả họ. Và, nếu bạn muốn trở thành một lập trình viên C ++, bạn nên học cách chú ý ngay cả đến bản in nhỏ. :)
sbi

1
@HR: Nếu bạn đã đọc hướng dẫn này, bạn sẽ biết những gì sai. Tôi thường đề nghị bạn nên đọc ba câu trả lời đầu tiên được liên kết từ câu hỏi. Điều đó không nên kéo dài hơn nửa giờ trong cuộc sống của bạn và mang đến cho bạn sự hiểu biết cơ bản. Cú pháp dành riêng cho người vận hành, bạn có thể tra cứu sau. Vấn đề cụ thể của bạn cho thấy bạn cố gắng quá tải operator+()như một hàm thành viên, nhưng đã cho nó chữ ký của một hàm miễn phí. Xem ở đây .
sbi

1
@sbi: Tôi đã đọc ba bài đầu tiên rồi và cảm ơn bạn đã làm chúng. :) Tôi sẽ cố gắng giải quyết vấn đề nếu không tôi nghĩ rằng tốt hơn là hỏi nó bằng một câu hỏi riêng biệt. Cảm ơn bạn một lần nữa đã làm cho cuộc sống dễ dàng cho chúng tôi! : D
Hosein Rahnama

251

Quyết định giữa thành viên và không thành viên

Toán tử nhị phân =(gán), [](thuê bao mảng), ->(truy cập thành viên), cũng như toán tử n-ary ()(gọi hàm), phải luôn luôn được triển khai như các hàm thành viên , vì cú pháp của ngôn ngữ yêu cầu chúng phải.

Các nhà khai thác khác có thể được thực hiện với tư cách là thành viên hoặc không phải là thành viên. Tuy nhiên, một số trong số chúng thường phải được triển khai dưới dạng các hàm không phải thành viên, bởi vì toán hạng bên trái của chúng không thể được sửa đổi bởi bạn. Nổi bật nhất trong số này là các toán tử đầu vào và đầu ra <<>>, có toán hạng bên trái là các lớp luồng từ thư viện chuẩn mà bạn không thể thay đổi.

Đối với tất cả các toán tử mà bạn phải chọn thực hiện chúng như một hàm thành viên hoặc hàm không phải thành viên, hãy sử dụng các quy tắc ngón tay sau để quyết định:

  1. Nếu nó là một toán tử đơn nguyên , thực hiện nó như là một hàm thành viên .
  2. Nếu một toán tử nhị phân đối xử với cả hai toán hạng như nhau (nó không thay đổi chúng), thì triển khai toán tử này như là một hàm không phải là thành viên .
  3. Nếu một toán tử nhị phân không xử lý cả hai toán hạng của nó như nhau (thông thường nó sẽ thay đổi toán hạng bên trái của nó), có thể hữu ích để biến nó thành một hàm thành viên của kiểu toán hạng bên trái của nó, nếu nó phải truy cập các phần riêng của toán hạng.

Tất nhiên, như với tất cả các quy tắc của ngón tay cái, có những trường hợp ngoại lệ. Nếu bạn có một loại

enum Month {Jan, Feb, ..., Nov, Dec}

và bạn muốn quá tải các toán tử tăng và giảm cho nó, bạn không thể thực hiện điều này như một hàm thành viên, vì trong C ++, các kiểu enum không thể có các hàm thành viên. Vì vậy, bạn phải quá tải nó như là một chức năng miễn phí. Và operator<()đối với một mẫu lớp được lồng trong một mẫu lớp thì việc viết và đọc dễ dàng hơn nhiều khi được thực hiện như là một hàm thành viên nội tuyến trong định nghĩa lớp. Nhưng đây thực sự là những ngoại lệ hiếm.

(Tuy nhiên, nếu bạn tạo một ngoại lệ, đừng quên vấn đề const-ness cho toán hạng, đối với các hàm thành viên, sẽ trở thành thisđối số ngầm . Nếu toán tử với tư cách là hàm không phải thành viên sẽ lấy tham số ngoài cùng của nó làm consttham chiếu , toán tử tương tự như một hàm thành viên cần phải có một constở cuối để tạo *thismột consttham chiếu.)


Tiếp tục các toán tử phổ biến để quá tải .


9
Mục của Herb Sutter trong C ++ hiệu quả (hay đó là Tiêu chuẩn mã hóa C ++?) Nói rằng người ta nên ưu tiên các hàm không phải là thành viên cho các hàm thành viên, để tăng sự đóng gói của lớp. IMHO, lý do đóng gói được ưu tiên theo quy tắc ngón tay cái của bạn, nhưng nó không làm giảm giá trị chất lượng của quy tắc ngón tay cái của bạn.
paercebal

8
@paercebal: C ++ hiệu quả là của Meyers, Tiêu chuẩn mã hóa C ++ của Sutter. Mà một trong những bạn đang đề cập đến? Dù sao, tôi không thích ý tưởng về việc operator+=()không phải là thành viên. Nó phải thay đổi toán hạng bên trái của nó, vì vậy theo định nghĩa, nó phải đào sâu vào bên trong nó. Bạn sẽ đạt được gì khi không biến nó thành thành viên?
sbi

9
@sbi: Mục 44 trong Tiêu chuẩn mã hóa C ++ (Sutter) Thích viết các hàm không kết bạn không phải là tháng , tất nhiên, nó chỉ áp dụng nếu bạn thực sự có thể viết hàm này chỉ bằng giao diện chung của lớp. Nếu bạn không thể (hoặc có thể nhưng nó sẽ cản trở hoạt động kém), thì bạn phải biến nó thành thành viên hoặc bạn bè.
Matthieu M.

3
@sbi: Rất tiếc, Hiệu quả, Đặc biệt ... Không có gì lạ khi tôi trộn tên lên. Dù sao, mức tăng là để giới hạn càng nhiều càng tốt số lượng hàm có quyền truy cập vào một đối tượng dữ liệu riêng tư / được bảo vệ. Bằng cách này, bạn tăng đóng gói của lớp của bạn, làm cho việc bảo trì / kiểm tra / tiến hóa của nó dễ dàng hơn.
paercebal

12
@sbi: Một ví dụ. Giả sử bạn đang mã hóa một lớp String, với cả phương thức operator +=appendphương thức. Các appendphương pháp là hoàn thiện hơn, bởi vì bạn có thể thêm một chuỗi con của tham số từ chỉ số i để chỉ số n -1: append(string, start, end)Nó có vẻ hợp lý để có +=append cuộc gọi với start = 0end = string.size. Tại thời điểm đó, chắp thêm có thể là một phương thức thành viên, nhưng operator +=không cần phải là thành viên và làm cho nó không phải là thành viên sẽ làm giảm số lượng mã chơi với các chuỗi String, vì vậy đó là một điều tốt .... ^ _ ^ ...
paercebal

165

Toán tử chuyển đổi (còn được gọi là Chuyển đổi do người dùng xác định)

Trong C ++, bạn có thể tạo toán tử chuyển đổi, toán tử cho phép trình biên dịch chuyển đổi giữa các loại của bạn và các loại được xác định khác. Có hai loại toán tử chuyển đổi, ẩn và rõ ràng.

Toán tử chuyển đổi ngầm định (C ++ 98 / C ++ 03 và C ++ 11)

Toán tử chuyển đổi ngầm định cho phép trình biên dịch chuyển đổi ngầm định (như chuyển đổi giữa intlong) giá trị của loại do người dùng xác định thành một số loại khác.

Sau đây là một lớp đơn giản với một toán tử chuyển đổi ẩn:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Các toán tử chuyển đổi ngầm định, như các hàm tạo một đối số, là các chuyển đổi do người dùng định nghĩa. Trình biên dịch sẽ cấp một chuyển đổi do người dùng xác định khi cố gắng khớp lệnh gọi với hàm quá tải.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

Lúc đầu, điều này có vẻ rất hữu ích, nhưng vấn đề với điều này là việc chuyển đổi ngầm thậm chí còn xảy ra khi nó không được mong đợi. Trong đoạn mã sau, void f(const char*)sẽ được gọi vì my_string()không phải là giá trị , vì vậy mã đầu tiên không khớp:

void f(my_string&);
void f(const char*);

f(my_string());

Người mới bắt đầu dễ dàng mắc phải lỗi này và ngay cả các lập trình viên C ++ có kinh nghiệm đôi khi cũng ngạc nhiên vì trình biên dịch chọn quá tải mà họ không nghi ngờ. Những vấn đề này có thể được giảm thiểu bởi các nhà khai thác chuyển đổi rõ ràng.

Toán tử chuyển đổi rõ ràng (C ++ 11)

Không giống như các toán tử chuyển đổi ngầm định, các toán tử chuyển đổi rõ ràng sẽ không bao giờ khởi động khi bạn không mong đợi chúng. Sau đây là một lớp đơn giản với một toán tử chuyển đổi rõ ràng:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Hãy chú ý explicit. Bây giờ khi bạn cố gắng thực thi mã không mong muốn từ các toán tử chuyển đổi ẩn, bạn sẽ gặp lỗi trình biên dịch:

prog.cpp: Trong hàm 'int main ()':
prog.cpp: 15: 18: error: không có chức năng phù hợp cho cuộc gọi đến 'f (my_opes)'
prog.cpp: 15: 18: lưu ý: ứng viên là:
prog.cpp: 11: 10: lưu ý: void f (my_opes &)
prog.cpp: 11: 10: lưu ý: không có chuyển đổi nào được biết cho đối số 1 từ 'my_opes' sang 'my_opes &'
prog.cpp: 12: 10: lưu ý: void f (const char *)
prog.cpp: 12: 10: lưu ý: không có chuyển đổi nào được biết cho đối số 1 từ 'my_opes' sang 'const char *'

Để gọi toán tử cast rõ ràng, bạn phải sử dụng static_cast, kiểu cast C hoặc kiểu cast constructor (tức là T(value)).

Tuy nhiên, có một ngoại lệ cho điều này: Trình biên dịch được phép ngầm chuyển đổi thành bool. Ngoài ra, trình biên dịch không được phép thực hiện một chuyển đổi ngầm định khác sau khi nó chuyển đổi thành bool(trình biên dịch được phép thực hiện 2 chuyển đổi ngầm định tại một thời điểm, nhưng chỉ tối đa 1 chuyển đổi do người dùng xác định).

Vì trình biên dịch sẽ không bỏ "quá khứ" bool, nên các toán tử chuyển đổi rõ ràng hiện đã loại bỏ sự cần thiết của thành ngữ Safe Bool . Ví dụ: con trỏ thông minh trước C ++ 11 đã sử dụng thành ngữ Safe Bool để ngăn chuyển đổi thành các loại tích phân. Trong C ++ 11, các con trỏ thông minh sử dụng toán tử tường minh thay vì trình biên dịch không được phép chuyển đổi hoàn toàn thành một kiểu tích phân sau khi nó chuyển đổi rõ ràng một loại thành bool.

Tiếp tục quá tải newdelete .


148

Quá tải newdelete

Lưu ý: Điều này chỉ liên quan đến cú pháp quá tảinewdelete, không phải với việc thực hiện các toán tử quá tải như vậy. Tôi nghĩ rằng ngữ nghĩa của quá tảinewdeletexứng đáng với Câu hỏi thường gặp của riêng họ , trong chủ đề về quá tải nhà điều hành, tôi không bao giờ có thể làm điều đó công lý.

Khái niệm cơ bản

Trong C ++, khi bạn viết một biểu thức mới giống như new T(arg)hai điều xảy ra khi biểu thức này được ước tính: Đầu tiên operator newđược gọi để lấy bộ nhớ thô, và sau đó hàm tạo thích hợp của Tđược gọi để biến bộ nhớ thô này thành một đối tượng hợp lệ. Tương tự như vậy, khi bạn xóa một đối tượng, đầu tiên hàm hủy của nó được gọi và sau đó bộ nhớ được trả về operator delete.
C ++ cho phép bạn điều chỉnh cả hai thao tác này: quản lý bộ nhớ và xây dựng / phá hủy đối tượng tại bộ nhớ được phân bổ. Điều thứ hai được thực hiện bằng cách viết các hàm tạo và hàm hủy cho một lớp. Quản lý bộ nhớ tinh chỉnh được thực hiện bằng cách viết của riêng bạn operator newoperator delete.

Quy tắc cơ bản đầu tiên của quá tải toán tử - không thực hiện - đặc biệt áp dụng cho quá tải newdelete. Hầu như các lý do duy nhất để quá tải các toán tử này là các vấn đề về hiệu nănghạn chế bộ nhớ , và trong nhiều trường hợp, các hành động khác, như thay đổi thuật toán được sử dụng, sẽ cung cấp tỷ lệ chi phí / tăng cao hơn nhiều so với cố gắng điều chỉnh quản lý bộ nhớ.

Thư viện chuẩn C ++ đi kèm với một tập hợp các toán tử newdeletetoán tử được xác định trước . Những cái quan trọng nhất là:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

Hai bộ nhớ đầu tiên phân bổ / giải phóng bộ nhớ cho một đối tượng, hai bộ nhớ sau cho một mảng các đối tượng. Nếu bạn cung cấp các phiên bản của riêng bạn, chúng sẽ không quá tải, nhưng thay thế các phiên bản từ thư viện tiêu chuẩn.
Nếu bạn quá tải operator new, bạn cũng nên luôn quá tải kết hợp operator delete, ngay cả khi bạn không bao giờ có ý định gọi nó. Lý do là, nếu một hàm tạo ném trong khi đánh giá biểu thức mới, hệ thống thời gian chạy sẽ trả lại bộ nhớ cho operator deletekhớp với operator newcái được gọi để phân bổ bộ nhớ để tạo đối tượng. Nếu bạn không cung cấp kết quả khớp operator delete, cái mặc định được gọi, gần như luôn luôn sai.
Nếu bạn quá tải newdelete, bạn cũng nên xem xét quá tải các biến thể mảng.

Vị trí new

C ++ cho phép các toán tử mới và xóa lấy các đối số bổ sung.
Cái gọi là vị trí mới cho phép bạn tạo một đối tượng tại một địa chỉ nhất định được chuyển đến:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

Thư viện chuẩn đi kèm với sự quá tải thích hợp của các toán tử mới và xóa cho điều này:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Lưu ý rằng, trong mã ví dụ cho vị trí mới được đưa ra ở trên, operator deletesẽ không bao giờ được gọi, trừ khi hàm tạo của X ném ngoại lệ.

Bạn cũng có thể quá tải newdeletevới các đối số khác. Như với đối số bổ sung cho vị trí mới, các đối số này cũng được liệt kê trong ngoặc đơn sau từ khóa new. Chỉ vì lý do lịch sử, các biến thể như vậy thường được gọi là vị trí mới, ngay cả khi các đối số của chúng không dành cho việc đặt một đối tượng tại một địa chỉ cụ thể.

Mới và xóa lớp cụ thể

Thông thường nhất bạn sẽ muốn tinh chỉnh quản lý bộ nhớ vì phép đo đã chỉ ra rằng các thể hiện của một lớp cụ thể hoặc của một nhóm các lớp liên quan, được tạo và hủy thường xuyên và quản lý bộ nhớ mặc định của hệ thống thời gian chạy, được điều chỉnh cho hiệu suất chung, giao dịch không hiệu quả trong trường hợp cụ thể này. Để cải thiện điều này, bạn có thể quá tải mới và xóa cho một lớp cụ thể:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

Quá tải do đó, mới và xóa hoạt động như các hàm thành viên tĩnh. Đối với các đối tượng my_class, std::size_tđối số sẽ luôn luôn là sizeof(my_class). Tuy nhiên, các toán tử này cũng được gọi cho các đối tượng được phân bổ động của các lớp dẫn xuất , trong trường hợp đó nó có thể lớn hơn thế.

Toàn cầu mới và xóa

Để quá tải toàn cầu mới và xóa, chỉ cần thay thế các toán tử được xác định trước của thư viện chuẩn bằng thư viện riêng của chúng tôi. Tuy nhiên, điều này hiếm khi cần phải được thực hiện.


11
Tôi cũng không đồng ý rằng việc thay thế toán tử toàn cầu mới và xóa thường là để thực hiện: ngược lại, nó thường là để theo dõi lỗi.
Yttrill

1
Bạn cũng nên lưu ý rằng, nếu bạn sử dụng toán tử mới quá tải, bạn cũng cần phải cung cấp toán tử xóa với các đối số phù hợp. Bạn nói rằng trong phần mới / xóa toàn cầu, nơi nó không được quan tâm nhiều.
Yttrill

13
@Yttrill bạn là những điều khó hiểu. Các ý nghĩa bị quá tải. "Quá tải toán tử" có nghĩa là ý nghĩa bị quá tải. Điều đó không có nghĩa là các chức năng theo nghĩa đen bị quá tải và đặc biệt là toán tử mới sẽ không làm quá tải phiên bản của Standard. @sbi không yêu cầu ngược lại. Người ta thường gọi nó là "quá tải mới" vì người ta thường nói "quá tải toán tử bổ sung".
Julian Schaub - litb

1
@sbi: Xem (hoặc tốt hơn, liên kết đến) gotw.ca/publications/mill15.htm . Nó chỉ là thực hành tốt đối với những người đôi khi sử dụng nothrowmới.
Alexandre C.

1
"Nếu bạn không cung cấp xóa toán tử phù hợp, thì mặc định được gọi là" -> Trên thực tế, nếu bạn thêm bất kỳ đối số nào và không tạo xóa xóa phù hợp, thì không có thao tác xóa toán tử nào được gọi và bạn bị rò rỉ bộ nhớ. (15.2.2, bộ nhớ bị chiếm giữ bởi đối tượng chỉ được giải phóng nếu tìm thấy ... xóa toán tử thích hợp)
dascandy

46

Tại sao operator<<chức năng không thể truyền phát các đối tượng đến std::couthoặc đến một tệp là một hàm thành viên?

Hãy nói rằng bạn có:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Cho rằng, bạn không thể sử dụng:

Foo f = {10, 20.0};
std::cout << f;

Do operator<<bị quá tải như là một hàm thành viên của Foo, LHS của toán tử phải là một Foođối tượng. Có nghĩa là, bạn sẽ được yêu cầu sử dụng:

Foo f = {10, 20.0};
f << std::cout

đó là rất không trực quan.

Nếu bạn xác định nó là một hàm không phải thành viên,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

Bạn sẽ có thể sử dụng:

Foo f = {10, 20.0};
std::cout << f;

đó là rất trực quan.

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.