chức năng trao đổi bạn bè công cộng


169

Trong câu trả lời hay cho thành ngữ copy-and-exchange-idiom, có một đoạn mã tôi cần một chút trợ giúp:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        using std::swap; 
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
    // ...
};

và anh ấy thêm một ghi chú

Có những tuyên bố khác rằng chúng ta nên chuyên std :: exchange cho loại của mình, cung cấp một trao đổi trong lớp cùng với một hoán đổi chức năng miễn phí, v.v. và chức năng của chúng tôi sẽ được tìm thấy thông qua ADL. Một chức năng sẽ làm.

Với friendtôi là một chút về các điều khoản "không thân thiện", tôi phải thừa nhận. Vì vậy, câu hỏi chính của tôi là:

  • Trông giống như một chức năng miễn phí , nhưng bên trong cơ thể lớp?
  • Tại sao điều này không swaptĩnh ? Nó rõ ràng không sử dụng bất kỳ biến thành viên.
  • "Bất kỳ việc sử dụng trao đổi thích hợp sẽ tìm ra trao đổi qua ADL" ? ADL sẽ tìm kiếm các không gian tên, phải không? Nhưng nó cũng nhìn vào bên trong các lớp học? Hay là ở đây friendđi vào đâu?

Câu hỏi phụ:

  • Với C ++ 11, tôi có nên đánh dấu swaps của mình noexceptkhông?
  • Với C ++ 11 và phạm vi của nó , tôi có nên đặt friend iter begin()friend iter end()cùng một cách trong lớp không? Tôi nghĩ rằng friendkhông cần thiết ở đây, phải không?

Xem xét câu hỏi phụ về phạm vi dựa trên phạm vi: tốt hơn hết là viết các hàm thành viên và để lại quyền truy cập phạm vi trên start () và end () trong không gian tên std (§24.6.5), dựa trên phạm vi để sử dụng nội bộ này từ toàn cầu hoặc không gian tên std (xem §6.5.4). Tuy nhiên, nhược điểm là các chức năng này là một phần của tiêu đề <iterator>, nếu bạn không bao gồm nó, bạn có thể muốn tự viết chúng.
Vitus

2
tại sao nó không tĩnh - bởi vì một friendchức năng hoàn toàn không phải là một thành viên.
aschepler

Câu trả lời:


175

Có một số cách để viết swap, một số tốt hơn so với những cách khác. Tuy nhiên, theo thời gian, nó đã được tìm thấy một định nghĩa duy nhất hoạt động tốt nhất. Chúng ta hãy xem xét cách chúng ta có thể nghĩ về việc viết một swapchức năng.


Trước tiên chúng ta thấy rằng các thùng chứa như std::vector<>có một hàm thành viên một đối số swap, chẳng hạn như:

struct vector
{
    void swap(vector&) { /* swap members */ }
};

Đương nhiên, sau đó, lớp học của chúng ta cũng nên, phải không? Vâng, không thực sự. Thư viện tiêu chuẩn có tất cả những thứ không cần thiết , và một thành viên swaplà một trong số đó. Tại sao? Hãy tiếp tục đi.


Những gì chúng ta nên làm là xác định những gì hợp quy, và những gì lớp chúng ta cần làm để làm việc với nó. Và phương pháp hoán đổi kinh điển là với std::swap. Đây là lý do tại sao các chức năng thành viên không hữu ích: chúng không phải là cách chúng ta trao đổi mọi thứ, nói chung và không có liên quan đến hành vi của std::swap.

Vậy thì, để thực hiện std::swapcông việc chúng ta nên cung cấp (và std::vector<>nên cung cấp) một chuyên môn std::swap, phải không?

namespace std
{
    template <> // important! specialization in std is OK, overloading is UB
    void swap(myclass&, myclass&)
    {
        // swap
    }
}

Chà điều đó chắc chắn sẽ hoạt động trong trường hợp này, nhưng nó có một vấn đề rõ ràng: chuyên môn hóa chức năng không thể là một phần. Đó là, chúng tôi không thể chuyên môn hóa các lớp mẫu với điều này, chỉ có các cảnh báo cụ thể:

namespace std
{
    template <typename T>
    void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
    {
        // swap
    }
}

Phương pháp này hoạt động một số thời gian, nhưng không phải tất cả thời gian. Phải có cách tốt hơn.


Có! Chúng ta có thể sử dụng một friendhàm và tìm thấy nó thông qua ADL :

namespace xyz
{
    struct myclass
    {
        friend void swap(myclass&, myclass&);
    };
}

Khi chúng tôi muốn trao đổi một cái gì đó, chúng tôi kết hợp std::swap và sau đó thực hiện cuộc gọi không đủ điều kiện:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first

// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

