Câu hỏi Các quy tắc và thành ngữ cơ bản cho quá tải nhà điều hành là gì?


Lưu ý: Các câu trả lời được đưa ra trong 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, chứ không phải thời gian họ được đưa ra, dưới đây là chỉ số câu trả lời theo thứ tự mà chúng có ý nghĩa nhất:

(Lưu ý: Điều này có nghĩa là mục nhập 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ì đăng trên meta đã bắt đầu tất cả điều này sẽ là nơi để làm điều đó. Câu trả lời cho câu hỏi đó được theo dõi trong C ++ chatroom, nơi mà ý tưởng FAQ bắt đầu ngay từ đầu, vì vậy câu trả lời của bạn rất có khả năng được đọc bởi những người nghĩ ra ý tưởng đó.)  


1845
2017-12-12 12:44


gốc


Nếu chúng ta tiếp tục với thẻ C ++ - FAQ, đây là cách định dạng các mục. - John Dibling
Tôi đã viết một loạt bài viết ngắn cho cộng đồng C ++ Đứ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 làm câu trả lời không? - Arne Mertz
Oh, và bản dịch tiếng Anh cũng có sẵn: những thứ cơ bản và thực tế phổ biến - Arne Mertz


Các câu trả lời:


Các 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ã tấm nồi hơi. Đó là một chút ngạc nhiên, vì các nhà khai thác đơn thuần chỉ là cú pháp đường, công việc thực tế của họ có thể được thực hiện bởi (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 phải nhận được mã tấm nồi hơi này ngay. Nếu bạn không thành công, mã của nhà điều hành sẽ không biên dịch hoặc mã người dùng của bạn sẽ không biên dịch hoặc mã người dùng của bạn sẽ hoạt động đáng ngạc nhiên.

Toán tử chuyển nhượng

Có rất nhiều điều để nói về nhiệm vụ. Tuy nhiên, hầu hết nó đã được nói trong Câu hỏi thường gặp về Sao chép và Trao đổi của GMan, vì vậy tôi sẽ bỏ qua hầu hết ở đâ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;
}

Bitshift Operators (được sử dụng cho Stream I / O)

Các toán tử bithift << và >>, mặc dù vẫn được sử dụng trong phần cứng giao tiếp cho các chức năng thao tác bit mà chúng kế thừa từ C, đã trở nên phổ biến hơn như 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. Để hướng dẫn quá tải như các toán tử thao tác bit, hãy xem phần bên dưới trên các toán tử số học nhị phân. Để triển khai định dạng tùy chỉnh của riêng bạn và phân tích cú pháp logic 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 thông thường nhất, là các toán tử nhị phân nhị phân mà cú pháp chỉ định không có giới hạn về việc liệu chúng có phải là thành viên hay không phải thành viên hay không. Vì họ thay đổi đối số trái của họ (họ thay đổi trạng thái của luồng), họ nên, theo các quy tắc chung, đượ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, toán hạng bên trái của chúng là các luồng từ thư viện chuẩn, và trong khi hầu hết các luồng đầu ra và các toán tử đầu vào được định nghĩa bởi thư viện chuẩn thực sự được định nghĩa là thành viên của các lớp luồng, khi bạn thực hiện các hoạt động đầu ra và đầu vào cho các kiểu của riêng bạn, bạn không thể thay đổi 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 chức năng không phải là thành viên. Các hình thức kinh điển của cả 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>>, thiết lập trạng thái của luồng theo cách thủ công chỉ cần thiết khi bản thân việc đọc đã thành công, nhưng kết quả không phải là điều mong đợi.

Toán tử gọi hàm

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à hàm functors, phải được định nghĩa là hội viên chức năng, vì vậy nó luôn có tiềm ẩn this đối số của các hàm thành viên. Ngoài ra, nó có thể bị quá tải để lấy bất kỳ số đối số bổ sung nào, bao gồm số không.

Dưới đâ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 suốt 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 hàm của riêng bạn sẽ rẻ để sao chép. Nếu một đối tượng hàm hoàn toàn cần sử dụng dữ liệu đắt tiền để sao chép, thì tốt hơn là lưu trữ dữ liệu đó ở nơi khác và có đối tượng hàm tham chiếu đến nó.

