Форум программистов, компьютерный форум CyberForum.ru

Mayers S. vs C++11 standard - C++

Восстановить пароль Регистрация
 
ITcrusader
Эксперт C++
 Аватар для ITcrusader
176 / 162 / 8
Регистрация: 12.02.2013
Сообщений: 410
19.03.2013, 10:20     Mayers S. vs C++11 standard #1
Привет, ребят!

У Майерса читал: для предоставления виртуальной функции реализации по умолчанию, которой нужно пользоваться по явному требованию наследующего и переопределяющего виртуальную функцию программиста, т.е. чтобы не получилось так, что в наследном классе забыл её переопределить и будет срабатывать реализация из базового класса (а так же с целью незагромождения пространства имен класса функцией defaultImplementation, которую можно было бы использовать для реализации этой идеи, например), используется определение чисто виртуальной функции. Короче, вот выжимка из книги:

Кликните здесь для просмотра всего текста
Оказывается, иногда может быть опасно использовать обычные виртуальные функции, которые обеспечивают как интерфейс функции, так и ее реализацию по умолчанию. Для того чтобы понять, почему имеется такая вероятность, рассмотрим иерархию самолетов в компании XYZ Airlines. XYZ располагает самолетами только двух типов: модель A и модель B, и оба летают одинаково. В связи с этим разработчики XYZ проектирует такую иерархию:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 class Airport {}; // представляет аэропорты
    class Airplane {
    public:
    virtual void fly(const Airport& destination);
    ...
    };
    void Airplane::fly(const Airport& destination)
    {
    код по умолчанию, описывающий полет самолета
 
    в заданный пункт назначения – destination
    }
    class ModelA: public Airplane {...};
    class ModelB: public Airplane {...};
Чтобы выразить тот факт, что все самолеты должны поддерживать функцию fly, и для того чтобы засвидетельствовать, что для разных моделей, в принципе, могут потребоваться различные реализации fly, функция Airplane::fly объявлена виртуальной. При этом во избежание написания идентичного кода в классах ModelA и ModelB в качестве стандартного поведения используется тело функции Airplane::fly, которую наследуют как ModelA, так и ModelB.
Это классический пример объектно-ориентированного проектирования. Два класса имеют общее свойство (способ реализации fly), поэтому оно реализуется в базовом классе и наследуется обоими подклассами. Благодаря этому проект явным образом выделяет общие свойства, что позволяет избежать дублирования, благоприятствует проведению будущих модернизаций и упрощает долгосрочную эксплуатацию – иными словами, обеспечивает все, за что так ценится объектно-ориентированная технология. Программисты компании XYZ Airlines могут собой гордиться.
А теперь предположим, что дела XYZ идут в гору, и компания решает приобрести новый самолет модели C. Эта модель отличается от моделей A и B, в частности, тем, что летает по-другому.
Программисты компании XYZ добавляют в иерархию класс ModelC, но в спешке забывают переопределить функцию fly:

C++
1
2
3
 class ModelB: public Airplane {
    ... // функция fly не объявлена
    };
В своем коде потом они пишут что-то вроде этого:

C++
1
2
3
4
Airport PDX(...); // PDX – аэропорт возле моего дома
    Airplane *pa = new ModelC;
    ...
    pa->fly(PDX); // вызывается Airplane::fly!
Назревает катастрофа: делается попытка отправить в полет объект ModelC, как если бы он принадлежал одному из классов ModelA или ModelB. Такой образ действия вряд ли может внушить доверие пассажирам.
Проблема здесь заключается не в том, что Airplane::fly ведет себя определенным образом по умолчанию, а в том, что такое наследование допускает неявное применение этой функции для ModelC. К счастью, легко можно предложить подклассам поведение по умолчанию, но не предоставлять его, если они сами об этом не попросят. Трюк состоит в том, чтобы разделить интерфейс виртуальной функции и ее реализацию по умолчанию. Вот один из способов добиться этого:

