суббота, марта 11, 2006

Права доступа при наследовании

С правами доступа при наследовании довольно легко запутаться. Мало того, что для данных и функций класса в С++ есть целых три уровня доступа: private, protected и public, еще ведь можно и само наследование сделать private, protected и public. Самым загадочным их них является protected-наследование. Запутаться во всем этом зоопарке очень просто, поэтому я аккуратно расписала где какой уровень досупа получится при каком наследовании. Известно, что private можно опускать при описании классов, private ставится по умолчанию. Но я не стала этого делать, чтобы было нагляднее.

Я беру базовый класс, CBase и смотрю какие из его данных видны его наследнику, наследнику наследника, и какие видны извне.


class CBase
{
private:
int privateBase;
protected:
int protBase;
public:
int pubBase;
};

Ни один из наследников не сможет доступиться к private-данным CBase. Как у них сложатся отношения с остальными данными зависит от того, как они были отнаследованы.

private наследование
Те данные, что в CBase были protected и public, стали private в CDerived.

class CDerived : private CBase
{
//унаследованные данные класса
//недоступно:
// int privateBase;
//private:
// int protBase;
//private:
// int pubBase;

public:
void updateDerived()
{
//privateBase=0; //нельзя доступиться
//к private данным CBase
protBase=0;
pubBase=0;
}
};

class CDerived1 : public CDerived
{
public:
void updateDerived1()
{
//privateBase=1; //нельзя доступиться
//к private данным CBase
//protBase=1;//protBase недоступно,
//потому что CDerived использовал
//private при наследовании от CBase
//pubBase=1; //pubBase недоступно, потому что
//CDerived использовал private
//при наследовании от CBase
}
};

При вызове извне, доступиться не получится ни к чему

CDerived dd;
//dd.privateBase=3; недоступно
//dd.protBase=3; недоступно
//dd.pubBase=3; недоступно


protected наследование
Редкий зверь. Встречается в тестах и на собеседованих. Удачных способов его применения в реальной жизни мало.
Те даннные, что в CBase были protected и public стали protected.

class CDerived : protected CBase
{
//унаследованные данные класса
//недоступно:
// int privateBase;
//protected:
// int protBase;
//protected:
// int pubBase;

public:
void updateDerived()
{
//privateBase=0; //нельзя доступиться к
//private данным CBase
protBase=0;
pubBase=0;
}
};

class CDerived1 : public CDerived
{
public:
void updateDerived1()
{
//privateBase=1; //нельзя доступиться к
//private данным CBase
protBase=1; //а тут уже все в порядке,
//в CDerived они стали protected
pubBase=1; // что значит, что наследник имеет
//к ним доступ
}
};

Доступ извне

CDerived dd;
//dd.privateBase=3; недоступно
//dd.protBase=3; недоступно
//dd.pubBase=3; недоступно


public наследование
protected и public данные из CBase остаются, соответственно protected и public.

class CDerived : public CBase
{
//унаследованные данные класса
//недоступно:
// int privateBase;
//protected:
// int protBase;
//public:
// int pubBase;

public:
void updateDerived()
{
//privateBase=0; //нельзя доступиться к
//private данным CBase
protBase=0;
pubBase=0;
}
};

class CDerived1 : public CDerived
{
public:
void updateDerived1()
{
//privateBase=1; //нельзя доступиться к
//private данным CBase
protBase=1;
pubBase=1;
}
};

Доступ извне

CDerived dd;
//dd.privateBase=3;
//dd.protBase=3;
dd.pubBase=3; //pubBase остался с уровнем доступа public,
//к нему теперь можно доступиться.


Ссылки по теме:
Inheritance — private and protected inheritance. Глава из C++ FAQ Lite.
What is protected inheritance?

36 коммент.:

Maniac комментирует...

Интересно как... Только после прочтения я осознал, что всю жизнь думал, что как раз "public" -- по умолчанию. А посмотрев в свой код, увидел, что везде "public" написано явно. То есть, пользовался я им совершенно бессознательно. :-)