Toán tử so sánh

Các toán tử so sánh nhị phân nhị phân nên, theo các quy tắc của ngón cái, được thực hiện như các hàm không phải của thành viên1. Phủ định tiền tố đơn nhất ! nên (theo các quy tắc tương tự) được thực hiện như một chức năng thành viên. (nhưng nó thường không phải là một ý tưởng tốt để quá tải nó.)

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 mong đợi operator< hiện tại. Tuy nhiên, người dùng thuộc loại của bạn sẽ mong đợi tất cả các nhà khai thác khác có mặtcũng vậy, vì vậy nếu bạn xác định operator<, hãy đảm bảo tuân thủ quy tắc cơ bản thứ ba của 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 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 người khác chỉ chuyển tiếp các đối số của họ tới một trong hai người này để thực hiện công việc thực tế.

Cú pháp cho quá tải các toán tử boolean nhị phân còn lại (||, &&) tuân thủ các quy tắc của toán tử so sánh. Tuy nhiên nó là rất không chắc rằng bạn sẽ tìm thấy một trường hợp sử dụng hợp lý cho những2.

1  Như với tất cả các quy tắc của ngón tay cái, đôi khi có thể có lý do để phá vỡ này, quá. Nếu vậy, đừng quên rằng toán hạng bên trái của toán tử so sánh nhị phân, đối với các hàm thành viên sẽ là *this, cần phải const, quá. Vì vậy, một toán tử so sánh được thực hiện 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 cùng.)