C++
1
2
3
4
5
6
7
8
9
10
11
 class Airplane {
    public:
    virtual void fly(const Airport& destination) = 0;
    ...
    protected:
    void defaultFly(const Airport& destination);
    };
    void Airplane::defaultFly(const Airport& destination)
    {
    код по умолчанию, описывающий полет самолета в заданный пункт назначения
    }
Обратите внимание, что функция Airplane::fly преобразовна в чисто виртуальную. Она предоставляет интерфейс для полета. В классе Airplane присутствует и реализация по умолчанию, но теперь она представлена в форме независимой функции defaultFly. Классы, подобные ModelA и ModelB, которые хотят использовать поведение по умолчанию, просто выполняют встроенный вызов defaultFly внутри fly (см. также правило 30 о взаимодействии встраивания и виртуальных функций):

C++
1
2
3
4
5
6
7
8
9
10
11
12
class ModelA: public Airplane {
    public:
    virtual void fly(const Airport& destination)
    { defaultFly(destination};}
    ...
    };
    class ModelB: public Airplane {
    public:
    virtual void fly(const Airport& destination)
    { defaultFly(destination};}
    ...
    };
Теперь для класса ModelC возможность случайно унаследовать некорректную реализацию fly исключена, поскольку чисто виртуальная функция в Airplane вынуждает ModelC создавать свою собственную версию fly.

C++
1
2
3
4
5
6
7
8
9
class ModelC: public Airplane {
    public:
    virtual void fly(const Airport& destination)
    ...
    };
    void ModelC::fly(const Airport& destination)
    {
    код, описывающий полет самолета ModelC в заданный пункт назначения
    }
Эта схема не обеспечивает "защиту от дурака" (программисты все же могут создать себе проблемы копированием/вставкой), но она более надежна, чем исходная. Что же касается функции Airplane::defaultFly, то она объявлена защищенной, поскольку действительно является деталью реализации класса Airplane и производных от него. Пассажиры теперь должны беспокоиться только о том, чтобы улететь, а не о том, как происходит полет.
Важно также то, что Airplane::defaultFly объявлена как невиртуальная функция. Это связано с тем, что никакой подкласс не должен ее переопределять – обстоятельство, которому посвящено правило 36. Если бы defaultFly была виртуальной, перед вами снова встала бы та же самая проблема: что, если некоторые подклассы забудут переопределить defaultFly должным образом?
Иногда высказываются возражения против идеи разделения функций на обеспечивающие интерфейс и реализацию по умолчанию, такие, например, как fly и defaultFly. Прежде всего, отмечают противники этой идеи, это засоряет пространство имен класса близкими названиями функций. Все же они соглашаются с тем, что интерфейс и реализация по умолчанию должны быть разделены. Как разрешить кажущееся противоречие? Для этого используется тот факт, что производные классы должны переопределять чисто виртуальные функции и при необходимости предоставлять свои собственные реализации. Вот как можно было бы использовать возможность определения чисто виртуальных функций в иерархии Airplane:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Airplane {
    public:
    virtual void fly(const Airport& destination) = 0;
    ...
    };
    void Airplane::fly(const Airport& destination) // реализация чисто
    { // виртуальной функции
    код по умолчанию, описывающий полет
    самолета в заданный пункт назначения
    }
    class ModelA: pubic Airplane {
    public:
    virtual void fly(const Airport& destination)
    { Airplane::fly(destination);}
    ...
    };
    class ModelB: pubic Airplane {
    public:
    virtual void fly(const Airport& destination)
    { Airplane::fly(destination);}
    ...
    };
    class ModelC: pubic Airplane {
    public:
    virtual void fly(const Airport& destination);
    ...
    };
    void ModelC::fly(const Airport& destination)
    {
    код, описывающий полет самолета ModelC в заданный пункт назначения
    }
