Чому публічний метод const не називається, коли non-const - приватний?


117

Розглянемо цей код:

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();
}

Помилка компілятора:

помилка: 'void A :: foo ()' є приватним '.

Але коли я видаляю приватне, воно просто працює. Чому метод public const не називається, коли non-const є приватним?

Іншими словами, чому дозвіл на перевантаження виникає перед контролем доступу? Це дивно. Як ви вважаєте, це відповідає? Мій код працює, і тоді я додаю метод, і мій робочий код зовсім не збирається.


3
У C ++, без додаткових зусиль, як-от використання ідіоми PIMPL, немає реальної "приватної" частини класу. Це лише одна з проблем (додавання "приватного" методу перевантаження та порушення компіляції старого коду вважається проблемою в моїй книзі, навіть якщо цей тривіальний варіант, уникнути якого, не роблячи цього), викликаний ним.
hyde

Чи існує який-небудь код у реальному житті, де можна було б сподіватися викликати функцію const, але його нестандартний аналог буде частиною приватного інтерфейсу? Це звучить як поганий дизайн інтерфейсу.
Вінсент Фурмонд

Відповіді:


125

Коли ви телефонуєте a.foo();, компілятор проходить роздільну здатність перевантаження, щоб знайти найкращу функцію, яку слід використовувати. Коли він будує встановлений набір перевантажень

void foo() const

і

void foo()

Тепер, оскільки aце не так const, найкраща відповідність має non-const версія, тому компілятор вибирає void foo(). Потім встановлюються обмеження доступу, і ви отримуєте помилку компілятора, оскільки void foo()є приватною.

Пам'ятайте, що в роздільній здатності перевантаження це не "знайти найкращу функціональну функцію". Це "знайти найкращу функцію та спробувати її використати". Якщо він не може через обмеження доступу або видалений, ви отримуєте помилку компілятора.

Іншими словами, чому перед контролем доступу виникає дозвіл на перевантаження?

Що ж, давайте подивимось:

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);
}

Тепер скажемо, що насправді я не мав намір робити void foo(Derived * d)приватним. Якщо контроль доступу з’явився першим, ця програма збиралася б і запускалася та Baseбула б надрукована. Це може бути дуже важко знайти у великій базі коду. Оскільки контроль доступу приходить після вирішення перевантаження, я отримую гарну помилку компілятора, яка повідомляє мені функцію, яку я хочу, щоб її викликали, не можна викликати, і я можу знайти помилку набагато простіше.


Чи є якась причина, чому контроль доступу після вирішення перевантаження?
drake7707

3
@ drake7707 У моєму кодовому зразку я покажу, якби контроль доступу був спочатку, тоді буде зібраний вищезгаданий код, що змінює семантику програми. Не впевнений у вас, але я скоріше маю помилку і мені потрібно робити явний переклад, якби я хотів, щоб функція залишалася приватною, а тоді неявний склад і код мовчки "працює".
NathanOliver

"і мені потрібно зробити чіткий склад, якщо я хотів, щоб функція залишалася приватною" - це здається, що справжня проблема тут - неявні касти ... хоча з іншого боку, ідея про те, що ви також можете неявно використовувати похідний клас як базовий клас є визначальною характеристикою парадигми ОО, чи не так?
Стівен Бикс

35

Зрештою, це зводиться до твердження стандарту, що доступність не повинна враховуватися під час вирішення перевантаження . Це твердження може бути знайдено у пункті [over.match] пункту 3:

... Коли дозвіл на перевантаження проходить успішно, а найкраща життєздатна функція недоступна (пункт [class.access]) у контексті, в якому вона використовується, програма неправильно формується.

а також Примітка в пункті 1 цього ж розділу:

[Примітка: Функція, обрана роздільною здатністю перевантаження, не гарантована відповідно до контексту. Інші обмеження, такі як доступність функції, можуть зробити її використання в контексті виклику неправильним. - кінцева примітка]

Щодо того, я можу придумати пару можливих мотивацій:

  1. Це запобігає несподіваним змінам поведінки внаслідок зміни доступності кандидата на перевантаження (натомість відбудеться помилка компіляції).
  2. Це знімає контекстну залежність від процесу вирішення перевантаження (тобто роздільна здатність перевантаження буде мати той же результат, як усередині класу, так і поза ним).

32

Припустимо, контроль доступу прийшов до вирішення перевантаження. Ефективно це означатиме, що public/protected/privateконтролюється видимість, а не доступність.