2  Cần lưu ý rằng phiên bản tích hợp của || và && sử dụng ngữ nghĩa phím tắt. Trong khi người dùng định nghĩa (vì chúng là cú pháp đường 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 chúng có thể phụ thuộc vào nó, do đó, rất được khuyên là KHÔNG BAO GIỜ để xác định chúng.

Toán tử số học

Toán tử số học đơn nhất

Các toán tử tăng và giảm dần đơn nhất có cả tiền tố và hậu tố. Để nói với một từ khác, các biến thể postfix có một đối số int giả bổ sung. Nếu bạn tăng hoặc giảm quá tải, hãy đảm bảo luôn triển khai cả hai 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 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 tiền tố. Cũng lưu ý rằng postfix hiện một bản sao thêm.2

Quá tải trừ đi trừ đi và cộng thêm không phải là rất phổ biến và có thể tránh được tốt nhất. Nếu cần thiết, chúng có lẽ sẽ bị quá tải như các hàm thành viên.

2  Cũng lưu ý rằng biến thể postfix hoạt động nhiều hơn và do đó ít hiệu quả hơn so với biến thể tiền tố. Đây là một lý do tốt để nói chung thích tiền tố gia tăng trên postfix tăng. 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 của tăng postfix cho các kiểu dựng sẵn, chúng có thể không thực hiện tương tự cho các kiểu do người dùng định nghĩa (có thể là một cái gì đó vô tội như một trình vòng lặp danh sách). Một khi bạn đã quen với việc làm i++nó trở nên rất khó nhớ ++i thay vào đó khi i không phải là kiểu 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 nên tạo thói quen luôn sử dụng tăng tiền tố, trừ khi postfix là cần thiết một cách rõ ràng.

Toán tử số học nhị phân

Đối với toán tử số học nhị phân, đừng quên tuân thủ quy tắc 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 -=, vv Andrew Koenig được cho là người đầu tiên quan sát thấy rằng các toán tử gán phép có thể được sử dụng làm cơ sở cho các đối tác không hợp chất của chúng. Đó là, toán tử + được thực hiện dưới dạng +=, - được thực hiện dưới dạng -= v.v.

Theo quy tắc chung của chúng tôi, + và bạn đồng hành của nó không phải là thành viên, trong khi đối tác phân công hợp chất của họ (+= vv), thay đổi đối số trái của họ, phải là thành viên. Đây là mã mẫu cho += và +, các toán tử số học nhị phân khác cần được triển khai 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ó cho 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, việc trả về một tham chiếu thường hiệu quả hơn là trả lại 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+ mất toán hạng bên trái của nó theo bản sao thay vì tham chiếu const. Lý do cho điều này cũng giống như lý do cho operator= lấy đối số của nó cho mỗi bản sao.

Các toán tử thao tác bit ~  &  |  ^  <<  >> nên được thực hiện giống 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 các trường hợp này.

3  Một lần nữa, bài học được lấy từ đây là a += b nói chung, hiệu quả hơn a + b và nên được ưu tiên nếu có thể.

Array Subscripting

Toán tử chỉ số mảng là toán tử nhị phân phải được thực hiện như một thành viên của lớp. Nó được sử dụng cho các kiểu giống như vùng chứa cho phép truy cập vào các phần tử dữ liệu của chúng bằng một khóa. Hình thức kinh điển cung cấp những điều 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 trong lớp của bạn có thể thay đổi các phần tử dữ liệu được trả về bởi operator[] (trong trường hợp này bạn có thể bỏ qua biến thể không phải 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 một kiểu dựng sẵn, biến thể const của toán tử sẽ trả về một bản sao thay vì tham chiếu const.

Các toán tử cho các kiểu con trỏ giống như

Để xác định trình vòng lặp của riêng bạn hoặc con trỏ thông minh, bạn phải quá tải toán tử dereference tiền tố đơn nhất * và toán tử truy cập thành viên con trỏ infix 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 sẽ hầu như luôn cần cả phiên bản const và phiên bản không phải const. Cho -> toán tử, nếu value_type là của class (hoặc là struct hoặc là union) loại 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ớp.

Toán tử địa chỉ đơn nhất không bao giờ bị quá tải.

Dành cho operator->*() xem câu hỏi này. Nó hiếm khi được sử dụng và do đó hiếm khi bị quá tải. Trong thực tế, ngay cả vòng lặp không quá tải nó.


Tiếp tục Toán tử chuyển đổi


898
2017-12-12 12:47



operator->() thực sự là vô cùng kỳ dị. Không bắt buộc phải trả lại value_type* - trên thực tế, nó có thể trả về một loại lớp khác, miễn là loại lớp có operator->(), sau đó sẽ được gọi sau đó. Cuộc gọi đệ quy này của operator->()s tiếp tục cho đến khi value_type* loại trả về xảy ra. Điên cuồng! :) - j_random_hacker
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 con trỏ của bạn, ví dụ: `const value_type & operator * () const;` - điều này sẽ giống như có một T* const trả lại const T& trên dereferencing, đó không phải là trường hợp. Hay nói cách khác: một con trỏ const không ngụ ý một con trỏ const. Trong thực tế, nó không phải là tầm thường để bắt chước T const * - đó là lý do cho toàn bộ const_iterator công cụ trong thư viện chuẩn. Kết luận: chữ ký nên được reference_type operator*() const; pointer_type operator->() const - Arne Mertz
Một bình luận: Việc thực hiện 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. Se Boost các toán tử tiêu đề chú thích simmetry: boost.org/doc/libs/1_54_0/libs/utility/operators.htm#symmetry Có thể tránh thêm một bản sao nếu bạn sử dụng một bản sao cục bộ của tham số đầu tiên, làm + = và trả về bản sao cục bộ. Điều này cho phép tối ưu hóa NRVO. - Manu343726
Như tôi đã đề cập trong cuộc trò chuyện, L <= R cũng có thể được diễn tả như !(R < L) thay vì !(L > R). Có thể tiết kiệm thêm một lớp nội tuyến trong các biểu thức khó tối ưu hóa (và nó cũng là cách Boost.Operators thực hiện nó). - TemplateRex
@thomthom: Nếu một lớp không có API có thể truy cập công khai để nhận được trạng thái của nó, bạn sẽ phải thực hiện mọi thứ cần truy cập vào trạng thái của một thành viên hoặc một friend của lớp. Điều này, tất nhiên, cũng đúng cho tất cả các nhà khai thác. - sbi


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, có thực sự là ngoại lệ. Đôi khi mọi người đã bị lệ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 ít và xa. Ít nhất, 99 trong số 100 độ lệch mà tôi đã thấy là không hợp lý. Tuy nhiên, nó cũng có thể là 999 trong số 1000. Vì vậy, bạn nên tuân theo các quy tắc sau đây.

  1. Bất cứ khi nào ý nghĩa của một nhà điều hành không rõ ràng là rõ ràng và không thể tranh cãi, nó không nên bị quá tải.  Thay vào đó, hãy cung cấp một hàm có tên được chọn tốt.
    Về cơ bản, quy tắc đầu tiên và quan trọng nhất cho các nhà khai thác quá tải, tại chính trái tim của nó, nói: Đừng làm thế. Điều đó có vẻ lạ, bởi vì có rất nhiều điều cần biết về quá tải của nhà điều hành và rất nhiều bài viết, chương sách, và các văn bản khác đối phó với tất cả điều này. Nhưng bất chấp bằng chứng rõ ràng này, chỉ có một vài trường hợp đáng ngạc nhiên khi quá tải toán tử là thích hợp. Lý do là thực sự khó hiểu ngữ nghĩa đằng sau việc áp dụng toán tử trừ khi việc sử dụng toán tử trong miền ứng dụng được biết rõ và không thể tranh cãi. Trái ngược 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 tuân theo ngữ nghĩa nổi tiếng của nhà điều hành.
    C ++ 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 nhị phân + toán tử để trừ toán hạng bên phải của nó. Tuy nhiên, người dùng của một nhà điều hành như vậy sẽ không bao giờ nghi ngờ biểu thức a + b trừ a từ b. Tất nhiên, điều này giả định rằng ngữ nghĩa của toán tử trong miền ứng dụng là không thể tranh cãi.

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


Tiếp tục Quyết định giữa thành viên và không phải là thành viên.


441
2017-12-12 12:45



Điều duy nhất tôi biết là vi phạm bất kỳ điều nào trong số này là boost::spirit lol. - Billy ONeal
@ Billy: Theo một số, lạm dụng + cho chuỗi nối là một hành vi vi phạm, nhưng nó đã trở thành thành lập praxis, để nó có vẻ tự nhiên. Mặc dù tôi nhớ một lớp chuỗi home-brew tôi thấy trong những năm 90 sử dụng nhị phân &cho mục đích này (đề cập đến BASIC cho việc thành lập praxis). Nhưng, vâng, đặt nó vào trong thư viện std về cơ bản đã thiết lập điều này bằng đá. Cũng vậy vì lạm dụng << và >> cho IO, BTW. Tại sao việc dịch chuyển trái lạ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 "Hello, world!" Đầu tiên của chúng ta! ứng dụng. Và không có lý do nào khác. - sbi
@curiousguy: Nếu bạn phải giải thích nó, nó 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
@sbi: "đánh giá ngang hàng" luôn là ý tưởng hay. Đối với tôi, một toán tử được chọn không khác với tên hàm bị chọn sai (tôi thấy nhiều). Toán tử chỉ là các hàm. Không nhiều không ít. Quy tắc chỉ giống nhau. Và để hiểu nếu một ý tưởng là tốt, cách tốt nhất là hiểu phải mất bao lâu để hiểu được. (Do đó, đánh giá ngang hàng là phải, nhưng các đồng nghiệp phải được lựa chọn giữa những người không có kiến ​​thức và định kiến.) - Emilio Garavaglia
@sbi Với tôi, sự thật hoàn toàn rõ ràng và không thể chối cãi về 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ó rất nhiều quan hệ tương đương hữu ích trên các thùng chứa. Bình đẳng nghĩa là gì? "a bằng b"có nghĩa là a và b có cùng giá trị toán học. Khái niệm về giá trị toán học của một (không phải NaN) float là rõ ràng, nhưng giá trị toán học của một vùng chứa có thể có nhiều định nghĩa hữu ích khác nhau (loại đệ quy). Định nghĩa bình đẳng mạnh nhất là "chúng là cùng một đối tượng", và nó là vô dụng. - curiousguy


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 kiểu dựng sẵn trong C ++, các toán tử chỉ có thể bị quá tải cho các kiểu do người dùng định nghĩa1. Đó là, ít nhất một trong các toán hạng phải thuộc loại do người dùng xác định. Như với các hàm bị quá tải khác, các toán tử có thể bị quá tải cho một tập hợp các thông số nhất định chỉ một lần.

Không phải tất cả các toán tử đều 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: +  -  *  /  % và +=  -=  *=  /=  %= (tất cả các thông tin nhị phân); +  - (tiền tố đơn nhất); ++  -- (tiền tố đơn nhất và postfix)
  • thao tác bit: &  |  ^  <<  >> và &=  |=  ^=  <<=  >>= (tất cả các thông tin nhị phân); ~ (tiền tố đơn nhất)
  • đại số Boolean: ==  !=  <  >  <=  >=  ||  && (tất cả các thông tin nhị phân); ! (tiền tố đơn nhất)
  • quản lý bộ nhớ: new  new[]  delete  delete[]
  • toán tử chuyển đổi ngầm
  • miscellany: =  []  ->  ->*  ,  (tất cả các thông tin nhị phân); *  & (tất cả tiền tố đơn nhất) () (gọi hàm, 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 chức năng với tên đặc biệt. Như với các chức năng khác, các toán tử quá tải thường có thể được thực hiện như một hàm thành viên của kiểu toán hạng bên trái của họ hoặc là chức năng không phải thành viên. Cho dù bạn được tự do lựa chọn hoặc bị ràng buộc để sử dụng một trong hai phụ thuộc vào một số tiêu chí.2 Một toán tử đơn nhất @3, được áp dụng cho đối tượng x, được gọi hoặc là operator@(x) hoặc là x.operator@(). Toán tử nhị phân nhị phân @, áp dụng cho các đối tượng x và y, được gọi là operator@(x,y) hoặc là x.operator@(y).4 

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

1  Thuật ngữ "do người dùng xác định" có thể hơi gây hiểu lầm. C ++ tạo sự khác biệt giữa các kiểu dựng sẵn và các kiểu do người dùng định nghĩa. Đối với các cựu thuộc về int, char, và double; sau này thuộc về tất cả các loại struct, class, union và enum, bao gồm cả các loại từ thư viện chuẩn, mặc dù chúng không được định nghĩa 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  Các @ 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 thi như một hàm thành viên.


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


230
2017-12-12 12:46



%= không phải là toán tử "thao tác bit" - curiousguy
~ là tiền tố đơn nhất, không phải là nhị phân. - mrkj
.* bị thiếu trong danh sách các toán tử không thể quá tải. - celticminstrel
@celticminstrel: Thật vậy, và không ai để ý trong 4,5 năm ... Cảm ơn vì đã chỉ ra nó, tôi đặt nó vào. - sbi
@ H.R .: Bạn đã đọc hướng dẫn này chưa, bạn sẽ biết điều gì sai. Tôi thường khuyên bạn nên đọc ba câu trả lời đầu tiên được liên kết từ câu hỏi. Đó không phải là hơn nửa giờ của cuộc đời bạn, và cho bạn một sự hiểu biết cơ bản. Cú pháp của toán tử cụ thể mà bạn có thể tra cứu sau này. 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


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

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

Các toán tử khác có thể được thực hiện dưới dạng 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 thực hiện như các hàm không phải là thành viên, vì toán hạng bên trái của bạn 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 << và >>, 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ử, trong đó bạn phải chọn triển khai chúng như một hàm thành viên hoặc một hàm không phải thành viên, sử dụng các quy tắc sau đây của ngón tay cái quyết định:

  1. Nếu nó là một toán tử đơn nhất, thực hiện nó như một hội viên chức năng.
  2. Nếu một toán tử nhị phân xử lý cả hai toán hạng đều (nó để chúng không thay đổi), thực hiện toán tử này như là một không phải thành viên chức năng.
  3. Nếu toán tử nhị phân không phải đối xử với cả hai toán hạng của nó bằng nhau (thông thường nó sẽ thay đổi toán hạng bên trái của nó), nó có thể hữu ích để biến nó thành hội viên chức năng của loại 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 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ể làm đ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<() cho một mẫu lớp lồng nhau trong một mẫu lớp dễ dàng hơn nhiều khi viết và đọc khi được thực hiện như 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 thực hiện một ngoại lệ, đừng quên vấn đề const-Đối với toán hạng đó, đối với các hàm thành viên, trở thành ngầm this tranh luận. Nếu toán tử là một hàm không phải thành viên sẽ lấy đối số ngoài cùng bên trái của nó như là một const tham chiếu, toán tử giống như hàm thành viên cần có const ở cuối để làm *this một const tài liệu tham khảo.)


Tiếp tục Các toán tử thường gặp quá tải.


212
2017-12-12 12:49



Mục của Herb Sutter trong hiệu quả C ++ (hoặc là C ++ Coding Standard?) Nói rằng người ta không thích các hàm không phải là thành viên không phải là thành viên đối với 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 cho 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
@paercebal: Hiệu quả C ++ là bởi Meyers, Tiêu chuẩn mã hóa C ++ bởi Sutter. Mà một trong những bạn đang đề cập đến? Dù sao, tôi không thích ý tưởng, 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ó, do đó theo định nghĩa nó phải đào sâu vào bên trong nó. Những gì bạn sẽ đạt được bằng cách không làm cho nó thành viên? - sbi
@sbi: Mục 44 trong C ++ Tiêu chuẩn mã hóa (Sutter) Thích viết các hàm nonfriend không phải là thành viên, 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ỉ sử dụng giao diện công khai của lớp đó. Nếu bạn không thể (hoặc có thể nhưng nó sẽ cản trở hiệu suất xấu), thì bạn phải biến nó thành thành viên hoặc bạn bè. - Matthieu M.
@sbi: Rất tiếc, Hiệu quả, Đặc biệt ... Không có thắc mắc tôi trộn tên lên. Dù sao đạt được là để hạn chế càng nhiều càng tốt số lượng các chức năng có quyền truy cập vào một đối tượng dữ liệu cá nhân / bảo vệ. Bằng cách này, bạn tăng sự đóng gói của lớp học 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
@sbi: Một ví dụ. Giả sử bạn đang mã hóa một lớp String, với cả hai operator += và append phương pháp. Các append phương thức này hoàn chỉnh hơn, bởi vì bạn có thể nối thêm một chuỗi con của tham số từ chỉ mục i đến chỉ mục n -1: append(string, start, end) Có vẻ hợp lý để có += cuộc gọi nối thêm start = 0 và end = string.size. Tại thời điểm đó, nối thêm có thể là một phương pháp thành viên, nhưng operator += không cần phải là một 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 bên trong, vì vậy nó là một điều tốt .... ^ _ ^ ... - paercebal


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 kiểu được xác định khác. Có hai loại toán tử chuyển đổi, các toán tử tiềm ẩn và rõ ràng.

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

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

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

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, giống như các hàm tạo một đối số, là các chuyển đổi do người dùng xác định. 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 một cuộc gọi đến một hàm bị 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à chuyển đổi tiềm ẩn thậm chí đá vào 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à lvalue, vì vậy, đầ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 có được những lập trình viên C ++ sai và thậm chí đôi khi có kinh nghiệm 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 toán tử 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, 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 toán tử chuyển đổi rõ ràng:

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

Lưu ý 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 ngầm, bạn nhận được một lỗi trình biên dịch:

prog.cpp: Trong hàm ‘int main ()’:
prog.cpp: 15: 18: lỗi: không có hàm nào phù hợp để gọi ‘f (my_string)’
prog.cpp: 15: 18: lưu ý: ứng cử viên là:
prog.cpp: 11: 10: note: void f (my_string &)
prog.cpp: 11: 10: lưu ý: không có chuyển đổi nào được biết cho đối số 1 từ ‘my_string’ thành ‘my_string &’
prog.cpp: 12: 10: note: 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_string’ thành ‘const char *’

Để gọi toán tử cast rõ ràng, bạn phải sử dụng static_cast, một dàn diễn viên kiểu C hoặc dàn diễn viên tạo kiểu (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 chuyển đổi hoàn toàn 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 khác sau khi nó chuyển đổi thành bool (một trình biên dịch được phép thực hiện 2 chuyển đổi ngầm tại một thời điểm, nhưng chỉ có 1 chuyển đổi do người dùng xác định ở mức tối đa).

Bởi vì trình biên dịch sẽ không truyền "quá khứ" bool, các nhà khai thác chuyển đổi rõ ràng hiện đã loại bỏ sự cần thiết cho Thành ngữ Bool an toàn. Ví dụ, các con trỏ thông minh trước C ++ 11 sử dụng thành ngữ Safe Bool để ngăn các chuyển đổi thành các kiểu 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 rời sau khi nó chuyển đổi một cách rõ ràng một kiểu thành bool.

Tiếp tục Quá tải new và delete.


144
2018-05-17 18:32





Quá tải new và delete

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

Khái niệm cơ bản

Trong C ++, khi bạn viết biểu thức mới như new T(arg) hai điều xảy ra khi biểu thức này được đánh giá: Đầu tiên operator new được gọi để có được bộ nhớ thô, và sau đó là 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 hoạt động này: quản lý bộ nhớ và xây dựng / hủy diệt đối tượng tại bộ nhớ được cấp phát. Sau này đượ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 new và operator delete.

Đầu tiên của các quy tắc cơ bản của quá tải toán tử - đừng làm vậy - đặc biệt áp dụng cho quá tải new và delete. Hầu hết các lý do duy nhất để quá tải các nhà khai thác này là vấn đề hiệu suất và ràng buộc bộ nhớvà trong nhiều trường hợp, các hành động khác, như thay đổi các thuật toán được sử dụng, sẽ cung cấp nhiều tỷ lệ chi phí / lợi nhuận cao hơn hơn là cố gắng tinh chỉnh quản lý bộ nhớ.

Thư viện chuẩn C ++ đi kèm với một bộ định sẵn new và delete toán tử. Điều 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ớ phân bổ / deallocate đầu tiên cho một đối tượng, hai thứ hai 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ế những cái từ thư viện chuẩn.
Nếu bạn quá tải operator new, bạn nên luôn luôn quá tải phù 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 nhà xây dựng ném trong khi đánh giá một biểu thức mới, hệ thống thời gian chạy sẽ trả về bộ nhớ cho operator delete phù hợp với operator new được gọi để cấp phát bộ nhớ để tạo đối tượng. Nếu bạn không cung cấp kết hợp operator delete, cái mặc định được gọi, hầu như luôn luôn sai.
Nếu bạn quá tải new và delete, 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 có 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 tới:

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 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 delete không bao giờ được gọi, trừ khi hàm tạo X ném một ngoại lệ.

Bạn cũng có thể quá tải new và delete vớ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 dấu ngoặc đơn sau từ khóa new. Chỉ vì lý do lịch sử, các biến thể này thường được gọi là vị trí mới, ngay cả khi đối số của chúng không phải là để đặt một đối tượng tại một địa chỉ cụ thể.

Lớp mới cụ thể và xóa

Thông thường, bạn sẽ muốn tinh chỉnh quản lý bộ nhớ vì phép đo đã chỉ ra rằng các cá thể của một lớp cụ thể hoặc 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, các std::size_t đối số sẽ 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 lớp học có nguồn gốc, 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 nhà khai thác được xác định trước của thư viện chuẩn với 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.


131
2017-12-12 13:07



Tôi cũng không đồng ý rằng 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à để truy tìm lỗi. - Yttrill
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 trên toàn cầu mới / xóa nơi nó không được quan tâm nhiều. - Yttrill
@ 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ử" nghĩa là gì nghĩa là quá tải. Nó không có nghĩa là các hàm theo nghĩa đen bị quá tải, và đặc biệt toán tử mới sẽ không làm quá tải phiên bản của Standard. @sbi không tuyên bố ngược lại. Nó thường được gọi là "quá tải mới" nhiều như nó là phổ biến để nói "quá tải bổ sung nhà điều hành". - Johannes Schaub - litb
@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 mà đôi khi sử dụng nothrow Mới. - Alexandre C.
"Nếu bạn không cung cấp một toán tử phù hợp, cái 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 phù hợp, không có toán tử nào được gọi, và bạn bị rò rỉ bộ nhớ. (15.2.2, lưu trữ bị chiếm bởi đối tượng được deallocated chỉ khi tìm thấy một toán tử xóa ... thích hợp) - dascandy


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

Giả sử 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;

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

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

rất không trực quan.

Nếu bạn định nghĩa nó như 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;

rất trực quan.


29
2018-01-22 19:00