Различное поведение модификатора доступа при наследовании зависит от этого ключевого слова и шаблонов или их отсутствия.

Я хочу понять 4 различных поведения модификаторов доступа в отношении наследования, когда речь идет о 4 комбинациях использования и/или пропуска templates и ключевого слова this. Весь следующий код выполнен в g++ 4.8:

Вот класс GrandChild, который privately наследуется от Parent, который privately наследуется от GrandParent, который имеет public enum n. Необъектный клиентский код может получить доступ к GrandParent::n, потому что последний является public enum. Но GrandParent::n недоступен изнутри GrandChild:

#include <iostream>
using namespace std;

struct GrandParent { enum {n = 0}; };

struct Parent : private GrandParent { enum {n = 1}; };

struct GrandChild : private Parent {
    enum {n = 2};
    void f() {cout << GrandParent::n << endl;}
    // ^ error: 'struct GrandParent GrandParent::GrandParent'
    // is inaccessible
};
int main() {
    cout << GrandParent::n << endl;
    // ^ non-object access would have outputted `0` had `GrandChild`'s
    // definition compiled or been commented out.
}

1.) Вызвана ли недоступность GrandParent::n изнутри GrandChild тем, что GrandChild владеет базовым подобъектом GrandParent, который скрывает необъектный доступ к GrandParent::num и чья 2-генерационная privateность делает недоступным базовый подобъект n? Я ожидал, что сообщение об ошибке будет об этом.

2.) Но, видимо, это не так. Почему ошибка жалуется на конструктор GrandParent?

3.) Добавление this-> к GrandParent::n в определение f() добавит ошибку, которую я ожидал в # 1, но не удалит жалобу ctor. Почему? Я предположил, что включение this-> является излишним и что его отсутствие приведет к тому, что поиск попытается найти n подобъекта GrandParent в пределах области действия GrandChild перед не-объектом n с менее непосредственной областью действия.

4.) Почему этот вариант шаблона компилируется? Функционально он похож на нешаблонный:

#include <iostream>
using namespace std;

template <unsigned int N>
struct bar : private bar<N - 1> {
    enum {num = N};
    void g() {
        static_assert(N >= 2, "range error");
        cout << bar<N - 2>::num << endl;
    }
};

template <>
struct bar<0> { enum {num = 0}; };

int main() {
    bar<2> b2;
    b2.g(); // Output: 0
}

5.) Добавление this-> к bar<N - 2>::num в определении g() вызывает ошибку компилятора, которую я ожидал только в #1. Но почему он не включает ошибку № 2? И почему его упущение не приводит к ошибке №2?


person CodeBricks    schedule 27.12.2013    source источник


Ответы (2)


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

Каждый (именованный) класс получает внедренное-имя-класса. Например:

struct GrandParent
{
    // using GrandParent = ::GrandParent;
    enum {n = 0};
};

Вы можете использовать это имя внедренного класса для ссылки на сам класс. Это не так полезно для обычных классов (где неквалифицированный поиск все равно может найти имя GrandParent в окружающей области видимости), но для производных классов и шаблонов классов:

namespace A
{
    struct Foo
    {
        // using Foo = ::A::Foo;
    };
};

struct Bar : A::Foo
{
    void woof(Foo); // using the injected-class-name `::A::Foo::Foo`
};

template<class T, int N, bool b>
struct my_template
{
    // using my_template = ::my_template<T, N, b>;
    void meow(my_template); // using the injected-class-name
};

Это не наследование, как в случае «это часть подобъекта базового класса», а способ указания неквалифицированного поиска: если имя не найдено в области видимости текущего класса, поиск будет выполняться в области видимости базового класса.

Теперь для первого (не шаблонного) примера в OP:

struct Parent : private GrandParent
{
    // using Parent = ::Parent;

    enum {n = 1}; // hides GrandParent::n
};

struct GrandChild : private Parent {
    // using GrandChild = ::GrandChild;

    enum {n = 2};
    void f() {cout << GrandParent::n << endl;}
    // ^ error: 'struct GrandParent GrandParent::GrandParent'
    // is inaccessible
};