Давид Мзареулян комментирует...

Вызывает интерес вот какой ещё разрез… Ведь чуть ли не самое главное правило наследования состоит в том, что если B унаследован от A, то любой экземпляр B есть A и его, скажем, можно передавать в функции от A-аргументов и т.д. Получается, что private-наследование это правило нарушает и весь полиморфизм идёт лесом?

Raider комментирует...

Где-то встречал такую фразу: "public наследование - это наследование интерфейса, а private наследование - это наследование реализации". Коротко и понятно :)

Alax комментирует...

Всегда думал, что все это наследование - очень простой вопрос. Куда сложнее для понимания наследование "Virtual Base Classes"

class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

А protected - отнюдь не "редкий зверь". Такие права оставляют доступ для унаследованных классов (как бы public для наследников), но скрывают для внешнего досутпа (как бы private для всего кроме наследников).

Alena комментирует...

Ведь чуть ли не самое главное правило наследования состоит в том, что если B унаследован от A, то любой экземпляр B есть A и его, скажем, можно передавать в функции от A-аргументов и т.д. Получается, что private-наследование это правило нарушает и весь полиморфизм идёт лесом?

Это весьма тонкий момент. Я встречала хвостатые флеймы по поводу взаимоотношений полиморфизма, наследования, что было раньше и т.п. Я придерживаюсь следующего мнения: полиморфизм - это одно, а наследование - это другое. В C++ полиморфизм очень красиво можно выразить с помощью public-наследования.
С private-наследованием тоже все непросто. Герб Саттер приводит пример, который он назвал контролируемым полиморфизмом (Controlled Polymorphism), в котором используется private-наследование.

"public наследование - это наследование интерфейса, а private наследование - это наследование реализации". Коротко и понятно :)

Угу, а про protected нигде ни слова...

alax комментирует...

> Только после прочтения я осознал, что всю жизнь думал, что как раз "public" -- по умолчанию.

struct A
{
//assumed public:
INT m_nValue;
};

class A
{
//assumed private:
INT m_nValue;
};

Alena комментирует...

А protected - отнюдь не "редкий зверь". Такие права оставляют доступ для унаследованных классов (как бы public для наследников), но скрывают для внешнего досутпа (как бы private для всего кроме наследников).

Со смыслом protected-наследования вопросов не возникает. А вот хорошее применение в реальной жизни... Я видела синтетические примеры, а вот реальные применения - нет, не помню... Если кто знает - поделитесь.

Alax комментирует...

> Со смыслом protected-наследования вопросов не возникает.
> А вот хорошее применение в реальной жизни... Я видела синтетические примеры,
> а вот реальные применения - нет, не помню... Если кто знает - поделитесь.

MS VC++.NET 2003, atlalloc.h, class CHeapPtrBase