Một friendchức năng là gì? Có sự nhầm lẫn xung quanh khu vực này.

Trước khi C ++ được chuẩn hóa, các friendhàm đã thực hiện một cái gì đó gọi là "tiêm tên bạn bè", trong đó mã hoạt động như thể hàm được viết trong không gian tên xung quanh. Ví dụ: đây là những tiêu chuẩn tương đương:

struct foo
{
    friend void bar()
    {
        // baz
    }
};

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Tuy nhiên, khi ADL được phát minh, điều này đã bị xóa. Các friendchức năng có thể sau đó chỉ được tìm thấy qua ADL; nếu bạn muốn nó là một hàm miễn phí, thì nó cần phải được khai báo như vậy ( ví dụ, xem cái này ). Nhưng lo! Có một vấn đề.

Nếu bạn chỉ sử dụng std::swap(x, y), tình trạng quá tải của bạn sẽ không bao giờ được tìm thấy, bởi vì bạn đã nói rõ ràng "nhìn vào std, và không nơi nào khác"! Đây là lý do tại sao một số người đề nghị viết hai hàm: một là hàm được tìm thấy thông qua ADL và hàm kia để xử lý các std::bằng cấp rõ ràng .

Nhưng như chúng ta đã thấy, điều này không thể hoạt động trong mọi trường hợp và chúng ta kết thúc với một mớ hỗn độn xấu xí. Thay vào đó, hoán đổi thành ngữ đã đi theo con đường khác: thay vì biến nó thành công việc của các lớp để cung cấp std::swap, đó là công việc của người trao đổi để đảm bảo họ không sử dụng đủ điều kiện swap, như trên. Và điều này có xu hướng hoạt động khá tốt, miễn là mọi người biết về nó. Nhưng vấn đề nằm ở chỗ: thật không trực quan khi cần sử dụng một cuộc gọi không đủ tiêu chuẩn!

Để làm cho điều này dễ dàng hơn, một số thư viện như Boost đã cung cấp chức năng boost::swap, chỉ thực hiện một cuộc gọi không đủ tiêu chuẩn swap, với std::swapkhông gian tên liên quan. Điều này giúp làm cho mọi thứ trở nên cô đọng một lần nữa, nhưng nó vẫn là một người lập dị.

Lưu ý rằng không có thay đổi trong C ++ 11 đối với hành vi std::swap, điều mà tôi và những người khác nghĩ nhầm sẽ là trường hợp. Nếu bạn đã từng chút bởi điều này, đọc ở đây .


Tóm lại: chức năng thành viên chỉ là tiếng ồn, chuyên môn hóa là xấu xí và không đầy đủ, nhưng friendchức năng đã hoàn thành và hoạt động. Và khi bạn trao đổi, hoặc sử dụng boost::swaphoặc không đủ tiêu chuẩn swapvới std::swapliên quan.


Một cách không chính thức, một tên được liên kết nếu nó sẽ được xem xét trong khi gọi hàm. Để biết chi tiết, đọc §3.4.2. Trong trường hợp này, std::swapthông thường không được xem xét; nhưng chúng ta có thể liên kết nó (thêm nó vào tập hợp các tình trạng quá tải được xem xét bởi không đủ tiêu chuẩn swap), cho phép tìm thấy nó.


10
Tôi không đồng ý rằng chức năng thành viên chỉ là tiếng ồn. Hàm thành viên cho phép std::vector<std::string>().swap(someVecWithData);, ví dụ , không thể có với swaphàm miễn phí vì cả hai đối số đều được truyền bởi tham chiếu không phải là const.
ildjarn

3
@ildjarn: Bạn có thể làm điều đó trên hai dòng. Có chức năng thành viên vi phạm nguyên tắc DRY.
GManNickG

4
@GMan: Nguyên tắc DRY không áp dụng nếu cái này được thực thi theo nghĩa khác. Nếu không ai ủng hộ một lớp học với hiện thực của operator=, operator+operator+=, nhưng rõ ràng những nhà khai thác trên các lớp học có liên quan được chấp nhận / dự kiến sẽ tồn tại cho đối xứng. Theo ý kiến ​​của tôi, thành viên swap+ không gian tên swapnằm trong phạm vi .
ildjarn

3
@GMan Tôi nghĩ rằng nó đang xem xét quá nhiều chức năng. Ít được biết đến, nhưng ngay cả một function<void(A*)> f; if(!f) { }có thể thất bại chỉ vì Atuyên bố một operator!chấp nhận fcũng như fcủa chính mình operator!(không thể, nhưng có thể xảy ra). Nếu function<>tác giả nghĩ rằng "ồ tôi có 'bool toán tử', tại sao tôi phải triển khai 'toán tử!'? Điều đó sẽ vi phạm DRY!", Điều đó sẽ gây tử vong. Bạn chỉ cần có một operator!triển khai AAcó một hàm tạo cho một function<...>, và mọi thứ sẽ bị phá vỡ, bởi vì cả hai ứng cử viên sẽ yêu cầu chuyển đổi do người dùng xác định.
Johannes Schaub - litb