Здесь выражение GrandParent::n вызывает неквалифицированный поиск имени GrandParent. Поскольку неквалифицированный поиск останавливается, когда имя найдено (и не учитывает окружающие области), он находит введенное имя класса GrandParent::GrandParent. То есть поиск ищет в области GrandChild (имя не найдено), затем в области Parent (имя не найдено) и, наконец, в области GrandParent (где он находит введенное имя класса). Это делается до и независимо от проверки доступа.

После того, как имя GrandParent найдено, проверяется доступность. Требуется поиск имени, чтобы перейти от Parent к GrandParent, чтобы найти имя. Этот путь заблокирован для всех, кроме членов и друзей Parent, поскольку наследство является частным. (Вы можете видеть сквозь этот путь, но не можете его использовать; видимость и доступность — ортогональные понятия.)


Вот стандартный [basic.lookup.unqual]/8:

Для членов класса X имя, используемое в теле функции-члена [...], должно быть объявлено одним из следующих способов:

  • до его использования в блоке, в котором он используется, или во включающем блоке, или
  • должен быть членом класса X или быть членом базового класса X, или
  • если X является вложенным классом класса Y [...]
  • [...]
  • если X является членом пространства имен N, или [...], до использования имени, в пространстве имен N или в одном из пространств имен, включающих N.

Поиск имени в базовых классах довольно сложен, поскольку может потребоваться рассмотрение нескольких базовых классов. Для одиночного наследования и поиска члена в области тела функции-члена оно начинается с класса, членом которого является эта функция, а затем проходит вверх по базовым классам (база, база базы, база базы базы, ..). См. [class.member.lookup]


Случай шаблона отличается, так как bar — это имя шаблона класса:

template <unsigned int N>
struct bar : private bar<N - 1> {
    enum {num = N};
    void g() {
        static_assert(N >= 2, "range error");
        cout << bar<N - 2>::num << endl;
    }
};

Здесь используется bar<N - 2>. Это зависимое имя, поскольку N является параметром шаблона. Поэтому поиск имени откладывается до момента создания экземпляра g. Специализацию bar<0> можно найти, даже если она объявлена ​​после функции.

Введенное имя класса bar может использоваться как имя шаблона (ссылаясь на шаблон класса) или как имя типа (ссылаясь на текущую реализацию) [temp.local]/1:

Подобно обычным (не шаблонным) классам, шаблоны классов имеют имя внедренного класса (раздел 9). Внедренное-имя-класса можно использовать как имя-шаблона или имя-типа. Когда он используется с списком-аргументов-шаблона, в качестве аргумента-шаблона для параметра-шаблона шаблона или в качестве окончательного идентификатора в спецификаторе уточненного типа объявления шаблона дружественного класса он ссылается на сам шаблон класса. В противном случае он эквивалентен имя-шаблона, за которым следуют параметры-шаблона шаблона класса, заключенные в <>.

То есть bar<N - 2> находит bar как внедренное имя класса текущего класса (экземпляр). Поскольку он используется с списком-шаблонов-аргументов, он относится к другой, несвязанной специализации bar. Имя введенного класса базового класса скрыто.

Доступ к bar<0>::num осуществляется не через путь доступа, который проходит через частное наследование, а напрямую через имя-внедренного-класса текущего класса, ссылаясь на сам шаблон класса. num, будучи открытым членом bar<0>, доступен.