template {class T, class Allocator = CCRTAllocator}
class CHeapPtrBase
{
protected:
CHeapPtrBase() throw() :
m_pData(NULL)
{
}

Здесь CHeapPtrBase - класс не для конечного использование, как бы "промежуточный". Его constructor и assignment operator упрятаны в protected, что препятствует возможности пользоваться этим классом напрямую, а только через наследников (eg. CHeapPtr)

atlcom.h, class ISpecifyPropertyPagesImpl

метод GetPagesHelper объявлен как protected: вообще говоря, не предполагается что он вообще будет использоваться откуда-то кроме как из ISpecifyPropertyPages::GetPages, соовтетственно он сделан недоступным для тех кому он не нужен. Однако унаследовавшись от этого класса можно им пользоваться в тех редких случаях когда нужно сконструировать список property pages, отличный от того, который создается по умолчанию.

Protected удобно использовать вместо private когда вы хотите оставить возможность прямого доступа для тех случаев, когда, возможно, вы захотите унаследоваться от класса, чтобы некоторым образом изменить его реализацию. Private такой возможности не подразумевает, а Public наоборот открывает доступ для всех.

Alena комментирует...

Здесь CHeapPtrBase - класс не для конечного использование, как бы "промежуточный"

В приведенных примерах говорится о protected-данных и protected-функциях. А я говорю о примере protected-наследования. Это разные вещи.

Alax комментирует...

> А я говорю о примере protected-наследования. Это разные вещи.

Разные, но не так чтобы как небо и земля. Пример наследования:

template {typename _Foo}
class CMyList1 : protected CAtlList{_Foo}
{
protected:
CComCritSect m_DataLock;

public:
// CMyList
VOID RemoveAll() throw()
{
CComCritSectLock DataLock(m_DataLock);
__super::RemoveAll();
}
};

Вариант класса list, который имеет свою критическую секцию для обеспечения thread safety. Почему нужно protected, а не public: теперь, как в показанном методе RemoveAll, весь доступ должен проходить через блокировку критической секции, так что все базовые методы уже не должны быть доступны "наружу".

Теперь зачем может понадобиться protected, а не private:

template {typename _Foo}
class CMyList2 : public CMyList1{_Foo}
{
public:
// CMyList2
VOID MyAction()
{
CComCritSectLock DataLock(m_DataLock);
// ...
CAtlList{CFoo}::RemoveAll();
}
};

Допустим есть еще один класс, в котором для, к примеру, избежания повторных множественных блокировок, мы хотим заблокировать единожды и в дальнейшем вызывать напрямую методы CAtlList, а не CMyList. С protected inheritence мы можем это делать. Если бы было private, то уже не могли бы - пришлось бы довольствоваться protected+public интерфесом класса CMyList1, а так у нас еще в добавок есть protected+public интерфейс класса CAtlList.

Alax комментирует...

В добавок, почему я не вижу большой разницы между наследованием и members.

class A : protected B { }; // 1
class A { protected: B m_B; }; // 2
class C: public A { };

класс B, который внутри класса А, недоступен вне класс A хоть в варианте 1, хоть в варианте 2. Разница всегод лишь для классов-наследников типа C и заключается в том как доступна реализация B, то ли через member, толи через базу. С практической точки зрения разница, на мой взгляд, минимальная.

Давид Мзареулян комментирует...

ОК… для полной уверенности, могу ли я написать:

class A {};
class B: protected A {};
A x = new B();

?
Это корректный код или он вылетит ещё при компиляции?

Alena комментирует...

Это корректный код или он вылетит ещё при компиляции?

Там, наверное, звездочка пропущена...

A* x = new B();

Такой код вызовет ошибку на этапе компиляции.
За исключением всяких экзотических случаев: например, этот код находится внутри функции, которая является friend'ом класса B.

Prokrust комментирует...

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

Prokrust комментирует...

alfx:
class A : protected B { }; // 1
class A { protected: B m_B; }; // 2
С практической точки зрения разница, на мой взгляд, минимальная.

Разница в наследовании виртуальных методов из класса B.

Alena комментирует...

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

Раз уж зашла об этом речь, был старый пост, в котором написано когда это может пригодиться: Приватные чисто виртуальные функции.

valker комментирует...

> Со смыслом protected-наследования вопросов не возникает. А вот хорошее применение в реальной жизни... Я видела синтетические примеры, а вот реальные применения - нет, не помню...
> Если кто знает - поделитесь.

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

Yuri комментирует...

У Вас тут прямо целый ликбезЪ

void комментирует...

Вся эта байда в C++ может быть полезна только для того, чтобы можно было писать такие uber-библиотеки для миллионов, используя которые ни один идиот не сможет сделать что-то неправильно, как бы ни старался. На практике для полного обезвреживания идиотов надо вставлять столько подобных ограничений, что такую библиотеку смогли написать только одну -- boost. А вы часто писали такие библиотеки? Хоть раз в жизни, не для себя,а действительно для тысяч идиотов?

Анонимный комментирует...

Статья написана простым и понятным языком. Спасибо.

Tele комментирует...

С private-наследованием связана интересная особенность: в классе потомка от потомка базового класса, который наследуется приватно, нельзя использовать тип базового класса. На любое упоминание базового класса внутри потомка компилятор выдаст ошибку, сообщив, что данный класс inaccessible. А вот пример (не компилируется):

class A{};
class B: private A{};
class C: public B{
public:
A* a;
};

Т.е. уровень доступа к родительскому классу - не просто доступ к его членам, но и доступ к имени класса внутри иерархии. Предполагается, что потомки класса B не должны даже знать о существовании A (либо потомков класса B вообще не должно быть).

Навскидку приведу четыре способа исправить данную ситуацию:

1. наследовать класс A в B с доступом protected;
2. не использовать пространство имен, которое получаем через наследование, а использовать более глобальное пространство;
3. использовать множественное наследование, и наследовать класс A вместе с классом B;
4. хак через шаблоны.

Со случаем 1 все ясно: нужен доступ к классу A внутри иерархии, и об этом прямо заявляется.

Способ 2 больше походит на неправильное использование права доступа при наследовании. Тем не менее, данный способ соответствует стандарту. Пример (компилируется):

class A{};
class B: private A{};
class C: public B{
public:
::A* a;
};

Способ 3 несколько опасен и может иметь побочные эффекты, но допускается стандартом. Однако, этот дурацкий компилятор наверняка выдаст назойливое предупреждение о двусмысленности. Но что нам, пионерам экстремального множественного наследования, какие-то жалкие предупреждения компилятора! :)

