Tại sao một phương thức const công cộng không được gọi khi phương thức không phải const là riêng tư?


117

Hãy xem xét mã này:

struct A
{
    void foo() const
    {
        std::cout << "const" << std::endl;
    }

    private:

        void foo()
        {
            std::cout << "non - const" << std::endl;
        }
};

int main()
{
    A a;
    a.foo();
}

Lỗi trình biên dịch là:

error: 'void A :: foo ()' là riêng tư`.

Nhưng khi tôi xóa cái riêng tư thì nó vẫn hoạt động. Tại sao phương thức public const không được gọi khi phương thức không phải const là private?

Nói cách khác, tại sao giải quyết quá tải lại có trước kiểm soát truy cập? Điều này thật kỳ lạ. Bạn có nghĩ rằng nó là nhất quán? Mã của tôi hoạt động và sau đó tôi thêm một phương thức và mã làm việc của tôi hoàn toàn không biên dịch.


3
Trong C ++, không cần nỗ lực thêm như sử dụng thành ngữ PIMPL, không có phần "riêng tư" thực sự của lớp. Đây chỉ là một trong những vấn đề (thêm quá tải phương thức "riêng tư" và phá vỡ mã cũ biên dịch được coi là vấn đề trong sách của tôi, ngay cả khi vấn đề này là nhỏ cần tránh bằng cách không thực hiện nó) do nó gây ra.
hyde

Có bất kỳ mã đời thực nào mà bạn mong đợi có thể gọi một hàm const nhưng đối tác không phải const của nó sẽ là một phần của giao diện riêng tư không? Điều này nghe có vẻ như thiết kế giao diện xấu đối với tôi.
Vincent Fourmond

Câu trả lời:


125

Khi bạn gọi a.foo();, trình biên dịch sẽ đi qua giải quyết quá tải để tìm chức năng tốt nhất để sử dụng. Khi nó xây dựng bộ quá tải nó tìm thấy

void foo() const

void foo()

Bây giờ, vì akhông phải const, phiên bản không phải const là phù hợp nhất, vì vậy trình biên dịch chọn void foo(). Sau đó, các giới hạn truy cập được đưa ra và bạn gặp lỗi trình biên dịch, vì void foo()nó là riêng tư.

Hãy nhớ rằng, trong giải quyết quá tải, nó không phải là 'tìm chức năng có thể sử dụng tốt nhất'. Đó là 'tìm chức năng tốt nhất và cố gắng sử dụng nó'. Nếu nó không thể do hạn chế truy cập hoặc bị xóa, thì bạn sẽ gặp lỗi trình biên dịch.

Nói cách khác, tại sao giải quyết quá tải lại có trước kiểm soát truy cập?

Vâng, chúng ta hãy nhìn vào:

struct Base
{
    void foo() { std::cout << "Base\n"; }
};

struct Derived : Base
{
    void foo() { std::cout << "Derived\n"; }
};

struct Foo
{
    void foo(Base * b) { b->foo(); }
private:
    void foo(Derived * d) { d->foo(); }
};

int main()
{
    Derived d;
    Foo f;
    f.foo(&d);
}

Bây giờ chúng ta hãy nói rằng tôi thực sự không có ý định đặt void foo(Derived * d)riêng tư. Nếu quyền kiểm soát truy cập đến trước thì chương trình này sẽ biên dịch và chạy và Basesẽ được in. Điều này có thể rất khó để theo dõi trong một cơ sở mã lớn. Vì kiểm soát truy cập xuất hiện sau khi giải quyết quá tải, tôi nhận được một lỗi trình biên dịch đẹp cho tôi biết chức năng tôi muốn nó gọi không thể được gọi và tôi có thể tìm thấy lỗi dễ dàng hơn rất nhiều.


Có lý do gì tại sao kiểm soát truy cập sau khi giải quyết quá tải không?
drake7707

3
@ drake7707 Sẽ như tôi hiển thị trong mẫu mã của mình, nếu kiểm soát truy cập đến trước thì mã trên sẽ biên dịch, điều này sẽ thay đổi ngữ nghĩa của chương trình. Không chắc về bạn nhưng tôi thà gặp lỗi và cần thực hiện ép kiểu rõ ràng nếu tôi muốn hàm ở chế độ riêng tư sau đó là một phép ép kiểu ngầm và mã sẽ âm thầm "hoạt động".
NathanOliver

"và cần thực hiện ép kiểu rõ ràng nếu tôi muốn hàm giữ ở chế độ riêng tư" - có vẻ như vấn đề thực sự ở đây là truyền ngầm ... mặc dù mặt khác, ý tưởng rằng bạn cũng có thể sử dụng một lớp dẫn xuất ngầm như lớp cơ sở là một đặc điểm xác định của mô hình OO, phải không?
Steven Byks

35

Cuối cùng điều này đi đến khẳng định trong tiêu chuẩn rằng khả năng truy cập không nên được xem xét khi thực hiện giải quyết quá tải . Khẳng định này có thể được tìm thấy trong [over.match] khoản 3:

... Khi giải quyết quá tải thành công và không thể truy cập chức năng khả thi tốt nhất (Mệnh đề [class.access]) trong ngữ cảnh mà nó được sử dụng, chương trình không được hình thành.

và cả Lưu ý trong khoản 1 của cùng một phần:

[Lưu ý: Chức năng được chọn theo độ phân giải quá tải không được đảm bảo phù hợp với ngữ cảnh. Các hạn chế khác, chẳng hạn như khả năng truy cập của hàm, có thể làm cho việc sử dụng nó trong ngữ cảnh gọi không hợp lý. - ghi chú cuối]

Về lý do tại sao, tôi có thể nghĩ đến một vài động lực có thể có:

  1. Nó ngăn chặn những thay đổi không mong muốn của hành vi do thay đổi khả năng truy cập của ứng viên quá tải (thay vào đó, lỗi biên dịch sẽ xảy ra).
  2. Nó loại bỏ sự phụ thuộc ngữ cảnh khỏi quá trình giải quyết quá tải (nghĩa là giải quyết quá tải sẽ có cùng kết quả cho dù bên trong hay bên ngoài lớp).

32

Giả sử kiểm soát truy cập đến trước khi giải quyết quá tải. Về mặt hiệu quả, điều này có nghĩa là public/protected/privatekhả năng hiển thị được kiểm soát hơn là khả năng truy cập.

Phần 2.10 của Thiết kế và Tiến hóa C ++ của Stroustrup có một đoạn về vấn đề này, nơi anh ấy thảo luận về ví dụ sau

int a; // global a

class X {
private:
    int a; // member X::a
};

class XX : public X {
    void f() { a = 1; } // which a?
};

Stroustrup đề cập rằng lợi ích của các quy tắc hiện tại (khả năng hiển thị trước khả năng truy cập) là (tạm thời) điều khiển privatebên trong class Xthành public(ví dụ: cho mục đích gỡ lỗi) là không có sự thay đổi âm thầm trong ý nghĩa của chương trình trên (tức X::alà được cố gắng được truy cập trong cả hai trường hợp, điều này gây ra lỗi truy cập trong ví dụ trên). Nếu public/protected/privatesẽ kiểm soát khả năng hiển thị, ý nghĩa của chương trình sẽ thay đổi (toàn cầu asẽ được gọi bằng private, ngược lại X::a).

Sau đó, anh ta nói rằng anh ta không nhớ liệu đó là do thiết kế rõ ràng hay do tác dụng phụ của công nghệ tiền xử lý được sử dụng để triển khai C với Classess tiền nhiệm cho Standard C ++.

Điều này liên quan đến ví dụ của bạn như thế nào? Về cơ bản vì Tiêu chuẩn thực hiện giải quyết quá tải tuân theo quy tắc chung rằng tra cứu tên được thực hiện trước kiểm soát truy cập.

10.2 Tra cứu tên thành viên [class.member.lookup]

1 Tra cứu tên thành viên xác định ý nghĩa của tên (biểu thức id) trong phạm vi lớp (3.3.7). Việc tra cứu tên có thể dẫn đến sự không rõ ràng, trong trường hợp đó, chương trình không được định hình tốt. Đối với biểu thức id, tra cứu tên bắt đầu trong phạm vi lớp của điều này; đối với id đủ điều kiện, tra cứu tên bắt đầu trong phạm vi của mã định danh tên lồng nhau. Việc tra cứu tên diễn ra trước khi kiểm soát truy cập (3.4, Khoản 11).

8 Nếu tên của một hàm quá tải được tìm thấy một cách rõ ràng, thì việc giải quyết quá tải (13.3) cũng diễn ra trước khi kiểm soát truy cập . Sự mơ hồ thường có thể được giải quyết bằng cách đặt tên hợp lệ với tên lớp của nó.


23

thiscon trỏ ngầm định không phải là con trỏ const, trình biên dịch trước tiên sẽ kiểm tra sự hiện diện của một constphiên bản không phải của hàm trước một constphiên bản.

Nếu bạn đánh dấu rõ ràng không phải là constmột privatethì giải pháp sẽ không thành công và trình biên dịch sẽ không tiếp tục tìm kiếm.


Bạn có nghĩ rằng nó là nhất quán? Mã của tôi hoạt động và sau đó tôi thêm một phương thức và mã làm việc của tôi hoàn toàn không biên dịch.
Narek

Tôi nghĩ vậy. Giải quyết quá tải là cố ý cầu kỳ. Tôi đã trả lời một câu hỏi tương tự ngày hôm qua: stackoverflow.com/questions/39023325/…
Bathsheba,

5
@Narek Tôi tin rằng nó hoạt động giống như cách các hàm đã xóa hoạt động trong độ phân giải quá tải. Nó chọn cái tốt nhất từ ​​tập hợp và sau đó nó thấy nó không có sẵn, vì vậy bạn gặp lỗi trình biên dịch. Nó không chọn chức năng có thể sử dụng tốt nhất mà là chức năng tốt nhất và sau đó cố gắng sử dụng nó.
NathanOliver

3
@Narek Đầu tiên tôi cũng tự hỏi tại sao nó không hoạt động, nhưng hãy xem xét điều này: làm thế nào bạn có thể gọi hàm private nếu const công cộng cũng nên được chọn cho các đối tượng không phải const?
idclev 463035818 19/08/2016

20

Điều quan trọng cần ghi nhớ là thứ tự của những điều xảy ra, đó là:

  1. Tìm tất cả các chức năng khả thi.
  2. Chọn chức năng khả thi tốt nhất.
  3. Nếu không có chính xác một hàm khả thi tốt nhất hoặc nếu bạn thực sự không thể gọi hàm khả thi tốt nhất (do vi phạm quyền truy cập hoặc hàm bị deleted), thì thất bại.

(3) xảy ra sau (2). Điều nào thực sự quan trọng, bởi vì nếu không thì việc tạo ra các hàm deleted hoặc privatesẽ trở nên vô nghĩa và khó lý luận hơn nhiều.

Trong trường hợp này:

  1. Các chức năng khả thi là A::foo()A::foo() const.
  2. Hàm khả thi tốt nhất là A::foo()vì hàm sau liên quan đến chuyển đổi chất lượng trên thisđối số ngầm định .
  3. Nhưng A::foo()privatevà bạn không có quyền truy cập vào nó, do đó mã là vô hình thành.

1
Người ta có thể nghĩ rằng "khả thi" sẽ bao gồm các hạn chế truy cập có liên quan. Nói cách khác, nó không "khả thi" để gọi một hàm riêng từ bên ngoài lớp, vì nó không phải là một phần của giao diện công khai của lớp đó.
RM

15

Điều này đi đến một quyết định thiết kế khá cơ bản trong C ++.

Khi tìm kiếm hàm để đáp ứng một cuộc gọi, trình biên dịch thực hiện tìm kiếm như sau:

  1. Nó tìm kiếm để tìm 1 phạm vi đầu tiên có một cái gì đó có tên đó.

  2. Trình biên dịch tìm tất cả các chức năng (hoặc chức năng, v.v.) có tên đó trong phạm vi đó.

  3. Sau đó, trình biên dịch thực hiện giải quyết quá tải để tìm ra ứng cử viên tốt nhất trong số những người mà nó tìm thấy (cho dù chúng có thể truy cập được hay không).

  4. Cuối cùng, trình biên dịch kiểm tra xem chức năng đã chọn đó có thể truy cập được hay không.

Vì thứ tự đó, vâng, có thể trình biên dịch sẽ chọn quá tải không thể truy cập được, mặc dù có một quá tải khác có thể truy cập (nhưng không được chọn trong quá trình giải quyết quá tải).

Về việc liệu có thể làm những điều khác đi không: vâng, chắc chắn là có thể. Tuy nhiên, nó chắc chắn sẽ dẫn đến một ngôn ngữ hoàn toàn khác với C ++. Nó chỉ ra rằng rất nhiều quyết định có vẻ khá nhỏ có thể có những phân nhánh ảnh hưởng nhiều hơn những gì có thể rõ ràng ban đầu.


  1. Bản thân "đầu tiên" có thể hơi phức tạp, đặc biệt là khi / nếu các mẫu có liên quan, vì chúng có thể dẫn đến tra cứu hai giai đoạn, nghĩa là có hai "gốc" hoàn toàn riêng biệt để bắt đầu khi thực hiện tìm kiếm. Tuy nhiên, ý tưởng cơ bản khá đơn giản: bắt đầu từ phạm vi bao quanh nhỏ nhất và làm việc theo cách của bạn hướng ra ngoài đến phạm vi bao quanh lớn hơn và lớn hơn.

1
Stroustrup suy đoán trong D&E rằng quy tắc có thể là tác dụng phụ của bộ tiền xử lý được sử dụng trong C với các Lớp chưa bao giờ được xem xét sau khi công nghệ trình biên dịch tiên tiến hơn có sẵn. Hãy xem câu trả lời của tôi .
TemplateRex

12

Điều khiển truy cập ( public, protected, private) không ảnh hưởng đến tình trạng quá tải độ phân giải. Trình biên dịch chọn void foo()vì nó phù hợp nhất. Thực tế là nó không thể truy cập được không thay đổi điều đó. Loại bỏ nó chỉ để lại void foo() const, sau đó là kết quả phù hợp nhất (tức là, duy nhất).


11

Trong cuộc gọi này:

a.foo();

Luôn luôn thiscó sẵn một con trỏ ngầm trong mọi hàm thành viên. Và trình constđộ của thisđược lấy từ tham chiếu / đối tượng đang gọi. Lệnh gọi trên được trình biên dịch coi là:

A::foo(a);

Nhưng bạn có hai khai báo A::foođược xử lý như sau:

A::foo(A* );
A::foo(A const* );

Theo độ phân giải quá tải, đầu tiên sẽ được chọn cho không phải const this, thứ hai sẽ được chọn cho a const this. Nếu bạn xóa cái đầu tiên, cái thứ hai sẽ liên kết với cả constnon-const this.

Sau khi giải quyết quá tải để chọn chức năng khả thi tốt nhất, đi kèm với kiểm soát truy cập. Vì bạn đã chỉ định quyền truy cập vào quá tải đã chọn là private, trình biên dịch sau đó sẽ khiếu nại.

Tiêu chuẩn nói như vậy:

[class.access / 4] : ... Trong trường hợp tên hàm bị quá tải, điều khiển truy cập được áp dụng cho hàm được chọn theo độ phân giải quá tải ....

Nhưng nếu bạn làm điều này:

A a;
const A& ac = a;
ac.foo();

Sau đó, chỉ có constquá tải sẽ phù hợp.


Đó là CẠNH TRANH rằng Sau khi giải quyết quá tải để chọn chức năng khả thi tốt nhất, đi kèm với kiểm soát truy cập . Kiểm soát truy cập phải có trước khi giải quyết quá tải vì nếu bạn không có quyền truy cập thì bạn không nên xem xét nó chút nào, bạn nghĩ sao?
Narek

@Narek, .. Tôi đã cập nhật câu trả lời của mình với tham chiếu đến tiêu chuẩn C ++. Nó thực sự có ý nghĩa như vậy, có rất nhiều của sự vật và thành ngữ trong C ++ mà phụ thuộc vào hành vi này
WhiZTiM

9

Lý do kỹ thuật đã được trả lời bởi các câu trả lời khác. Tôi sẽ chỉ tập trung vào câu hỏi này:

Nói cách khác, tại sao giải quyết quá tải lại có trước kiểm soát truy cập? Điều này thật kỳ lạ. Bạn có nghĩ rằng nó là nhất quán? Mã của tôi hoạt động và sau đó tôi thêm một phương thức và mã làm việc của tôi hoàn toàn không biên dịch.

Đó là cách ngôn ngữ được thiết kế. Mục đích là cố gắng gọi là quá tải khả thi tốt nhất, càng nhiều càng tốt. Nếu không thành công, một lỗi sẽ được kích hoạt để nhắc bạn xem xét lại thiết kế.

Mặt khác, giả sử mã của bạn được biên dịch và hoạt động tốt với consthàm thành viên đang được gọi. Một ngày nào đó, một người nào đó (có thể là chính bạn) sau đó quyết định thay đổi khả năng truy cập của constchức năng không phải thành viên từ privatethành public. Sau đó, hành vi sẽ thay đổi mà không có bất kỳ lỗi biên dịch nào! Đây sẽ là một bất ngờ .



8

Các chỉ định truy cập không ảnh hưởng đến việc tra cứu tên và độ phân giải cuộc gọi hàm, bao giờ hết. Hàm được chọn trước khi trình biên dịch kiểm tra xem liệu lệnh gọi có kích hoạt vi phạm quyền truy cập hay không.

Bằng cách này, nếu bạn thay đổi mã xác định quyền truy cập, bạn sẽ được cảnh báo tại thời điểm biên dịch nếu có vi phạm trong mã hiện có; nếu quyền riêng tư được tính đến để giải quyết cuộc gọi chức năng, hành vi của chương trình của bạn có thể âm thầm thay đổi.

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.