Это практически такой же подход, как и прежде, за исключением того, что тело чисто виртуальной функции Airplane::fly заменяет собой независимую функцию Airplane::defaultFly. По существу, fly разбита на две основные составляющие. Объявление задает интерфейс (который должен быть использован в производных классах), а определение задает поведение по умолчанию (которое может использоваться производным классом, но только по явному требованию). Однако, производя слияние fly и defaultFly, мы теряем возможность задать для этих функций разные уровни доступа: код, который должен быть защищенным (функция defaultFly), становится открытым (потому что теперь он находится внутри fly).


Сегодня нахожу кусочек стандарта:
[ Note: a function declaration cannot provide both a pure-specifier and a definition —end
note ] [ Example:
C++
1
2
3
struct C {
virtual void f() = 0 { }; // ill-formed
};
—end example ]

Майерс пишет примеры, которые соответствуют стандартному переносимому C++, в иных случаях - оговаривает обратное.

Я в недоумении.
После регистрации реклама в сообщениях будет скрыта и будут доступны все возможности форума.
Toshkarik
 Аватар для Toshkarik
1139 / 856 / 50
Регистрация: 03.08.2011
Сообщений: 2,381
Завершенные тесты: 1
19.03.2013, 10:33     Mayers S. vs C++11 standard #2
ITcrusader, этого нельзя делать в определении класса. А так можно:
C++
1
2
3
4
5
6
7
struct C {
virtual void f() = 0;
};
 
void C::f() {
   //some code
}
Хоть в наследниках и придется определять чисто виртуальную функцию, они могут вызвать в каких то частях явно функцию, определенную в родителе.

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct C {
virtual void f() = 0;
};
 
void C::f() {
   std::cout << "Base pure func";
}
 
struct B : C {
   virtual void f() {
      C::f();
      std::cout << " from derived" << std::endl;
   }
};
 
int main() {
   C *a = new B();
   
   a->f();
 
   delete a;
}
ITcrusader
Эксперт C++
 Аватар для ITcrusader
176 / 162 / 8
Регистрация: 12.02.2013
Сообщений: 410
19.03.2013, 10:43  [ТС]     Mayers S. vs C++11 standard #3
Toshkarik, да, сама цель этого приема мне ясна. Просто я полагал, что определение, данное в самом классе отличается от внешнего единственно тем, что функция рекомендуется мной компилятору как inline (только запись неявная). Я полагал, что это единственное отличие (если не брать в расчет хороший тон с разделением кода).

Правильно ли я сейчас понял, что это, по-видимому, не единственная разница? Т.е. в этом случае разделение объявления и определения уже не идет вразрез со стандартом? Если да - не идет, то я только вот не понимаю, а в чем отличие?

P.S. Сама идея и цель использования реализации чисто виртуальной функции мне абсолютно ясна - идея красивая, лучше использования дополнительной, конечно.
Toshkarik
 Аватар для Toshkarik
1139 / 856 / 50
Регистрация: 03.08.2011
Сообщений: 2,381
Завершенные тесты: 1
19.03.2013, 10:53     Mayers S. vs C++11 standard #4
Цитата Сообщение от ITcrusader Посмотреть сообщение
Т.е. в этом случае разделение объявления и определения уже не идет вразрез со стандартом?
Все вполне по стандарту.
http://stackoverflow.com/questions/4...definition-why
ITcrusader
Эксперт C++
 Аватар для ITcrusader
176 / 162 / 8
Регистрация: 12.02.2013
Сообщений: 410
19.03.2013, 14:09  [ТС]     Mayers S. vs C++11 standard #5
Toshkarik, отличная ссылка. Спасибо. Вопрос снят
Yandex
Объявления
19.03.2013, 14:09     Mayers S. vs C++11 standard
Ответ Создать тему
Опции темы

Текущее время: 16:31. Часовой пояс GMT +3.
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2016, vBulletin Solutions, Inc.
Рейтинг@Mail.ru