Розділ 2.10 « Дизайн та еволюція C ++» від Stroustrup має уривок про це, де він обговорює наступний приклад

int a; // global a

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

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

Страуструп згадує про те , що користь від нинішніх правил (видимість до того доступності) є те , що (тимчасово) чейнінга на privateвнутрішню class Xв public(наприклад , для цілей налагодження) є те , що немає ніяких змін тихо в значенні вище програми (тобто X::aзроблена спроба отримати доступ в обох випадках, що дає помилку доступу у наведеному вище прикладі). Якщо public/protected/privateконтролювати видимість, зміст програми зміниться (глобальне aназивалося б privateінакше X::a).

Потім він заявляє, що не пам'ятає, чи це було за чітким дизайном чи побічним ефектом технології препроцесора, що використовується для впровадження C з попередником Classess до стандарту C ++.

Як це пов’язано з вашим прикладом? В основному тому, що стандартна роздільна здатність перевантаження відповідає загальному правилу, що пошук імен відбувається перед контролем доступу.

10.2 Пошук імені учасника [class.member.lookup]

1 Пошук імені члена визначає значення імені (вираз id) в області класу (3.3.7). Пошук імен може призвести до неоднозначності, в цьому випадку програма неправильно формується. Для id-вираження пошук імен починається в області класу цього; для кваліфікованого ідентифікатора пошук імен починається в області специфікованого імені вкладеного імені. Пошук імен відбувається перед контролем доступу (3.4, п. 11).

8 Якщо ім'я перевантаженої функції однозначно знайдено, роздільна здатність перевантаження (13.3) також має місце перед контролем доступу . Неоднозначності часто можна вирішити, визначивши ім’я з назвою його класу.


23

Оскільки неявний thisпокажчик не є const, компілятор спочатку перевірить наявність неверсійної constфункції перед constверсією.

Якщо ви чітко позначите не- constодин, privateроздільна здатність не вдасться, і компілятор не буде продовжувати пошук.


Як ви вважаєте, це відповідає? Мій код працює, а потім я додаю метод, і мій робочий код зовсім не збирається.
Нарек

Я так думаю. Розв’язання перевантаження навмисно суєтне. Я відповів на подібне запитання вчора: stackoverflow.com/questions/39023325/…
Вірсавія,

5
@Narek Я вважаю, що це працює так само, як виконуються видалені функції в роздільній здатності перевантаження. Він вибирає найкращий з набору, а потім бачить, що він недоступний, тому ви отримуєте помилку компілятора. Він вибирає не найкращу функціональну функцію, а найкращу функцію, а потім намагається її використовувати.
NathanOliver

3
@Narek Я також спершу поцікавився, чому це не працює, але врахуйте це: як ви коли-небудь називатимете приватну функцію, якщо публічний const також слід обирати для об'єктів, які не є const?
idclev 463035818

20

Важливо пам’ятати про порядок того, що відбувається, а саме:

  1. Знайдіть усі життєздатні функції.
  2. Виберіть найкращу життєздатну функцію.
  3. Якщо немає точно однієї найкращої життєздатності або якщо ви не можете фактично назвати найкращу функціональну функціональність (через порушення доступу чи функцію deleted), не виходить .

(3) відбувається після (2). Що дійсно важливо, бо в іншому випадку виконання функцій deleted або privateстало б якось безглуздим і набагато складніше міркувати.

В цьому випадку:

  1. Життєздатними функціями є A::foo()і A::foo() const.
  2. Найкраща життєздатна функція полягає в A::foo()тому, що остання передбачає перетворення кваліфікації на неявний thisаргумент.
  3. Але A::foo()є privateі ви не маєте до нього доступу, отже, код неправильно сформований.

1
Можна подумати, що "життєздатний" буде включати відповідні обмеження доступу. Іншими словами, непридатним викликати приватну функцію поза класом, оскільки це не є частиною публічного інтерфейсу цього класу.
РМ

15

Це зводиться до досить базового дизайнерського рішення в C ++.

Під час пошуку функції для задоволення виклику компілятор здійснює пошук таким чином:

  1. Він шукає, щоб знайти перший 1 область, у якій є щось з цим ім'ям.

  2. Компілятор знаходить усі функції (або функтори тощо) з цим ім'ям у цій області.

  3. Тоді компілятор робить перевантажувальну роздільну здатність, щоб знайти найкращого кандидата серед тих, кого він знайшов (доступні вони чи ні).

  4. Нарешті, компілятор перевіряє, чи є обрана функція доступною.