Со способом 4 возможны варианты. Некоторые компиляторы согласятся и на такой код:

class A{};
class B:private A{};
template < class T=A >class C:public B{
public:
C(A a){}
};

int main(){
A a;
C<> c=a;
cin.ignore();
}

А некоторые придется уговаривать:

class A{};
class B:private A{};
template< class T=A >class C:public B{
public:
C(T a){}
};

int main(){
A a;
C<> c=a;
cin.ignore();
}

Хороший компилятор должен отказать в обоих случаях.

Можно еще как-нибудь усложнить работу компилятору, благо шаблоны - отличное поле для работы.

Про protected. На мой взгляд, protected имеет смысл использовать (не всегда) для наследования реализации, если предполагается, что у данного класса будут потомки (и они могут менять реализацию/обращаться к реализации).

Алёна комментирует...

2Tele:
С private-наследованием связана интересная особенность

Tele, спасибо за прекрасные комментарии!

Если не лень, укажите, пожалуйста, какими компиляторами вы пользовались. И главы Стандарта, если помните.

Tele комментирует...

2Алёна

Компиляторы:
1. Visual C++ 2008 15.00.21022.8 (SP1)
2. Visual C++ 2010 16.00.21003.1 (beta2)
3. g++ 3.4.5

Пункты стандарта:
11/4
11.2/3
11.2/4


"A base class is said to be accessible if an invented public member of the base class is accessible" - механизм определения, является ли базовый класс доступным. Отсюда же следует, что изменить доступ к базовому классу можно только через наследование и friend-объявления.

Илья Весенний комментирует...

Лучше поздно, чем никогда. Поправьте, пожалуйста, опечатку в "При вызове извне, доступится не получится ни к чему" (пропущен мягкий знак в "доступиться").
Успехов!

Alena комментирует...

Илья Весенний

Поправьте, пожалуйста, опечатку

поправила, спасибо!

ash комментирует...

я страшно извиняюсь за занудство, но от слова "доступиться" немного ломает :)

хотя уже подмечено, что терминология наследования в целом довольно нечеловеческая, как например этот популярный оборот: "класс наследует такому-то базовому классу", как будто наследовать можно не что-то а кому-то, или не неотличимые без должной сноровки "суперкласс" и "сабкласс"..