person dyp    schedule 27.12.2013
comment
+1, принято. Блестящая разбивка поиска имени и ортогональности между видимостью и доступностью. cppreference разделяет уточнение всего выражения на основе каждого имени справа от :: знак препинания: «… неполное имя, то есть имя, которое не появляется справа от оператора разрешения области…» Означает ли это, что в bar<N – 2>::num num является квалифицированным, а bar<N – 2> неквалифицированным, поэтому последнее не помогает найти подобъект в b2? - person CodeBricks; 28.12.2013
comment
@CodeBricks Я не понимаю, почему вы говорите здесь о подобъектах. Объект — это область хранения с некоторыми свойствами (например, типом). Имя не является членом объекта, оно может быть членом класса. Вы правы в том, что в bar<N-2>::num bar является неквалифицированным, а num - квалифицированным. Даже если бы num был нестатическим элементом данных, поиск по имени должен найти его как имя (а не как объект, на который ссылается это имя). - person dyp; 28.12.2013
comment
Разве у b2 нет базового подобъекта bar<1>, у которого есть bar<0> базового подобъекта? Предназначен для того, чтобы спросить для b2, вызвала ли неквалифицированность bar<N – 2> в bar<N - 2>::num поиск от num до ::bar<0>::num, а не через базовый подобъект bar<0> объекта b2 (т. е. this->bar<0>::num). Мой вопрос сформулирован неправильно? (без сарказма). Должен ли я изменить выделенное курсивом на «не через введенное имя базового подобъекта bar<0>»? Я знаю, что различие между доступом к num спорно, потому что существует только один экземпляр. Я знаю, что this-> спорно из-за недоступности. - person CodeBricks; 28.12.2013
comment
@CodeBricks Да, b2 имеет подобъект bar<1>, а bar<0> имеет подобъект base. Но это не имеет значения для поиска имени. Имена не просматриваются через объекты, имена могут ссылаться только на объект (int x; есть объект типа int и x относится к этому объекту). Для класса, не являющегося шаблоном, неквалифицированный bar<N-2> будет искаться (также) в областях действия базового класса. Поскольку базовый класс шаблона класса bar является зависимым, он не рассматривается для неквалифицированного поиска. Если бы это было квалифицировано как something::bar<N-2>::num, только искалась бы область действия something. - person dyp; 28.12.2013
comment
@CodeBricks Как оказалось, мои рассуждения были неверны для случая с шаблоном: поиск в области базового класса действительно не выполняется, но введенное имя класса шаблона может использоваться в качестве имени шаблона. Таким образом, наследование не имеет значения для этого примера. Смотрите обновленный ответ. - person dyp; 28.12.2013

Для хорошего объяснения того, почему частное наследование не похоже на публичное и защищенное наследование, см. два хороших ответа из частное наследование.

С точки зрения общепринятого понимания наследования «частное наследование» в C++ — ужасно неправильное название: это не наследование (в том, что касается всего, что находится за пределами класса), а полная деталь реализации класса.

С внешней стороны частное наследование на самом деле очень похоже на композицию. Только внутри класса вы получаете особый синтаксис, который больше напоминает наследование, чем композицию.

Однако есть одно предостережение: C++ синтаксически рассматривает это как наследование со всеми вытекающими из этого преимуществами и проблемами, такими как видимость области действия и доступность. Кроме того, приведения в стиле C (но не приведения в C++!) фактически игнорируют видимость и, таким образом, преуспевают в приведении вашего указателя Derived к Base:

Base* bPtr = (Base*) new Derived();

Излишне говорить, что это зло.


Публичное наследование означает, что все знают, что Derived является производным от Base.

Защищенное наследование означает, что только Derived, друзья Derived и классы, производные от Derived, знают, что Derived является производным от Base.*

Частное наследование означает, что только Derived и друзья Derived знают, что Derived является производным от Base.

Поскольку вы использовали частное наследование, ваша функция main() не имеет понятия о наследовании от базы, поэтому не может назначить указатель.

Частное наследование обычно используется для выполнения отношения «реализовано в терминах». Одним из примеров может быть то, что Base предоставляет виртуальную функцию, которую вам нужно переопределить — и, следовательно, она должна быть унаследована, — но вы не хотите, чтобы клиенты знали, что у вас есть отношения наследования.

А также недоступный тип из-за частного наследования.

Это связано с тем, что введенное имя класса из A скрывает глобальный A внутри C. Хотя A виден, он недоступен (поскольку он импортируется как частный), отсюда и ошибка. Вы можете получить доступ к A, найдя его в глобальном пространстве имен:

void foo(::A const& a) {}

Так, например, это будет работать:

class GrandChild;
struct GrandParent { enum {n = 0}; };

struct Parent : private GrandParent { 
   enum {n = 1}; 
   friend GrandChild; 
};

struct GrandChild : private Parent {
    void f() {cout << GrandParent::n << endl;}
};

В противном случае вам нужно использовать глобальную область или директиву using, чтобы ввести ::GrandParent в область видимости.

person Community    schedule 28.12.2013
comment
+1. Ваши ссылки и пример инъекции friend полезны, но принятый ответ самым прямым образом касается моих вопросов. - person CodeBricks; 28.12.2013