1
Chúng ta hãy xem xét cách chúng ta có thể nghĩ về việc viết một hàm hoán đổi [thành viên]. Đương nhiên, sau đó, lớp học của chúng ta cũng nên, phải không? Vâng, không thực sự. Thư viện tiêu chuẩn có tất cả những thứ không cần thiết , và trao đổi thành viên là một trong số đó. GotW liên kết ủng hộ chức năng hoán đổi thành viên.
Xeverous

7

Mã đó tương đương ( gần như mọi cách) với:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second);
    // ...
};

inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
    using std::swap; 
    swap(first.mSize, second.mSize); 
    swap(first.mArray, second.mArray);
}

Một hàm bạn được định nghĩa bên trong một lớp là:

  • được đặt trong không gian tên kèm theo
  • tự động inline
  • có thể tham khảo các thành viên tĩnh của lớp mà không cần thêm bằng cấp

Các quy tắc chính xác nằm trong phần [class.friend](tôi trích dẫn đoạn 6 và 7 của dự thảo C ++ 0x):

Một hàm có thể được định nghĩa trong một khai báo bạn bè của một lớp khi và chỉ khi lớp đó là một lớp không cục bộ (9.8), tên hàm không đủ tiêu chuẩn và hàm có phạm vi không gian tên.

Một chức năng như vậy là ngầm định. Hàm friend được định nghĩa trong một lớp nằm trong phạm vi (từ vựng) của lớp mà nó được định nghĩa. Một hàm bạn được xác định bên ngoài lớp là không.


2
Trên thực tế, các hàm bạn bè không được đặt trong không gian tên kèm theo, trong C ++ tiêu chuẩn. Hành vi cũ được gọi là "tiêm tên bạn bè", nhưng được thay thế bởi ADL, được thay thế trong tiêu chuẩn đầu tiên. Xem đầu trang này . (Tuy nhiên, hành vi này khá giống nhau.)
GManNickG

1
Không thực sự tương đương. Mã trong câu hỏi làm cho nó swapchỉ hiển thị với ADL. Nó là một thành viên của không gian tên kèm theo, nhưng tên của nó không hiển thị với các hình thức tra cứu tên khác. EDIT: Tôi thấy rằng @GMan đã nhanh hơn một lần nữa :) @Ben luôn như vậy trong ISO C ++ :)
Johannes Schaub - litb

2
@Ben: Không, tiêm bạn bè không bao giờ tồn tại trong một tiêu chuẩn, nhưng nó đã được sử dụng rộng rãi trước đó là lý do tại sao ý tưởng (và hỗ trợ trình biên dịch) có xu hướng tiếp tục, nhưng về mặt kỹ thuật thì không có. friendCác hàm chỉ được tìm thấy bởi ADL và nếu chúng chỉ cần là các hàm miễn phí có friendquyền truy cập, chúng cần phải được khai báo như friendtrong lớp và như một khai báo hàm miễn phí bình thường bên ngoài lớp. Bạn có thể thấy sự cần thiết trong câu trả lời này , ví dụ.
GManNickG

2
@towi: Vì hàm friend nằm trong phạm vi không gian tên, nên câu trả lời cho cả ba câu hỏi của bạn sẽ trở nên rõ ràng: (1) Đây là một hàm miễn phí, cộng với việc bạn bè có quyền truy cập vào các thành viên riêng tư và được bảo vệ của lớp. (2) Nó hoàn toàn không phải là thành viên, không phải là trường hợp hay tĩnh. (3) ADL không tìm kiếm trong các lớp nhưng điều này không sao vì hàm friend có phạm vi không gian tên.
Ben Voigt

1
@Ben. Trong thông số kỹ thuật, hàm là một thành viên không gian tên và cụm từ "hàm có phạm vi không gian tên" có thể được hiểu là hàm này là một thành viên không gian tên (nó phụ thuộc khá nhiều vào ngữ cảnh của câu lệnh đó). Và nó thêm một tên vào không gian tên chỉ hiển thị với ADL (thực ra, IIRC một số phần mâu thuẫn với các phần khác trong thông số về việc có thêm tên nào hay không. Nhưng việc thêm tên là cần thiết để phát hiện các khai báo không tương thích được thêm vào đó không gian tên, vì vậy trên thực tế, một tên vô hình được thêm vào. Xem ghi chú tại 3.3.1p4).
Julian Schaub - litb
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.