Прочитать на английском

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


Семантика типа значения

В Visual C++, по сравнению с управляемыми расширениями для C++ семантика типа значения изменилась.

Ниже приведен традиционный простой тип значения, используемый в спецификациях управляемых расширений для C++:

__value struct V { int i; };
__gc struct R { V vr; };

В управляемых расширениях могут встречаться четыре варианта написания типа значения (где формы 2 и 3 несут одинаковую семантическую нагрузку):

V v = { 0 };       // Form (1)
V *pv = 0;         // Form (2) an implicit form of (3)
V __gc *pvgc = 0;  // Form (3)
__box V* pvbx = 0; // Form (4) must be local 

Вызов унаследованных виртуальных методов

Form (1) является традиционным объектом значения, сравнительно точно изученным, за исключением случаев вызова унаследованного виртуального метода, такого как ToString(). Примеры.

v.ToString(); // error!

Для вызова данного метода, не переопределенного в V, компилятору должен предоставляться доступ к связанной виртуальной таблице основного класса. Поскольку типы значений находятся в процессе хранения состояний, не привязывая указатель к виртуальной таблице (vptr), v должен быть упакован. В конструкции языка с управляемыми расширениями неявная упаковка-преобразование не поддерживается, и всегда должна явно указываться программистом.

__box( v )->ToString(); // Managed Extensions: note the arrow

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

Лексическая сложность явной упаковки-преобразования объекта, но не связанные с самой упаковкой-преобразованием затраты, не заметна в новом синтаксисе:

v.ToString(); // new syntax

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

Критериями, которые определяют необходимость переопределения экземпляров ToString в классе значения является частота и место их использования. Если вызов осуществляется редко, в определении существует некоторые преимущества. Подобным образом, если вызов осуществляется в непродуктивных областях приложения, добавление определения существенно не улучшит эффективность приложения. Если для дескриптора отслеживания сохраняется упакованное значение, то вызовы, осуществленные с помощью дескриптора, не будут нуждаться в упаковке-преобразовании.

Отказ от конструктора класса значения по умолчанию

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

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

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

При этом объявление объекта типа значения в новом синтаксисе остается неизменным. Недостатком процесса является то, что типы значений не подходят для обертывания машинных типов по следующим причинам.

  • В типе значения не поддерживается деструктор. Поэтому невозможно автоматизировать набор действий, активированный к концу времени существования объекта.

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

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

Внутренние указатели

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

__value struct V { int i; };
__gc struct R { V vr; };

V v = { 0 };  // Form (1)
V *pv = 0;  // Form (2)
V __gc *pvgc = 0;  // Form (3)
__box V* pvbx = 0;  // Form (4)

R* r;

pv = &v;            // address a value type on the stack
pv = __nogc new V;  // address a value type on native heap
pv = pvgc;          // we are not sure what this addresses
pv = pvbx;          // address a boxed value type on managed heap
pv = &r->vr;        // an interior pointer to value type within a
                    //    reference type on the managed heap

Таким образом, V* может обращаться к следующим расположениям: в пределах локального блока (и поэтому может быть висячим); в глобальной области, в переделах машинной кучи (например, если объект, к которому он обращается был удален); в куче CLR (и таким образом будет отслежен, если во время сборки мусора осуществлялось его перемещение) и в пределах внутреннего ссылочного объекта в куче CLR (с явным отслеживанием так называемого внутреннего указателя).

В управляемых расширениях нет возможности разделять машинные аспекты V*; таким образом, он рассматривается как включенный элемент и существует вероятность, что он обратиться к объекту или вложенному объекту управляемой кучи.

В новом синтаксисе указатель типа значения представлен в двух типах: V*, который должен обращаться только к расположениям за пределами кучи среды CLR, и внутренний указатель, interior_ptr<V>, который позволяет, но не требует указания адреса в пределах управляемой кучи.

// may not address within managed heap 
V *pv = 0; 

// may or may not address within managed heap
interior_ptr<V> pvgc = nullptr; 

Варианты Form (2) и Form (3) управляемых расширений сопоставляются с interior_ptr<V>. Form (4) является дескриптором отслеживания. Он обращается ко всему объекту, упакованному в управляемой куче. В новом синтаксисе он преобразуется в V^,

V^ pvbx = nullptr; // __box V* pvbx = 0;  

Следующие объявления в управляемых расширениях полностью соответствуют внутренним указателям в новом синтаксисе (они представляют типы значений в пространстве имен System).

Int32 *pi;   // => interior_ptr<Int32> pi;
Boolean *pb; // => interior_ptr<Boolean> pb;
E *pe;       // => interior_ptr<E> pe; // Enumeration

Встроенные типы не считаются управляемыми, несмотря на то, что они выполняют роль псевдонимов для типов в пространстве имен System. Таким образом, следующие сопоставления между управляемыми расширениями и новым синтаксисом являются верными.

int * pi;     // => int* pi;
int __gc * pi2; // => interior_ptr<int> pi2;

Во время перевода V* в существующей программе, наиболее консервативной стратегией всегда являлось превращение его в interior_ptr<V>. Это процесс его обработки в управляемых расширениях. Указывая V* вместо внутреннего указателя в новом синтаксисе, программист имеет возможность ограничить тип значения адресами неуправляемой кучи. Оставить формат V* можно в том случае, если при выполнении программы может выполняться транзитивное закрытие всех ее использований, при этом гарантируя, что в управляемой куче нет присвоенных адресов.

Закрепление указателей

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

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

__gc struct H { int j; };

int main() 
{
   H * h = new H;
   int __pin * k = & h -> j;
  
   // …
};

В новой конструкции языка закрепляющий указатель объявлен с синтаксисом, аналогичным внутреннему указателю.

ref struct H
{
public:
   int j;
};

int main()
{
   H^ h = gcnew H;
   pin_ptr<int> k = &h->j;

   // …
}

Закрепляющий указатель в новом синтаксисе представляет особый случай использования внутреннего указателя. В закрепляющем указателе сохраняются исходные ограничения. Например, указатель не может использоваться в качестве параметра или возвращаемого типа метода; он может объявляться только в локальном объекте. Однако в новом синтаксисе был добавлен ряд дополнительных ограничений.

Значением закрепляющего указателя по умолчанию является nullptr, а не 0. pin_ptr<> не может инициализироваться или получать значение 0. Все назначения значения 0 в существующем коде должны быть изменены на nullptr.

Закрепляющему указателю в управляемых расширениях предоставлено разрешение на обращение ко всему объекту, как показано в следующем примере, взятом из спецификации управляемых расширений:

__gc class G {
public:
   void incr(int* pi) { pi += 1; }
};
__gc struct H { int j; };
void f( G * g ) {
   H __pin * pH = new H;   
   g->incr(& pH -> j);   
};

В новом синтаксисе закрепление всего объекта, возвращенного с помощью выражения new, не поддерживается. Предпочтительнее закрепить адрес внутреннего члена. Например:

ref class G {
public:
   void incr(int* pi) { *pi += 1; }
};
ref struct H { int j; };
void f( G^ g ) {
   H ^ph = gcnew H;
   Console::WriteLine(ph->j);
   pin_ptr<int> pj = &ph->j;
   g->incr(  pj );
   Console::WriteLine(ph->j);
}

См. также

Ссылки

Классы и структуры (расширения компонентов C++)

interior_ptr (C++/CLI)

pin_ptr (C++/CLI)

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

Типы значений и их режимы работы (C++/CLI)