Поделиться через


Нотация приведения типов и знакомство с safe_cast<>

В Visual C++ 2010 нотация приведения типов отличается от управляемых расширений для C++.

Изменение существующей структуры отличается от создания структуры и является более сложным процессом. Решение стремится к компромиссу между идеальной реструктуризацией и практической возможностью выполнения, принимая во внимание существующие структурные зависимости.

Другим примером является расширение языка. В начале 1990-х гг., когда объектно-ориентированное программирование стало важным принципом, возникла острая необходимость в возможности типобезопасного приведения типов в C++. Образование производных типов является открытым для пользователя преобразованием ссылки или указателя базового класса в ссылку или указатель производного класса. Для образования производного типа необходимо выполнить явное приведение типов. Это объяснятся тем, что фактический тип указателя базового класса является свойством среды выполнения, поэтому компилятор не может проверить его. Другими словами, для приведения типов, как и для вызова виртуальной функции, требуется так называемое динамическое разрешение. В связи с этим возникает два вопроса:

  • Почему образование производных типов является обязательным в объектно-ориентированном программировании? Не достаточно ли использовать методы виртуальных функций? Другими словами, почему нельзя сказать, что необходимость образования производных типов (или любое другое приведение типов) является недоработкой разработчиков?

  • Почему поддержка образования производных типов является проблемой для языка C++? Ведь это не является проблемой в других объектно-ориентированных языках программирования, таких как Smalltalk (или, впоследствии, Java и C#)? Что особенного в языке C++, что осложняет поддержку образования производных типов?

Виртуальная функция представляет зависимый от типа алгоритм, общий для семейства типов. (Здесь не рассматриваются интерфейсы, которые не поддерживаются в ISO-C++, но доступны для программирования в среде CLR и представляют интересную альтернативу разработки. Это семейство представлено в виде иерархии классов, в которой имеется абстрактный базовый класс, объявляющий общий интерфейс (виртуальные функции) и набор конкретных производных классов, представляющих фактические типы семейства в домене приложения.

Иерархия Light в домене приложения CGI, например, будет иметь общие атрибуты color, intensity, position, on, off и т. д. Можно управлять несколькими источниками света с помощью общего интерфейса, не беспокоясь, что какой-либо из них окажется источником точечного освещения, направленного света, ненаправленного света (как, например, свет солнца) или света от светильника со створками. В этом случае образование производных типов от light для использования виртуального интерфейса является необязательным. В производственной среде скорость играет важную роль. Можно создавать производные классы и явно вызывать каждый метод, если встроенная обработка вызовов может быть использована вместо механизма виртуальных функций.

Таким образом, одной из причин образования производных классов в C++ является снижение использования механизма виртуальных функций для значительного повышения производительности во время выполнения. (Обратите внимание, что автоматизация данного процесса повышения производительности является предметом научных исследований. Однако это является более сложной задачей, чем просто явное использование ключевого слова register или inline.)

Вторая причина использования производных классов вытекает из двойственности полиморфизма. Полиморфизм можно рассматривать как разделение на пассивные и динамические пары форм.

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

Присваивание объекта производного класса указателю базового класса, в свою очередь, является пассивной формой полиморфизма, поскольку при этом полиморфизм используется в качестве механизма передачи. Объекты Object используются главным образом в доуниверсальном программировании в среде CLR. При пассивном использовании выбранный для передачи и хранения указатель базового класса, как правило, предоставляет слишком абстрактный интерфейс. Объекты Object, например, предоставляют примерно пять методов, доступных в их интерфейсе. Для любого особого действия необходимо явно образовывать производные классы. Например, если нужно настроить угол падения света или коэффициент градиента освещения, необходимо выполнить явное образование производных типов. Виртуальный интерфейс в рамках семейства подтипов не может являться надмножеством для всех возможных методов его многочисленных дочерних объектов. Поэтому для объектно-ориентированных языков программирования всегда будет необходима функция образования производных типов.

Если функция безопасного образования производных типов так необходима в объектно-ориентированном языке, почему она так долго не использовалась в языке C++? Проблема заключается в том, как сделать информацию доступной во время выполнения для типа указателя. При использовании виртуальной функции информация о времени выполнения представляется компилятором в двух частях:

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

  • Каждая виртуальная функция имеет связанную фиксированную ячейку в таблице, поэтому фактический вызываемый экземпляр представлен адресом, хранимым в таблице. Например, виртуальный деструктор Light может быть связан с ячейкой 0, деструктор Color — с ячейкой 1 и т. д. Это эффективная, хотя и не допускающая изменений стратегия, поскольку она используется во время компиляции и приводит к незначительным издержкам.

Проблема заключается в том, как сделать информацию доступной для указателя без необходимости увеличивать размер указателей в C++, добавляя второй адрес или непосредственно добавляя кодировку типа. Это неприемлемо для программистов (и программ), не использующих объектно-ориентированный метод, составляющих самое большое сообщество пользователей. Другим вариантом является использование специального указателя для полиморфных типов класса, однако это может вызвать дополнительные трудности совместного использования указателей, особенно при выполнении арифметических операций с ними. Также неприемлемым было бы поддерживать во время выполнения таблицу, связывающую каждый указатель с текущим связанным типом, и динамически обновлять ее.

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

Еще одной проблемой при использовании типобезопасного приведения типов является синтаксис. Поскольку образование производных типов относится к приведению типов, первоначально в комитете ISO-C++ рассматривалось предложение использовать простой синтаксис приведения типов, показанный в данном примере:

spot = ( SpotLight* ) plight;

Однако это предложение было отклонено комитетом, поскольку данный синтаксис не позволял пользователю управлять издержками приведения типов. Если динамическое типобезопасное приведение типов будет иметь такой же синтаксис, какой использовался в предыдущей небезопасной, но статической нотации приведения типов, то оно просто станет замещением и пользователь не будет иметь возможности снизить ненужные и, возможно, слишком высокие издержки времени выполнения.

В C++ существует метод подавления функции поддержки компилятора. Например, можно отключить механизм использования виртуальных функций с помощью оператора class scope (Box::rotate(angle)) или вызова виртуального метода с помощью объекта класса (а не указателя или ссылки на этот класс). Это не требуется для языка программирования, оно относится лишь к вопросам реализации, так же как и отключение построения временных компонентов в объявлении формы.

// compilers are free to optimize away the temporary
X x = X::X( 10 );

Предложение было передано на доработку, в то время как было рассмотрено несколько альтернативных нотаций. В одном из предложенных комитету вариантов рассматривалась форма (?type), обозначающая неопределенную (а значит, динамическую) сущность синтаксиса. Это давало бы пользователю возможность переключаться между двумя формами — статической и динамической, — но полностью этот вариант не устраивал никого. Проект пришлось начинать сначала. Третьей (на этот раз успешной) нотацией оказалась ныне привычная dynamic_cast<type>, которая сочетала в себе набор из четырех новых нотаций приведения типов.

В ISO-C++ dynamic_cast возвращает значение 0, если применяется к несоответствующему типу указателя, и вызывает исключение std::bad_cast, если применяется к ссылочному типу. В управляемых расширениях для С++ при применении dynamic_cast к управляемому ссылочному типу (из-за представления указателя) всегда возвращается значение 0. __try_cast<type> разработана как аналог вызову исключений dynamic_cast, за исключением того, что она вызывает функцию System::InvalidCastException при неудачном завершении приведения типов.

public __gc class ItemVerb;
public __gc class ItemVerbCollection {
public:
   ItemVerb *EnsureVerbArray() [] {
      return __try_cast<ItemVerb *[]>
         (verbList->ToArray(__typeof(ItemVerb *)));
   }
};

В новом синтаксисе вместо __try_cast используется оператор safe_cast. Ниже приведен фрагмент того же кода, но написанный с использованием нового синтаксиса:

public ref class ItemVerb;
public ref class ItemVerbCollection {
public:
   array<ItemVerb^>^ EnsureVerbArray() {
      return safe_cast<array<ItemVerb^>^>
         ( verbList->ToArray( ItemVerb::typeid ));
   }
};

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

// internally recast into the 
// equivalent safe_cast expression above
( array<ItemVerb^>^ ) verbList->ToArray( ItemVerb::typeid ); 

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

// ok: cast performed at compile-time. 
// No run-time check for type correctness
static_cast< array<ItemVerb^>^>(verbList->ToArray(ItemVerb::typeid));

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

Существуют также "подводные камни", связанные с производительностью. В машинном программировании от использования старой или новой нотации static_cast производительность не меняется. Однако в новом синтаксисе использование старой нотации обходится значительно дороже, чем использование новой нотации static_cast. Это объясняется тем, что компилятор выполняет внутреннее преобразование старой нотации в проверку времени выполнения, которая вызывает исключение. Более того, он изменяет профиль выполнения кода, поскольку это вызывает исключение, приводящее к сбою в приложении. Эта же ошибка не привела бы к возникновению исключения, если бы использовалась нотация static_cast. Можно утверждать, что это помогает пользователям продукта освоить новую нотацию. Но только в случае сбоя. В противном случае это приведет к тому, что программы, использующие старую нотацию, будут выполняться значительно медленнее без видимых причин, а также к возникновению следующих "ловушек" для программистов:

// pitfall # 1: 
// initialization can remove a temporary class object, 
// assignment cannot
Matrix m;
m = another_matrix;

// pitfall # 2: declaration of class objects far from their use
Matrix m( 2000, 2000 ), n( 2000, 2000 );
if ( ! mumble ) return;

См. также

Ссылки

C-Style Casts with /clr

safe_cast

Основные понятия

Общие изменения в языке