Жанр: Учеба
Язык С++ с комментариями
...Все довольно наглядно, так что объяснения излишни. А для демонстрации работы
оператора присваивания попробуйте
A d('d',0);
d=a+b+c;
В данном случае будет задействовано на одну временную переменную больше:
a.A::A(char,int 1)
b.A::A(char,int 2)
c.A::A(char,int 3)
d.A::A(char,int 0)
operator+(const A& a,const A& b)
_1.A::A(int 3)
operator+(const A& _1,const A& c)
_2.A::A(int 6)
=_2.A::operator=(const A& _2)
_2.A::~A()
_1.A::~A()
d это =_2
d.val=6
=_2.A::~A()
c.A::~A()
b.A::~A()
a.A::~A()
11.9. Вызов функции, стр. 337
Функция, которая вызывается повторно, — это operator()() объекта Add(z).
Использование шаблонов и смысл их параметров может стать для вас совершенно
непонятным, если раз и навсегда не уяснить одну простую вещь: при вызове
функции-шаблона вы передаете объекты, но критически важной для инстанцирования
шаблонов информацией являются типы переданных объектов. Сейчас я проиллюстрирую
данную идею на приведенном в книге примере.
Рассмотрим, например, определение функции-шаблона for_each()
template "class InputIter, class Function"
Function for_each(InputIter first, InputIter last, Function f) {
for ( ; first != last; ++first)
f(*first);
return f;
}
Данное определение я взял непосредственно из SGI STL (предварительно убрав
символы подчеркивания для улучшения читаемости). Если его сравнить с приведенным
в книге, то сразу бросается в глаза исправление типа возвращаемого значения (по
стандарту должен быть аргумент-функция) и отказ от использования потенциально
менее эффективного постинкремента итератора.
Когда мы вызываем for_each() c аргументом Add(z),
for_each(ll.begin(),ll.end(),Add(z));
то Function — это Add, т.е. тип, а не объект Add(z). И по определению
for_each() компилятором будет сгенерирован следующий код:
Add for_each(InputIter first, InputIter last, Add f) {
for ( ; first != last; ++first)
f.operator()(*first);
return f;
}
Т.о. в момент вызова for_each() будет создан временный объект Add(z), который
затем и будет передан в качестве аргумента. После чего, внутри for_each() для
копии этого объекта будет вызываться Add::operator()(complex&). Конечно, тип
InputIter также будет заменен типом соответствующего итератора, но в данный
момент это нас не интересует.
На что же я хочу обратить ваше внимание? Я хочу отметить, что шаблон — это не
макрос в который передается что-то, к чему можно приписать скобки с
соответствующими аргументами. Если бы шаблон был макросом, непосредственно
принимающим переданный объект, то мы бы получили
Add for_each(. . .) {
for (. . .)
Add(z).operator()(*first);
return f;
}
что, в принципе, тоже корректно, только крайне неэффективно: при каждом проходе
цикла создается временный объект, к которому затем применяется операция вызова
функции.
11.12. Класс String, стр. 344
Обратите внимание, что для неконстантного объекта, s.operator[](1) означает
Cref(s,1).
А вот здесь хотелось бы по-подробнее. Почему в одном классе мы можем объявить
const и не const функции-члены? Как осуществляется выбор перегруженной функции?
Рассмотрим следующее объявление:
struct X {
void f(int);
void f(int) const;
};
void h()
{
const X cx;
cx.f(1);
X x;
x.f(2);
}
Ввиду того, что функция-член всегда имеет скрытый параметр this, компилятор
воспринимает данное объявление как
// псевдокод
struct X {
void f( X *const this);
void f(const X *const this);
};
void h()
{
const X cx;
X::f(&cx,1);
X x;
X::f(&x,2);
}
и выбор перегруженной функции осуществляется по обычным правилам. В общем --
никакой мистики.
12.2. Производные классы, стр. 351
Базовый класс иногда называют суперклассом, а производный — подклассом. Однако
подобная терминология вводит в заблуждение людей, которые замечают, что данные в
объекте производного класса являются надмножеством данных базового класса.
Вместе с тем, данная терминология совершенно естественна в
теоретико-множественном смысле. А именно: каждый объект производного класса
является объектом базового класса, а обратное, вообще говоря, неверно. Т.о.
базовый класс шире, поэтому он и суперкласс. Путаница возникает из-за того, что
больше сам класс, а не его объекты, которые ввиду большей общности класса должны
иметь меньше особенностей — членов.
12.2. Производные классы, стр. 361
Стоит помнить, что традиционной и очевидной реализацией вызова виртуальной
функции является просто косвенный вызов функции...
Это, вообще говоря, неверно. При применении множественного наследования "просто
косвенного вызова" оказывается недостаточно. Рассмотрим следующую программу:
#include "iostream.h"
struct B1 {
int b1; // непустая
};
struct B2 {
int b2; // непустая
virtual void vfun() { }
};
struct D : B1, B2 { // множественное наследование от непустых классов
virtual void vfun() {
cout"""D::vfun(): this="""this"""\n";
}
};
int main()
{
D d;
D* dptr=&d;
cout"""dptr\t"""dptr"""\n";
dptr-"vfun();
B2* b2ptr=&d;
cout"""b2ptr\t"""b2ptr"""\n";
b2ptr-"vfun();
}
На своей машине я получил следующие результаты:
dptr 0x283fee8
D::vfun(): this=0x283fee8
b2ptr 0x283feec
D::vfun(): this=0x283fee8
Т.е. при вызове через указатель на производный класс dptr, внутри D::vfun() мы
получим this=0x283fee8. Но несмотря на то, что после преобразования исходного
указателя в указатель на (второй) базовый класс b2ptr, его значение (очевидно)
изменилось, внутри D::vfun() мы все равно видим исходное значение, что полностью
соответствует ожиданиям D::vfun() относительно типа и значения своего this.
Что же все это означает? А означает это то, что если бы вызов виртуальной
функции
struct D : B1, B2 {
virtual void vfun(D *const this) { // псевдокод
// . . .
}
};
через указатель ptr-"vfun() всегда сводился бы к вызову vfun(ptr), то в нашей
программе мы бы получили this==b2ptr==0x283fee8.
Вопрос номер два: как они это делают? Элементарно, Ватсон! (Не переборщил?)
Известно, что виртуальная функция производного класса замещает (override)
соответствующую функцию базового класса, а именно: в соответствующую позицию
vtbl записывается адрес функции производного класса. Только вот в часть vtbl,
относящуюся лично к классу D будет записан адрес собственно D::vfun(), а в
часть, относящуюся к подъобекту B2 будет записан адрес некоторой,
сгенерированной компилятором функции vfun_hack():
// псевдокод
void vfun_hack(B2 *const this) // обратите внимание: указатель на B2
{
return D::vfun(static_cast"D*"(this)); // да, return void;
}
Хотя наверняка оптимизатор исключит вложенный вызов функции с помощью простого
goto D::vfun, однако для простоты восприятия об этом лучше не говорить.
13.2.3. Параметры шаблонов, стр. 382
В частности, строковый литерал не допустим в качестве аргумента шаблона.
Потому что строковый литерал — это объект с внутренней компоновкой (internal
linkage).
13.6.2. Параметризация и наследование, стр. 399
Любопытно, что конструктор шаблона никогда не используется для генерации
копирующего конструктора (так, чтобы при отсутствии явно объявленного
копирующего конструктора, генерировался бы копирующий конструктор по умолчанию).
Прекрасный образчик дословного перевода. Сразу же вспоминается другой, не менее
удачный перевод:
— How do you do? // Как вы это делаете?
— It's all right. // Все время правой.
Так что же имелось ввиду под этой туманной фразой? А имелось ввиду, что если
копирующий конструктор не был явно объявлен, то он будет сгенерирован
компилятором самостоятельно, причем предоставленные пользователем
конструкторы-шаблоны не принимаются во внимание, т.е. по ним конструктор
копирования сгенерирован быть не может.
Далее хочу отметить, что постоянно встречающуюся в переводе фразу "конструктор
шаблона" следует понимать как "конструктор-шаблон".
14.4.1. Использование конструкторов и деструкторов, стр. 419
Итак, там, где годится подобная простая модель выделения ресурсов, автору
конструктора нет необходимости писать явный код обработки исключений.
Если вы решили, что тем самым должна повыситься производительность, ввиду того,
что в теле функции отсутствуют блоки try/catch, то должен вас огорчить — они
будут автоматически сгенерированы компилятором, для корректной обработки
раскрутки стека. Но все-таки, какая версия выделения ресурсов обеспечивает
большую производительность? Давайте протестируем следующий код:
#include "stdio.h"
#include "stdlib.h"
#include "time.h"
long Var, Count;
void ResourceGet();
void ResourceReturn();
void Work();
struct SafeResource {
SafeResource() { ResourceGet(); }
~SafeResource() { ResourceReturn(); }
};
void f1()
{
ResourceGet();
try {
Work();
}
catch (...) {
ResourceReturn();
throw;
}
ResourceReturn();
}
void f2()
{
SafeResource sr;
Work();
}
int main(int argc,char** argv)
{
if (argc"1) Count=atol(argv[1]);
clock_t c1,c2;
{
c1=clock();
for (long i=0; i"Count; i++)
for (long j=0; j"1000000; j++)
f1();
c2=clock();
printf("f1(): %ld mln calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
{
c1=clock();
for (long i=0; i"Count; i++)
for (long j=0; j"1000000; j++)
f2();
c2=clock();
printf("f2(): %ld mln calls per %.1f sec\n",Count,double(c2-c1)/CLK_TCK);
}
}
void ResourceGet() { Var++; }
void ResourceReturn() { Var--; }
void Work() { Var+=2; }
Как выдумаете, какая функция работает быстрее? А вот и нет! В зависимости от
компилятора быстрее работает то f1(), то f2(), а иногда они работают совершенно
одинаково из-за полной идентичности сгенерированного компилятором кода. Все
зависит от используемых принципов обработки исключений и качества оптимизатора.
Как же работают исключения? Мне известны две принципиально разных стратегии:
метод раскрутки стека (РС) и метод естественных возвратов (ЕВ). Рассмотрим их
действие на следующем примере:
void g1(), g2();
void f()
{
g1();
g2();
}
void g1()
{
// 1
throw E();
// 2
}
Для метода ЕВ сгенерированный код будет выглядеть следующим образом:
extern bool InException;
void f()
{
g1();
if (InException) return;
g2();
}
void g1()
{
// 1
CopyAndSave(E());
InException=true;
return;
// 2
}
Т.е. происходит простое отображение исключений на традиционно применяемую в
С/C++ технику проверки кодов возврата. Конечно, для возвращающих значения и
создающих объекты функций картина значительно усложняется.
Метод РС реализуется с помощью разбора содержимого стека, ввиду чего каждая
использующая исключения функция обязана устанавливать специальный стековый
фрейм, по которому можно найти адреса созданных в стеке объектов и корректно их
уничтожить.
Каждый из методов обладает своими достоинствами и недостатками, так, например,
для метода РС пролог/эпилог функций становится довольно "тяжелым", каждая
функция обязана устанавливать стековый фрейм, метод ЕВ здесь существенно
выигрывает в производительности. С другой стороны, если мы должны осуществить
возврат из глубоко рекурсивной функции, не создававшей объектов, то для метода
РС работа заключается в простом сбросе стека, а ЕВ будет вынужден проделать
долгий обратный путь.
В целом, любая стратегия реализации исключений всегда приводит к накладным
расходам и разбуханию сгенерированного кода. Все известные мне реализации при
генерации кода с поддержкой исключений приводили к значительным накладным
расходам. Вместе с тем, д-р Страуструп пишет по этому поводу:
It is possible to drive the exception handling overhead down below 1% run-time
overhead in real implementations - I know because it has been done.
В принципе, вызываемые поддержкой исключений накладные расходы времени
выполнения (в реальных реализациях) могут быть сделаны менее 1%, я могу это
утверждать, т.к. это уже было реализовано.
Вместе с тем, там, где от исключений можно отказаться, не стоит пренебрегать
открывшейся возможностью. Как правило, критические участки кода изолируют в
отдельных файлах и компилируют с отключенной поддержкой исключений, конечно, при
этом вы обязаны гарантировать, что любой код в данном файле не приведет к
генерации исключения, даже посредством невинного new или вызова внешней по
отношению к оптимизированному модулю функции (любые возникающие в ней исключения
не должны ее покинуть).
Резюмируя данный раздел, хочется отметить, что как расширенное использование
шаблонов совершенно изменило внешний вид стандартного С++, исключения
кардинально изменили вид внутренний. С++ начал свою жизнь в UNIX с транслятора
cfront, который просто переводил код С++ в эквивалентный код С, однако после
года работы над cfront 4.0, когда он уже был написан, команда разработчиков
cfront решила от него отказаться, т.к. удовлетворяющий всем требованиям
стандарта механизм обработки исключений оказался совершенно неэффективным при
его реализации непосредственно на С.
14.4.2. auto_ptr, стр. 421
В стандартном заголовочном файле "memory" auto_ptr объявлен следующим образом...
Ввиду того, что после выхода первых (английских) тиражей стандарт претерпел
некоторые изменения в части auto_ptr, концовку данного раздела следует заменить
следующим текстом (он взят из списка авторских исправлений к 4 тиражу).
Для достижения данной семантики владения (также называемой семантикой разрушения
после копирования (destructive copy semantics)), семантика копирования шаблона
auto_ptr радикально отличается от семантики копирования обычных указателей:
когда один auto_ptr копируется или присваивается другому, исходный auto_ptr
очищается (эквивалентно присваиванию 0 указателю). Т.к. копирование auto_ptr
приводит к его изменению, то const auto_ptr не может быть скопирован.
Шаблон auto_ptr определен в "memory" следующим образом:
template"class X" class std::auto_ptr {
// вспомогательный класс
template "class Y" struct auto_ptr_ref { /* ... */ };
X* ptr;
public:
typedef X element_type;
explicit auto_ptr(X* p =0) throw() { ptr=p; }
~auto_ptr() throw() { delete ptr; }
// обратите внимание: конструкторы копирования и операторы
// присваивания имеют неконстантные аргументы
// скопировать, потом a.ptr=0
auto_ptr(auto_ptr& a) throw();
// скопировать, потом a.ptr=0
template"class Y" auto_ptr(auto_ptr"Y"& a) throw();
// скопировать, потом a.ptr=0
auto_ptr& operator=(auto_ptr& a) throw();
// скопировать, потом a.ptr=0
template"class Y" auto_ptr& operator=(auto_ptr"Y"& a) throw();
X& operator*() const throw() { return *ptr; }
X* operator-"() const throw() { return ptr; }
// вернуть указатель
X* get() const throw() { return ptr; }
// передать владение
X* release() throw() { X* t = ptr; ptr=0; return t; }
void reset(X* p =0) throw() { if (p!=ptr) { delete ptr; ptr=p; } }
// скопировать из auto_ptr_ref
auto_ptr(auto_ptr_ref"X") throw();
// скопировать в auto_ptr_ref
template"class Y" operator auto_ptr_ref"Y"() throw();
// разрушающее копирование из auto_ptr
template"class Y" operator auto_ptr"Y"() throw();
};
Назначение auto_ptr_ref — обеспечить семантику уничтожения после копирования,
ввиду чего копирование константного auto_ptr становится невозможным.
Конструктор-шаблон и оператор присваивания-шаблон обеспечивают возможность
неявного пребразования auto_ptr"D" в auto_ptr"B" если D* может быть преобразован
в B*, например:
void g(Circle* pc)
{
auto_ptr"Circle" p2 = pc; // сейчас p2 отвечает за удаление
auto_ptr"Circle" p3 = p2; // сейчас p3 отвечает за удаление,
// а p2 уже нет
p2-"m = 7; // ошибка программиста: p2.get()==0
Shape* ps = p3.get(); // извлечение указателя
auto_ptr"Shape" aps = p3; // передача прав собственности и
// преобразование типа
auto_ptr"Circle" p4 = pc; // ошибка: теперь p4 также отвечает за удаление
}
Эффект от использования нескольких auto_ptr для одного и того же объекта
неопределен; в большинстве случаев объект будет уничтожен дважды, что приведет к
разрушительным результатам.
Следует отметить, что семантика уничтожения после копирования не удовлетворяет
требованиям для элементов стандартных контейнеров или стандартных алгоритмов,
таких как sort(). Например:
// опасно: использование auto_ptr в контейнере
void h(vector"auto_ptr"Shape" "& v)
{
sort(v.begin(),v.end()); // не делайте так: элементы не будут отсортированы
}
Понятно, что auto_ptr не является обычным "умным" указателем, однако он
прекрасно справляется с предоставленной ему ролью — обеспечивать безопасную
относительно исключений работу с автоматическими указателями, и делать это без
существенных накладных расходов.
14.4.4. Исключения и оператор new, стр. 422
При некотором использовании этого синтаксиса выделенная память затем
освобождается, при некотором — нет.
Т.к. приведенные в книге объяснения немного туманны, вот соответствующая часть
стандарта:
5.3.4. New [expr.new]
- 17 - Если инициализация объекта завершается из-за возбуждения исключения и
может быть найдена подходящая функция освобождения памяти, она вызывается для
освобождения выделенной для размещения объекта памяти, а само исключение
передается окружающему контексту. Если подходящая функция освобождения не может
быть однозначно определена, освобождение выделенной памяти не производится (это
удобно, когда функция выделения памяти на самом деле память не выделяет, если же
память была выделена, то это, вероятно, приведет к утечке памяти).
14.6.1. Проверка спецификаций исключений, стр. 431
Спецификация исключений не является частью типа функции, и typedef не может ее
содержать.
Сразу же возникает вопрос: в чем причина этого неудобного ограничения? Д-р
Страуструп пишет по этому поводу следующее:
The reason is the exception spacification is not part of the type; it is a
constraint that is checked on assignment and exforced at run time (rather than
at compile time). Some people would like it to be part of the type, but it
isn't. The reason is to avoid difficulties when updating large systems with
parts from different sources. See "The Design and Evolution of C++" for details.
Спецификация исключений не является частью типа, данное ограничение проверяется
при присваивании указателей и является механизмом времени выполнения (а не
времени компиляции). Некоторым людям хотелось бы, чтобы спецификация исключений
была частью типа, но это не так. Причина в том, что мы хотим избежать
трудностей, возникающих при внесении изменений в большие системы, состоящие из
отдельных частей, полученных из разных источников. Для детального описания
смотрите "Проект и эволюция С++".
С великому сожалению русскоязычного читателя, смотреть оказывается нечего, и не
ясно получим ли мы перевод этой книги вообще.
14.6.3. Отображение исключений, стр. 431
В настоящее время стандарт не поддерживает отображение исключений в
std::bad_exception описанным в данном разделе образом. Вот что об этом пишет д-р
Страуструп:
The standard doesn't support the mapping of exceptions as I describe it in
14.6.3. It specifies mapping to std::bad_exception for exceptions thrown
explicitly within an unexpected() function. This makes std::bad_exception an
ordinary and rather pointless exception. The current wording does not agree with
the intent of the proposer of the mechanism (Dmitry Lenkov of HP) and what he
thought was voted in. I have raised the issue in the standards committee.
Стандарт не поддерживает отображение исключений в том виде, как это было описано
в разделе 14.6.3. Он специфицирует отображение в std::bad_exception только для
исключений, сгенерированных в unexpected(). Это лишает std::bad_exception
первоначального смысла, делая его обычным и отчасти бессмысленным исключением.
Текущая формулировка (стандарта) не совпадает с первоначально предложенной
Дмитрием Ленковым из HP. Комитет стандартов поставлен в известность.
Ну и раз уж столько слов было сказано про формулировку из стандарта, думаю, что
стоит здесь ее привести:
15.5.2 Функция unexpected() [except.unexpected]
- 1 - Если функция со спецификацией исключений возбуждает исключение не
принадлежащее ее спецификации, будет вызвана функция
void unexpected();
сразу же после завершения раскрутки стека (stack unwinding).
- 2 - Функция unexpected() не может вернуть управление, но может (пере)возбудить
исключение. Если она возбуждает новое исключение, которое разрешено нарушенной
до этого спецификацией исключений, то поиск подходящего обработчика будет
продолжен с точки вызова сгенерировавшей неожиданное исключение функции. Если же
она возбудит недозволенное исключение, то: Если спецификация исключений не
содержит класс std::bad_exception (18.6.2.1), то будет вызвана terminate(),
иначе (пере)возбужденное исключение будет заменено на определяемый реализацией
объект типа std::bad_exception и поиск соответствующего обработчика будет
продолжен описанным выше способом.
- 3 - Таким образом, спецификация исключений гарантирует, что могут быть
возбуждены только перечисленные исключения. Если спецификация исключений
содержит класс std::bad_exception, то любое неописанное исключение может быть
заменено на std::bad_exception внутри unexpected().
15.3.2.1. Множественное наследование и управление доступом, стр. 461
... доступ разрешен только в том случае, если он разрешен по каждому из
возможных путей.
Тут, конечно, имеет место досадная опечатка, что, кстати сказать, сразу видно из
приведенного примера. Т.е. читать следует так: ... если он разрешен по
некоторому из возможных путей.
15.6. Свободная память, стр. 478
В принципе, освобождение памяти осуществляется тогда внутри деструктора (который
знает размер).
Именно так. Т.е. если вы объявили деструктор некоторого класса
A::~A()
{
// тело деструктора
}
то компилятором (чаще всего) будет сгенерирован следующий код
// псевдокод
A::~A(A *const this,bool flag)
{
if (this) {
// тело деструктора
if (flag) delete(this,sizeof(A));
}
}
Ввиду чего функция
void f(Employee* ptr)
{
delete ptr;
}
превратится в
// псевдокод
void f(Employee* ptr)
{
Employee::~Employee(ptr,true);
}
и т.к. класс Employee имеет виртуальный деструктор, то это в конечном итоге
приведет к вызову соответствующего метода.
15.3.2.1. Множественное наследование и управление доступом, стр. 480
... допускаются некоторые ослабления по отношению к типу возвращаемого значения.
Следует отметить, что эти "некоторые ослабления" не являются простой
формальностью. Рассмотрим следующий пример:
#include "iostream.h"
struct B1 {
int b1; // непустая
};
struct B2 {
int b2; // непустая
virtual B2* vfun() {
cout"""B2::vfun()\n"; // этого мы не должны увидеть
return this;
}
};
struct D : B1, B2 { // множественное наследование от непустых классов
virtual D* vfun() {
cout"""D::vfun(): this="""this"""\n";
return this;
}
};
int main()
{
D d;
D* dptr=&d;
cout"""dptr\t"""dptr"""\n";
void* ptr1=dptr-"vfun();
cout"""ptr1\t"""ptr1"""\n";
B2* b2ptr=&d;
cout"""b2ptr\t"""b2ptr"""\n";
void* ptr2=b2ptr-"vfun();
cout"""ptr2\t"""ptr2"""\n";
}
Обратите внимание: в данном примере я воспользовался "некоторыми ослаблениями"
для типа возвращаемого значения D::vfun(), и вот к чему это привело:
dptr 0012FF6C
D::vfun(): this=0012FF6C
ptr1 0012FF6C
b2ptr 0012FF70
D::vfun(): this=0012FF6C
ptr2 0012FF70
Т.о. оба раза была вызвана D::vfun(), но возвращаемое ей значение зависит от
способа вызова (ptr1!=ptr2), как это, собственно говоря, и должно быть. Делается
это точно так же, как уже было описано, — через автоматическую генерацию
vfun_hack() и запись ее адреса в соответствующую часть vtbl. Только сама
vfun_hack() "существенно" усложняется:
// псевдокод
B2* vfun_hack(B2 *const this)
{
D* ptr= D::vfun(static_cast"D*"(this));
return static_cast"B2*"(ptr);
}
Коль скоро мы должны обеспечить как пре- так и постобработку, вложенного вызова
функции не избежать.
Подводя итог хочется отметить, что в общем случае вызов виртуальной функции
становится все меньше похож на "просто косвенный вызов функции". К счастью,
виртуальные функции с ковариантным (covariant) типом возврата встречаются
довольно редко.
И раз уж речь зашла о виртуальных функциях с ковариантным типом возврата, стоит
привести соответствующую часть стандарта:
10.3. Виртуальные функции (Virtual functions [class.virtual])
- 5 - Тип возвращаемого значения замещающей функции может быть или идентичен
типу замещаемой функции или быть ковариантным (covariant). Если функция D::f
замещает функцию B::f, типы возвращаемых ими значений будут ковариантными, если
они удовлетворяют следующим условиям:
они оба являются указателями или ссылками на класс (многоуровневые указатели
или ссылки на многоуровневые указатели запрещены)
класс в возвращаемом значении B::f идентичен классу в возвращаемом значении
D::f или он является однозначно определенным открытым прямым или косвенным
базовым классом возвращаемого значения D::f
как указатели так и ссылки имеют идентичные cv-квалификаторы и, при этом,
класс возвращаемого значения D::f имеет те же или меньшие cv-квалификаторы,
что и класс в возвращаемом значении B::f.
Если тип возвращаемого значения D::f отличается от типа возвращаемого значения
B::f, то тип класса в возвращаемом значении D::f должен быть завершен в точке
определения D::f или он должен быть типом D. Когда замещающая функция будет
вызывана (как последняя заместившая функция), тип ее возвращаемого значения
будет (статически) преобразован в тип возвращаемого значения замещаемой функции
(см. expr.call). Например:
class B {};
class D : private B { friend class Derived; };
struct Base {
virtual void vf1();
virtual void vf2();
virtual void vf3();
virtual B* vf4();
virtual B* vf5();
void f();
};
struct No_good : public Base {
D* vf4(); // ошибка: базовый класс B недоступен
};
class A;
struct Derived : public Base {
void vf1(); // виртуальная и замещает Base::vf1()
void vf2(int); // не виртуальная, скрывает Base::vf2()
char vf3(); // ошибка: неправильный тип возвр
...Закладка в соц.сетях