Почему не вызывается общедоступный константный метод, если неконстантный - частный?


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 ()' является закрытым '.

Но когда я удаляю частный, он просто работает. Почему общедоступный метод const не вызывается, если неконстантный метод является частным?

Другими словами, почему разрешение перегрузки предшествует контролю доступа? Это странно. Как вы думаете, это последовательно? Мой код работает, а затем я добавляю метод, и мой рабочий код вообще не компилируется.


3
В C ++ без дополнительных усилий, таких как использование идиомы PIMPL, нет реальной «частной» части класса. Это всего лишь одна из проблем (добавление перегрузки "частного" метода и нарушение старого кода компиляции в моей книге считается проблемой, даже если этого тривиально избежать, просто не выполняя ее), вызванных ею.
hyde

Есть ли какой-нибудь реальный код, в котором вы ожидаете, что сможете вызвать константную функцию, но что ее неконстантный аналог будет частью частного интерфейса? Для меня это звучит как плохой дизайн интерфейса.
Винсент Фурмонд,

Ответы:


125

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

void foo() const

и

void foo()

Теперь, поскольку aэто не так 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

В конечном итоге это сводится к утверждению в стандарте, что доступность не должна приниматься во внимание при выполнении разрешения перегрузки . Это утверждение можно найти в пункте 3 [over.match] :

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

а также примечание в пункте 1 того же раздела:

[Примечание: функция, выбранная с помощью разрешения перегрузки, не обязательно соответствует контексту. Другие ограничения, такие как доступность функции, могут сделать ее использование в вызывающем контексте некорректным. - конец примечания]

Что касается причины, я могу придумать несколько возможных мотивов:

  1. Это предотвращает неожиданные изменения поведения в результате изменения доступности кандидата на перегрузку (вместо этого произойдет ошибка компиляции).
  2. Он устраняет контекстную зависимость из процесса разрешения перегрузки (т. Е. Разрешение перегрузки будет иметь тот же результат, как внутри, так и вне класса).

32

Предположим, что управление доступом предшествовало разрешению перегрузки. Фактически это будет означать, что public/protected/privateвидимость будет контролироваться, а не доступность.

В разделе 2.10 « Проектирования и эволюции C ++» Страуструпа есть отрывок по этому поводу, в котором он обсуждает следующий пример.

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бы контролировать видимость, значение программы изменилось бы (global aбудет вызываться с помощью private, иначеX::a ).

Затем он заявляет, что не помнит, было ли это явным дизайном или побочным эффектом технологии препроцессора, использованной для реализации C с предшественником Classess для Standard C ++.

Как это связано с вашим примером? В основном потому, что стандартное разрешение перегрузки соответствует общему правилу, согласно которому поиск имени выполняется перед контролем доступа.

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

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

8 Если имя перегруженной функции обнаружено однозначно, разрешение перегрузки (13.3) также выполняется до управления доступом . Неоднозначности часто можно разрешить, уточнив имя с именем класса.


23

Поскольку неявный thisуказатель не является const, компилятор сначала проверяет наличие не- constверсии функции перед constверсией.

Если вы явно отметите не constодин, privateразрешение не будет выполнено, и компилятор не продолжит поиск.


Как вы думаете, это последовательно? Мой код работает, а затем я добавляю метод, и мой рабочий код вообще не компилируется.
Нарек

Я так думаю. Разрешение перегрузки намеренно суетливое. Вчера я ответил на аналогичный вопрос: stackoverflow.com/questions/39023325/…
Bathsheba

5
@Narek Я считаю, что это работает так же, как удаленные функции при разрешении перегрузки. Он выбирает лучший из набора, а затем видит, что он недоступен, поэтому вы получаете ошибку компилятора. Он выбирает не самую удобную функцию, а лучшую, а затем пытается ее использовать.
Натан Оливер,

3
@Narek Я также сначала задался вопросом, почему это не работает, но подумайте над этим: как бы вы когда-либо вызывали частную функцию, если бы общедоступную константную функцию следует выбирать также для неконстантных объектов?
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
Можно подумать, что «жизнеспособный» будет включать соответствующие ограничения доступа. Другими словами, «нецелесообразно» вызывать частную функцию извне класса, поскольку она не является частью открытого интерфейса этого класса.
RM

15

Это сводится к довольно простому дизайнерскому решению на C ++.

При поиске функции для удовлетворения вызова компилятор выполняет поиск следующим образом:

  1. Он ищет первую 1 область, в которой есть что- то с таким именем.

  2. Компилятор находит в этой области все функции (или функторы и т. Д.) С этим именем.

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

  4. Наконец, компилятор проверяет, доступна ли выбранная функция.

Да, из-за такого порядка компилятор может выбрать недоступную перегрузку, даже если есть другая доступная перегрузка (но не выбранная во время разрешения перегрузки).

Что касается того, можно ли поступить иначе: да, несомненно. Однако это определенно привело бы к совершенно другому языку, чем C ++. Оказывается, многие, казалось бы, довольно второстепенные решения могут иметь разветвления, влияющие на гораздо большее, чем может быть изначально очевидно.


  1. «Первый» сам по себе может быть немного сложным, особенно когда / если задействованы шаблоны, поскольку они могут вести к двухэтапному поиску, то есть есть два совершенно разных «корня», с которых можно начать поиск. Однако основная идея довольно проста: начните с наименьшей охватывающей области действия и продвигайтесь наружу к все большей и большей охватывающей области.

1
Страуструп предполагает в D&E, что это правило могло быть побочным эффектом препроцессора, используемого в 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* );

По разрешению перегрузки первый будет выбран для неконстантного this, второй будет выбран для 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.