Почему константный метод может вызвать неконстантный метод члена класса?

В коде ниже константный метод класса B вызывает неконстантный метод своего члена A &a_ref, что, казалось бы, противоречит логике const квалификатора (по идее код не должен компилироваться). Почему, собственно, так происходит?

Минимальный код:

#include <iostream>

class A
{
public:
    void NonConstHello() /*const*/
    {
        std::cout << "non const Hello from A!" << std::endl;
    }
};

class B
{
public:
    A &a_ref;

    explicit B(A &a) : a_ref(a) {}
    void ConstHello() const /*const method*/
    {
        a_ref.NonConstHello();  // calls non-const method for his member???
        std::cout << "    const Hello from B!" << std::endl;
    }
};

int main()
{
    A a;
    B b(a);
    b.ConstHello();
    return 0;
}

Вывод:

non const Hello from A!
    const Hello from B!

Ответы (1 шт):

Автор решения: zcorvid

В приведённом коде нет вызова неконстантного метода у константного объекта

Код из вопроса эквивалентен следующему:

#include <iostream>

class A
{
public:
    void NonConstHello() /*const*/
    {
        std::cout << "non const Hello from A!" << std::endl;
    }
};

class B
{
public:
// pointer instead of reference
    A *a_ptr;

    explicit B(A *a) : a_ptr(a) {}
    void ConstHello() const /*const method*/
    {
        (*a_ptr).NonConstHello();
        std::cout << "    const Hello from B!" << std::endl;
    }
};

int main()
{
    A a;
    B b(&a);
    b.ConstHello();
    return 0;
}

Причём эквивалентен как функционально, так и по определению ссылки.

Разберёмся, что тут происходит с const-квалификатором.

Константность метода класса B означает, что все его члены становятся константными, как значения, то есть указатель становится константным, получая тип A * const (а не const A*!).

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

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


Вернёмся к исходному примеру со ссылкой.

Теперь всё становится ясно. Ссылка это, если очень грубо, "указатель, неявно приводящийся к объекту там, где это необходимо". Вызов метода у ссылки на самом деле это не вызов метода ссылки, а вызов метода объекта, размещённого по соответствующему адресу.

Константность метода B запрещает нам менять член, который является ссылкой (то есть менять её адрес), но менять данные, на которые ссылка указывает нам не воспрещено, что мы успешно и делаем, вызывая неконстантный метод.

Грубо говоря, a_ref в константном методе класса B имеет тип не const A&, а A & const (справедливости ради заметим, что ссылку нельзя менять в любом случае, она лишь один раз инициализируется, и больше её адрес неизменен, по сути она итак как адрес константная).

→ Ссылка