Анонимный комментирует...

Очень поверхностная статья. Не дает ответ, например, на следующее:

#include
using namespace std;

class A
{
public:
A() {}
protected:
virtual void p() = 0;
};

class B: public A
{
public:
B() {}
public:
void p() {cout << "Hello";}
};

int main()
{
A* pA = new B();
pA->p(); // ошибка, protected
((B*)pA)->print(); // public
return 0;
}

Павел комментирует...

че эт не дает ответа вдруг?

#include
using namespace std;

class A
{
public:
A() {}
protected:
virtual void p() = 0;
};

class B: public A
{
public:
B() {}
public:
void p() {cout << "Hello";} //перегружается функция, которая до перегрузки будет, как правильно и описано, protected. Пионер сам перегружает ее как public, и это не часть механизма наследования
};

За статью огромное спасибо. Очень информативно и лаконично.

Анонимный комментирует...

Это не перегрузка, это переопределение виртуальной функции.
А вот вопрос про применение public, protected, private к виртуальным функциям достоин отдельной статьи
___________________________________
___________________________________
P.S:
Для классов по умолчанию первая секция private
class A
{
int a; //private член
public:
}

Для наследования тоже действует правило private по умолчанию
class B: A
равносильно
class B: private A
________________________
Однако если использовать вместо
class слово struct
вместо правила privateпо умолчанию
действует правило pulic по умолчанию
struct A
{
int a; //public член
public:
}

struct B: A
равносильно
class B: public A
_________________________________
Еще слово struct (в отличии от class) нельзя использовать в описании шаблонов
templateclass C{}; //ВЕРНО
templateclass C{};//НЕ ВЕРНО
___________________________________
Кроме выше описанный правил различий между class и struct в
С++ нет. (насколько мне известно, ссылки на стандарт дать не могу)

Анонимный комментирует...

ИСПРАВЛЕНИЕ ПРЕДЫДУЩЕГО СООБЩЕНИЯ (не отобразились угловые скобки)

...

struct B: A
равносильно
struct B: public A
_________________________________
Еще слово struct (в отличии от class) нельзя использовать в описании шаблонов
template<class T>class C{}; //ВЕРНО

template<struct T>class C{}; НЕ ВЕРНО
___________________________________
Кроме выше описанный правил различий между class и struct в
С++ нет. (насколько мне известно, ссылки на стандарт дать не могу)

Екклесиаст комментирует...

С правами доступа при наследовании всё очень просто. Мы просто указываем верхнее ограничение прав доступа при наследовании. Если указали private, то значит ничего не может быть более доступно, чем private. Если укажем protected, то ничто не может быть блее доступно, чем protected. Если указали public - то практически не ограничили ничем, как было, так и осталось.
относительно прав по умолчанию, то надо просто запомнить следующее: class у нас всегда должен ассоциировалься с private по умолчанию, а struct должно всегда у нас ассоциироваться с public по умолчанию. Кстати это единственная разница между class и struct (если отбросить все идиалогические моменты).
Жаль, что нет такого модификатора, который бы при наследовании сделал следующие изменения:
public -> public
protected -> private
private -> private
как-то мне это понадобилось...

AnotherAnkor комментирует...

Всё хорошо, но! я никак не могу прочитать то, что подразумевается под словом "доступиться". Это как-то ну уж совсем не по-русски звучит.

Анонимный комментирует...

Алёна спасибо за все Ваши статьи. Слово "доступиться" надо бы заменить :)

Анонимный комментирует...

Вы что, нерусские что ли? Доступиться - значит получить доступ. Тезаурусам меньше надо доверять и больше читать классической литературы.

Анонимный комментирует...

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

Andrii Kravchenko комментирует...

Алена, спасибо за интереснейшую статью!
Комментарии, кстати, тоже весьма информативные.

Разбираю C++, как дополнительный инструмент к основным своим языкам, эта статья дала сразу хорошее базовое понимание нюансов наследования.