Через таке впорядкування, так, можливо, компілятор вибере перевантаження, недоступне, навіть якщо є ще одне доступне перевантаження (але не вибране під час вирішення перевантаження).

Що стосується того , було б можливо зробити що - то по- іншому: так, це , безумовно , можливо. Це, безумовно, призведе до зовсім іншої мови, ніж C ++. Виявляється, що багато, здавалося б, досить незначних рішень можуть мати наслідки, які впливають набагато більше, ніж це може бути очевидно.


  1. "Перший" сам по собі може бути трохи складним, особливо коли / якщо шаблони залучаються, оскільки вони можуть призвести до двофазного пошуку, тобто два цілком окремі "корінці", починати з пошуку. Основна ідея досить просто , хоча: починаючи з найменшої огороджувальної рамки, і працювати ваш шлях назовні , щоб все більші й більші огороджувальні області.

1
У D&E Stroustrup припускає, що це правило може бути побічним ефектом препроцесора, який використовується в C з класами, який ніколи не переглядався, коли з'явилася більш досконала технологія компілятора. Дивіться мою відповідь .
TemplateRex

12

Контроль доступу ( public, protected, private) не впливає на перевантаження дозволу. Компілятор вибирає, void foo()тому що це найкраща відповідність. Те, що це недоступно, це не змінює. Вилучивши його, залишається лише той void foo() const, який тоді найкращий (тобто єдиний) збіг.


11

У цьому дзвінку:

a.foo();

Завжди є неявний thisпокажчик, наявний у кожній функції члена. А constкваліфікація thisвзята з виклику посилання / об'єкта. Вищеописаний виклик компілятором трактується як:

A::foo(a);

Але у вас є дві декларації, A::fooякі трактуються як :

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

За роздільною здатністю перевантаження, перший буде обраний для non-const this, другий буде вибраний для a const this. Якщо ви видалите перше, друге буде прив’язане до обох constі non-const this.

Після вирішення перевантаження, щоб вибрати найкращу функціональну функцію, приходить контроль доступу. Оскільки ви вказали доступ до обраного перевантаження як private, компілятор подасть скаргу.

Стандарт говорить так:

[class.access / 4] : ... У випадку перевантажених імен функції керування доступом застосовується до функції, вибраної роздільною здатністю перевантаження ....

Але якщо ви це зробите:

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

Тоді constпідійде тільки перевантаження.


Це СТРАХОВО, що після вирішення перевантаження, щоб вибрати найкращу функціональну функцію, відбувається контроль доступу . Контроль доступу повинен надходити перед вирішенням перевантаження, як якщо б у вас немає доступу, якщо ви взагалі не повинні враховувати це, що ви думаєте?
Нарек

@Narek, .. Я оновив свою відповідь посиланням на стандарт C ++. Це насправді має сенс, тому в C ++ є багато речей і ідіом, що залежить від такої поведінки
WhiZTiM

9

Технічна причина відповіла іншими відповідями. Я зупинюсь лише на цьому питанні:

Іншими словами, чому розв'язання перевантаження виникає перед контролем доступу? Це дивно. Як ви вважаєте, це відповідає? Мій код працює, а потім я додаю метод, і мій робочий код зовсім не збирається.

Ось так було розроблено мову. Намір намагається назвати найкращі життєздатні перевантаження, наскільки це можливо. Якщо це не вдасться, буде спровокована помилка, яка нагадує вам знову розглянути дизайн.

З іншого боку, припустимо, що ваш код складений і добре працює з constфункцією-членом, що викликається. Колись хтось (можливо, ти сам) потім вирішить змінити доступність функції, що не є constчленом, privateна " public. Тоді поведінка змінилася б без будь-яких помилок компіляції! Це було б несподіванкою .



8

Специфікатори доступу ніколи не впливають на пошук імен та дозвіл виклику функції. Функція вибирається до того, як компілятор перевірить, чи повинен виклик викликати порушення доступу.

Таким чином, якщо ви зміните специфікатор доступу, ви отримаєте сповіщення під час компіляції, якщо є порушення в існуючому коді; якщо конфіденційність була врахована для вирішення функціональних дзвінків, поведінка вашої програми могла б мовчки змінитися.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.