Почему метод public const не вызывается, когда не-const является закрытым?

Рассмотрим этот код:

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

Ошибка компилятора:

error: 'void A:: foo()' является частным.

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

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

Ответ 1

Когда вы вызываете 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 будет печататься. Это может быть очень сложно отследить в большой базе кода. Поскольку управление доступом происходит после разрешения перегрузки, я получаю хорошую ошибку компилятора, сообщающую мне, что функция, которую я хочу вызвать, не может быть вызвана, и я могу найти ошибку намного легче.

Ответ 2

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

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

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

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

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

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

Ответ 3

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

Раздел 2.10 Дизайн и эволюция С++ by Stroustrup содержит этот фрагмент, где он обсуждает следующий пример

int a; // global a

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

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

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

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

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

10.2 Поиск имени пользователя [class.member.lookup]

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

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

Ответ 4

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

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

Ответ 5

Важно помнить о порядке вещей, которые происходят:

  • Найдите все жизнеспособные функции.
  • Выберите наилучшую жизнеспособную функцию.
  • Если не существует какой-либо одной из лучших возможностей, или если вы не можете назвать наилучшую жизнеспособную функцию (из-за нарушений доступа или функции delete d), выполните сбой.

(3) происходит после (2). Это действительно важно, потому что иначе выполнение функций delete d или private станет своего рода бессмысленным и гораздо труднее рассуждать.

В этом случае:

  • Жизнеспособными функциями являются A::foo() и A::foo() const.
  • Лучшая жизнеспособная функция A::foo(), потому что последняя включает в себя преобразование квалификации на неявный аргумент this.
  • Но A::foo() есть private, и у вас нет доступа к нему, поэтому код плохо сформирован.

Ответ 6

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

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

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

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

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

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

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

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


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

Ответ 7

Элементы управления доступом (public, protected, private) не влияют на разрешение перегрузки. Компилятор выбирает void foo(), потому что это наилучшее совпадение. Тот факт, что он недоступен, не меняет этого. Удаление его оставляет только void foo() const, что является лучшим (то есть только) совпадением.

Ответ 8

В этом вызове:

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.

Ответ 9

На техническую причину ответили другие ответы. Я остановлюсь только на этом вопросе:

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

То, как был разработан язык. Намерение пытается, по мере возможности, назвать наилучшую жизнеспособную перегрузку. Если это не удастся, будет вызвана ошибка, чтобы напомнить вам снова рассмотреть дизайн.

С другой стороны, предположим, что ваш код скомпилирован и хорошо работает с вызываемой функцией-членом const. Когда-нибудь кто-то (возможно, вы сами) решите изменить доступность функции-члена не const от private до public. Тогда поведение изменилось бы без ошибок компиляции! Это было бы неожиданностью.

Ответ 10

Поскольку переменная a в функции main не объявляется как const.

Константные функции-члены вызываются в постоянных объектах.

Ответ 11

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

Таким образом, если вы измените спецификатор доступа, вы будете уведомлены во время компиляции, если в существующем коде есть нарушение; если конфиденциальность была принята во внимание для разрешения вызова функции, ваше поведение в программе может бесшумно измениться.