Отправляет email-рассылки с помощью сервиса Sendsay

Программирование на Visual С++

  Все выпуски  

Программирование на Visual С++ - No.75 (Управляемый С++)


Информационный Канал Subscribe.Ru


  ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 75 от 12 мая 2002 г.

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Здравствуйте, дорогие подписчики!


 CТАТЬЯ

Управляемый C++

Автор: Игорь Ткачёв
Источник: RSDN Magazine #0

Не правда ли, название “Управляемый C++” вряд ли можно назвать логичным? Как можно сделать управляемым язык программирования, одно из основных достоинств которого – свободное манипулирование таким ресурсом программы, как адресное пространство? Тем не менее, от такой компании, как Microsoft, можно ожидать чего угодно. Поэтому не очень удивительно, что вместе с новой платформой .NET она предлагает нам и новое расширение языка C++ - Managed Extensions for C++, которое иногда именуют просто Managed C++ или даже MC++. Что касается последнего сокращения, то оставим его на растерзание другим ярым “поклонникам” этой компании.

Итак, “Управляемый C++”. Давайте попробуем вместе прояснить ситуацию и разобраться, насколько он управляем, что под этим понимается и в чём необходимость его появления.

Прежде всего, рассмотрим важные для нас особенности самой .NET Framework.

Попытки создания всеобъемлющих сред разработки и исполнения программ делаются уже давно, возьмём тот же Smalltalk, Lisp или, наиболее яркого представителя последнего времени, Java. Обычно такие среды именуются виртуальными машинами (Virtual Machine, VM), но Microsoft выбрала другое название - Common Language Runtime (CLR), что можно перевести как “одна на всех среда исполнения”. Это и отличает CLR от обычных VM – отсутствие привязки к одному конкретному языку программирования. И хотя иногда, говоря C#, подразумевают .NET и наоборот, C# - это всего лишь один из длинной линейки языков, поддерживаемых CLR. Естественно, в этом ряду не мог не появится и C++.

Среди основных задач подобных сред исполнения программ можно отметить следующие:

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

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

Что касается контроля типов, то вот что об этом говорит создатель C++ Бьерн Страуструп:

“Механизм контроля доступа в C++ обеспечивает защиту от случайности, а не от преднамеренного обмана. Любой язык программирования, который поддерживает прямой доступ к памяти, обязательно оставляет любой элемент данных открытым для сознательного “жульничества”, нарушающего явные правила типов, сформулированные для этого элемента”

Прямая работа с операционной системой и различными API, а также “мирное сосуществование” с другими языками являются одной из основных причин успеха C++.

Таким образом, налицо явные противоречия между концепциями управляемой среды и такого языка программирования, как C++. Как же программистам из Редмонда удалось разрешить подобный конфликт и подружить CLR с “неуправляемым” C++?

К счастью, C++ остался самим собой. Мы по-прежнему можем не инициализировать указатели, выходить за границы массивов, забывать освобождать выделенную память, и никакие CLR не могут нам в этом помешать. С другой стороны, C++ должен уметь работать с объектами CLR и позволять этим объектам работать со своим кодом, т.е. играть по правилам той среды, в которой он используется.

Для решения этих задач разработчики из Microsoft пошли по пути расширения возможностей языка, добавления в него ряда ключевых слов, директив и опций компилятора. Как раз это расширение и получило название Managed Extensions for C++.

Managed Extensions for C++ позволяет C++ программам использовать следующие объекты CLR:

  • Управляемые типы, массивы и указатели
  • Интерфейсы
  • Строки
  • Делегаты и события
  • Свойства
  • Метаданные
Управляемые типы, массивы и указатели

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

Объявление управляемого типа в MC++ производится с помощью ключевых слов __gc или __value.

__gc

Идентификатор __gc применяется для объявления сложных типов, массивов и указателей, размещаемых в куче среды исполнения CLR. Сокращение gc, скорее всего, происходит от garbage collection. Рассмотрим пример:

__gc class Foo {
public:
    ~Foo();
    void Fun();
};

void test()
{
    Foo *p = new Foo;
    p->Fun();
}

Ключевое слово __gc перед объявлением класса говорит компилятору, что наш класс является управляемым и подчиняется всем правилам среды CLR. В частности, нам не нужно вызывать деструктор для удаления объекта из памяти, эту работу за нас сделает CLR. Более того, если мы всё же вызовем оператор delete, то компилятор выдаст сообщение об ошибке, говорящее о том, что в нашем классе не определён деструктор. Определение деструктора может быть добавлено, и вызов оператора delete приведёт к его немедленному вызову, но, тем не менее, память, занятая объектом, освобождена не будет. Если же оператор delete не вызывается, то деструктор будет вызван CLR по своему усмотрению во время сборки мусора. Такое поведение деструкторов управляемых классов обусловлено тем, что компилятор фактически переименовывает их в метод Finalize, являющийся стандартным для среды CLR и вызываемый ей непосредственно перед удалением объекта из памяти. Здесь будет уместно заметить, что все управляемые типы CLR происходят от класса System::Object, который и содержит виртуальный метод Finalize. Компилятор добавляет это наследование автоматически, хотя вполне допустимо делать это явно.

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

void test()
{
    Foo p;   // ошибка
    p.Fun();
}

__gc arrays

Так же, как и для объектов управляемых типов, память для управляемых массивов выделяется в куче CLR. Массивы могут содержать переменное число элементов и всегда инициализируются при создании. Для объявления управляемых массивов используется специальный синтаксис:

int ar __gc[] = new int __gc[10];
ar[0] = 1;
ar = new int __gc[20];

Создать массив объектов Foo можно следующим образом

Foo *ar[] = new Foo*[10];
ar[0] = new Foo;

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

Для возвращения массивов из функций и для объявления многомерных массивов в MC++ также применяется новый причудливый синтаксис:

int test() __gc[]
{
    return new int __gc[10];
}

void test()
{
    int ar __gc[,] = new int __gc[10,20];
    for (int i=0; i<ar->GetLength(0); i++)
        for (int j=0; j<ar->GetLength(1); j++)
            ar[i,j] = i + j;
}

Из последнего примера явно следует, что управляемые массивы не являются привычными для нас массивами C++. Скорее, это объекты со своим интерфейсом, и, к сожалению, мы не можем применять для них обычные в C++ методы работы с массивами. Это легко проверить на следующем примере:

void test()
{
    int  ar __gc[] = new int __gc[10];
    int *p1 = ar;     // ошибка
    int *p2 = &ar[0]; // ошибка
}

Компиляция следующего кода выдаст ошибки в строчках с инициализацией указателей p1 и p2. В принципе, даже если управляемые массивы и являются объектами, то на C++ можно довольно просто сделать перегрузку соответствующих операторов и эмулировать работу с массивами C++. Но разработчики из Microsoft не пошли на этот вполне очевидный шаг, и на то были достаточно веские основания. Дело в том, что управляемая куча – довольно сложная система, и среди прочих возможностей она обеспечивает дефрагментацию памяти после сборки мусора. Это значит, что любой управляемый объект может быть перемещён в памяти в любое время. Уже сам по себе этот факт не позволяет использовать обычные C++ указатели с управляемыми объектами CLR и проясняет многие ограничения управляемой среды.

__pin

Тем не менее, было бы странным не иметь доступа к памяти управляемых объектов. И такая возможность есть. Managed Extensions for C++ включает ещё одно ключевое слово - __pin, позволяющее объявлять pinning pointers на управляемые объекты. Один из переводов этого термина мог бы звучать, как “прикольные указатели”, но боюсь, что это может быть неправильно понято. Поэтому мы будем пользоваться оригиналом или, в крайнем случае, транслитерацией.

Pin-указатели позволяют зафиксировать объект в памяти и сделать его неперемещаемым до тех пор, пока такой указатель существует как объект и его значение не равно нулю. Рассмотрим пример:

void test()
{
    int  ar __gc[] = new int __gc[10];
    int __pin *p = &ar[0];
    memset(p-100,-1,2000); // делаем, что хотим
}

Здесь мы создаём управляемый массив, объявляем pin-указатель и работаем с ним уже “по всем правилам” C++. В частности, этот код заставил надолго задуматься мою тестовую программу, после чего её пришлось закрывать насильственным путём. Это наглядно демонстрирует необходимость аккуратной работы с управляемой памятью (впрочем, и неуправляемой тоже), т.к. даже управляемый C++ код, получив доступ к управляемой памяти таким способом, уже перестаёт быть контролируемым и может легко причинить вред всей системе.

__nogc

Как нетрудно догадаться, это ключевое слово обозначает обычный неуправляемый тип C++. Этот модификатор используется компилятором по умолчанию и приведён здесь лишь для полноты картины.

__value

Согласитесь, размещать абсолютно все переменные в управляемой куче расточительно, особенно если это просто байт или целое, использующиеся в качестве счётчика. Как известно, размещение переменных в стеке является наиболее эффективным для простых переменных с коротким жизненным циклом. Идентификатор __value позволяет объявлять управляемые типы, которые, в отличие от gc-типов, могут размещаться как в управляемой куче, так и в стеке программы. В таблице 1 приведено соответствие между примитивными типами C++ и управляемыми типами CLR.

Таблица 1.

C++ CLR
char Sbyte
signed char Sbyte
short Int16
int Int32
long Int32
__int64 Int64
unsigned char Byte
unsigned short UInt16
unsigned int UInt32
unsigned long UInt32
unsigned __int64 UInt64
float Single
double Double
void Void

С помощью модификатора __value можно объявлять как классы и структуры, так и управляемые перечислимые типы. Более того, это единственный способ объявлять перечисления, которые будет понимать CLR. Например:

__value class  cl { int a; };
__value struct st { int a; };
__value enum   en : int { En1, En2 };

Всё правильно, последняя строчка не содержит ошибки. CLR поддерживает типизированные перечисления, поэтому и в MC++ вполне допустимо задавать для них тип.

Ещё одним важным отличием value-типов является то, что они не происходят от общего для CLR типов класса System::Object. Это затрудняет их использование с CLR-коллекциями и в многочисленных методах, принимающих в качестве параметра System::Object. Для разрешения использования value-типов как gc-классов в .NET используется так называемый boxing.

__box

Ключевое слово __box создаёт обёртку для value-типов, после чего их можно использовать так же, как и gc-классы. Такие языки, как C# и VB.NET, создают обёртки для value-типов автоматически, в MC++ неявное преобразование запрещено из соображений производительности.

using namespace System::Collections;

void test()
{
    Stack *s = new Stack();
    int i = 1;
    s->Push(i);        // ошибка
    s->Push(__box(i)); // ok
}

Все box-value-типы, кроме перечислений, являются производными от System::ValueType, который, в свою очередь является наследником System::Object. Базовый класс для перечислений – System::Enum.

__gc pointers

Если существуют управляемые объекты, то должны существовать и управляемые указатели на такие объекты. Более того, мы уже не раз их использовали в наших примерах. Мы выяснили также, что природа управляемых и обычных C++ объектов различна, то же самое справедливо и для указателей. По аналогии с gc-массивами мы можем смело констатировать, что управляемые указатели являются самостоятельными объектами и имеют лишь внешнее сходство с регулярными указателями C++. В частности, обычный для C++ способ преобразования указателей через void* заменён для gc-типов преобразованием к System::Object*, а для value-типов к System::Void*. Среди ограничений можно отметить то, что к управляемым указателям не может быть применена адресная арифметика (вместо этого следует использовать управляемые массивы) и, как мы уже выяснили, управляемые указатели могут быть преобразованы к обычным C++ указателям только через pinning pointers.

Для преобразования одного типа управляемых указателей к другому можно использовать принятую в C++ семантику оператора dynamic_cast. В дополнение к этому MC++ определяет ещё один оператор __try_cast, основное отличие которого заключается в том, что в случае неуспеха этот оператор возбуждает исключение System::InvalidCastException. Применение операторов static_cast и reinterpret_cast также допустимо, но пользоваться ими стоит только в исключительных случаях, когда вы абсолютно уверены в том, что вы делаете. Оператор const_cast поддерживается без особых ограничений.

Интерфейсы

Необходимость появления интерфейсов в .NET вызвана, в том числе и соображениями совместимости с технологиями COM. И если CLR-объекты могут работать с Win32-кодом посредством импорта DLL, то Win32-программы имеют возможность взаимодействовать с объектами .NET только через механизм COM-интерфейсов. Кстати, эта возможность даёт нам альтернативный способ разработки COM-компонентов, который к тому же является более лёгким и приятным занятием, чем использование MFC или ATL.

Как известно, CLR не поддерживает множественного наследования классов. Возможно, это и правильно. Во-первых, не все языки его реализуют, а, во-вторых, механизм виртуальных базовых классов, обычно использующийся для разрешения конфликтов при множественном наследовании, значительно усложняет структуру таблицы виртуальных методов класса и делает вызовы виртуальных функций крайне неэффективными. С другой стороны, “облегчённый вариант” множественного наследования не порождает таких проблем и широко используется при разработке COM-компонентов вообще и с использованием библиотеки ATL в частности.

В отличие от наследования классов CLR разрешает множественное наследование интерфейсов, но это наследование и сами интерфейсы имеют ряд важных ограничений.

  • все методы интерфейсов являются виртуальными и абстрактными и всегда имеют public-доступ, при этом ключевые слова virtual и public и суффикс =0 указывать не обязательно;
  • методы не могут иметь реализации и должны быть переопределены в классах-наследниках;
  • интерфейсы не могут содержать данных;
  • интерфейсы могут быть наследниками других интерфейсов и не могут происходить от обычных классов.

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

Ко всему прочему MC++ разрешает двум или более наследуемым gc-интерфейсам иметь методы с идентичными именами и параметрами. Для того чтобы избежать неоднозначности при реализации этих методов можно использовать следующий синтаксис:

__gc __interface I1 {
    void f();
};

__gc __interface I2 {
    int f();
};

__gc class Foo: public I1, public I2 {
public:
    void I1::f() {}
    int  I2::f() { return 0; }
};

Кроме того, MC++ поддерживает для интерфейсов реализацию по умолчанию (default implementations):

__gc __interface I {
    void f();
};

__gc struct B {
    virtual void f() {};
};

__gc struct D: B, I {
    // по умолчанию D использует B::f для реализации I::f
};

Строки

Строки в CLR представлены классом System::String и ничем особенным не выделяются среди других объектов, за исключением принятого в MC++ нового префикса для объявления строковых констант – “S”. Этот префикс обозначает управляемую строковую константу, имеющую тип System::String*, и введён для повышения производительности. Следующий код справедлив для всех трёх объявляемых строк.

#using <mscorlib.dll>
using System::String;

void test()
{
    String *s1 =  "123";
    String *s2 = L"456";
    String *s3 = S"789";
}

Доступ к управляемым строкам как к обычным неуправляемым символам может быть осуществлён следующим образом:

#include <string.h>
#include <vcclr.h>
using namespace System::Text;

void test(System::String *s)
{
    // wide characters
    wchar_t __pin *ws = PtrToStringChars(s);
    wcslen(ws);

    // ASCII characters
    char mas __gc[] = Encoding::ASCII->GetBytes(s);
    char __pin *as = &mas[0];
    strlen(as);
}

Делегаты и события

Упрощённо делегаты можно рассматривать как узаконенные указатели на функции. Но всё же они, как и всё в CLR, являются объектами и обладают своей дополнительной функциональностью. Объявление делегатов производится с помощью ключевого слова __delegate. С помощью делегатов могут быть вызваны любые методы управляемых классов, как обычные, так и статические. Это принципиально отличает делегаты от указателей на функции, так как делегат хранит не только указатель на функцию, но и информацию о конкретном объекте, у которого эта функция должна быть вызвана. Единственное условие – прототип метода должен совпадать с типом делегата.

__delegate void DelegateSampl(int);

__gc class Foo {
public:
    void TestDelegate1(int n) {
      System::Console::WriteLine(n+1);
    }
    static void TestDelegate2(int n) {
      System::Console::WriteLine(n+2);
    }
};

void test()
{
    Foo *f = new Foo();
    DelegateSampl *d1 = new DelegateSampl(f,Foo::TestDelegate1);
    d1(1); // вызов TestDelegate1
    d1 += new DelegateSampl(0,Foo::TestDelegate2);
    d1(2); // одновременный вызов TestDelegate1 и TestDelegate2
}

Как следует из примера, один делегат может использоваться для обслуживания нескольких функций, т.е. делегат это не просто указатель, а список указателей. Ещё один момент – для TestDelegate2 мы опустили указание объекта, это допустимо, поскольку этот метод является статическим. Кроме того, с помощью делегатов можно вызывать даже функции Windows API, если они соответствующим образом объявлены:

using namespace System;
using namespace System::Runtime::InteropServices;

__delegate int MsgBox(void*,String*,String*,unsigned);

__gc class Foo {
public:
    [DllImport("user32",CharSet=CharSet::Ansi)]
    static int MessageBox(void*,String*,String*,unsigned);
};

void test()
{
    MsgBox *mb = new MsgBox(0,Foo::MessageBox);
    mb(0,S"2",S"1",0);
}

Наиболее логичным применением делегатов является обработка событий, и, надо отдать должное редмондчанам, в этом вопросе они потрудились на славу. Теперь организовать генерацию событий и их обработку так же просто, как, например, переслать два байта. В дополнение к делегатам в CLR введена модель публикации/подписки на события (events). События объявляются с помощью ключевого слова __event.

__delegate void ClickEvent(int,int);

__gc class EventSource {
public:
    __event ClickEvent *OnClick;
    void FireEvent() {
      OnClick(1,2);
    }
};

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

__delegate void ClickEvent(int,int);

__gc class EventSource {
ClickEvent *OnClick;
public:
    // subscribe to OnClick
    __event void add_OnClick(ClickEvent *ce) {
        OnClick = static_cast<ClickEvent*>(Delegate::Combine(ce,OnClick));
    }
    // unsubscribe to OnClick
    __event void remove_OnClick(ClickEvent *ce) {
        OnClick = static_cast<ClickEvent*>(Delegate::Remove(ce,OnClick));
    }
    void FireEvent() {
        raise_OnClick(1,2);
    }
protected:
    // generate notification
    void raise_OnClick(int x,int y) {
        if (OnClick) OnClick->Invoke(x,y);
    }
    // initialization
    EventSource() {
        OnClick = 0;
    }
};

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

__delegate void ClickEvent(int,int);

__gc class EventSource {
public:
    __event ClickEvent *OnClick;
    void FireEvent() {
        OnClick(1,2);
    }
};

__gc class EventReceiver {
public:
    void ClickHandler(int x,int y) {
        Console::Write(x);
    }
};

void test()
{
    EventSource *es = new EventSource();
    EventReceiver *er = new EventReceiver();

    es->OnClick += new ClickEvent(er,EventReceiver::ClickHandler);
    es->FireEvent();
    es->OnClick -= new ClickEvent(er,EventReceiver::ClickHandler);
    es->FireEvent();
}

Для подписки на события используется оператор “+=”, для её отмены “-=”.

Свойства

Свойства уже давно стали привычной вещью даже в C++, стандарт которого их всё ещё не поддерживает. Производители компиляторов на свой лад расширяют синтаксис языка, добавляя в него поддержку свойств. В таких же языках как Visual Basic и ObjectPascal, которые не слишком связаны стандартами, свойства применяются повсеместно. Технология COM, а точнее интерфейс IDispatch, также поддерживает свойства, которые с успехом используются даже скриптовыми языками. Никуда они не делись и в .NET. Для объявления свойств в MC++ служит ключевое слово __property.

__gc class Foo {
public:
    __property int  get_X() { return 0; }
    __property void set_X(int) {}
};

void test()
{
    Foo *f = new Foo();
    f->X = 1;     // вызов set_X
    int i = f->X; // вызов get_X
}

Фактически в этом примере компилятор создаёт псевдопеременную X. Префиксы get_ и set_ являются обязательными в соответствии с соглашениями об именовании в .NET Developer Platform, но при желании мы можем использовать только один из них. Объявление свойства, которое мы рассмотрели, является скалярным объявлением и подчиняется следующим правилам:

  • метод get не имеет параметров и имеет тип возвращаемого значения T;
  • метод set имеет единственный параметр типа T и возвращаемый тип void;
  • скалярные свойства не могут быть перегружены.

Кроме того, в CLR допустимо объявление индексируемых свойств, для которых справедливо следующее:

  • метод get имеет набор параметров (T1,…,TN) и тип возвращаемого значения TR;
  • метод set имеет набор параметров (T1,…,TN,TR) и возвращаемый тип void;
  • индексированные свойства могут перегружаться как обычные методы.
Метаданные

Метаданные – это одно из фундаментальных понятий, на которых базируется платформа .NET. Обсудить все детали столь обширной темы в статье об MC++ просто нет никакой возможности, поэтому мы будем отталкиваться от следующего упрощения – метаданные представляют собой описания используемых в программе типов и методов в стандартном двоичном формате, хранящиеся в одном модуле вместе с кодом программы (сборке). Отдалённо это напоминает библиотеки типов из COM, но в отличие от них метаданные знают об используемых в вашей программе типах абсолютно всё. Буквально каждый ваш чих незамедлительно регистрируется в базе метаданных, будь то маленькая и скромная вспомогательная private-переменная или большой и важный public-метод. Компилятор генерирует информацию об управляемых типах автоматически, основываясь на их определении, что позволяет создавать самодостаточные в плане описания типы. Благодаря этому совершенно не важно, на каком языке программирования написан класс, от которого вы собираетесь наследоваться, и вас совершенно не должно волновать, из каких языков будет использоваться ваш код.

Вполне естественно, что значительная часть Managed Extensions for C++ отвечает за управление генерацией метаданных.

Импорт метаданных

Программа на MC++ может импортировать метаданные путём включения директивы #using специфицирующей файл, которым может быть:

  • сборка .NET Developer Platform;
  • .NET exe-модуль;
  • obj-файл, скомпилированный с опцией /clr;
  • .netmodule-файл

Следующий пример демонстрирует импорт базовых классов .NET Developer Platform.

#using <mscorlib.dll>
using namespace System;

void test()
{
}

Видимость классов

Ключевое слово public перед объявлением класса или структуры говорит компилятору, что класс будет виден любым программам, использующим для подключения сборки директиву #using. Если же класс помечен ключевым словом private, то он будет виден только внутри сборки. Это значение используется по умолчанию. Например:

__gc public  class Foo1 {};
__gc private class Foo2 {};

Видимость полей и методов класса

Внешняя и внутренняя видимость членов public-классов может быть различной. Это достигается путём применения пары спецификаторов доступа из public, private и protected. Из двух спецификаторов наиболее ограничивающий используется для внешней области видимости. Порядок следования спецификаторов не важен. Например, следующий пример определяет одинаковую (только внутри сборки) область видимости для обоих методов:

public __gc class Foo {
public private:
    void Fun1() {}
private public:
    void Fun2() {}
};

Пользовательские атрибуты

Атрибуты представляют собой универсальное средство расширения метаданных. Любой класс или его элемент может быть помечен атрибутом, информация о котором будет сохранена в метабазе. Практическое применение атрибутов мы уже видели на примере [DllImport]. Этот атрибут говорит управляющей среде, что специфицированную им функцию следует искать в модуле user32.dll. Атрибуты могут использоваться не только самой CLR или компиляторами, доступ к ним возможен из любой программы. Так же мы можем определять и свои собственные атрибуты (Custom Attributes).

Объявление пользовательского атрибута производится следующим образом:

[attribute(AttributeTargets::Class)]
__gc class FooAttr: public Attribute {
public:
    FooAttr(int,float) {}
};

Все пользовательские атрибуты должны быть помечены атрибутом attribute и происходить от класса System::Attribute или его наследников. Забавно, не правда ли? Вот вам ещё одно применение атрибутов – чтобы класс стал атрибутом, нужно его пометить атрибутом attribute. В остальном это просто тип данных, информация о котором так же сохраняется в метабазе. Когда же вы применяете этот атрибут к вашим объявлениям типов, его параметры сохраняются вместе с описанием вашего типа данных. Значение перечисления AttributeTargets позволяет указывать, где синтаксически можно использовать атрибут. Определение этого перечисления выглядит следующим образом:

__value enum AttributeTargets {
    Assembly     = 0x1,
    Module       = 0x2,
    Class        = 0x4,
    Struct       = 0x8,
    Enum         = 0x10,
    Constructor  = 0x20,
    Method       = 0x40,
    Property     = 0x80,
    Field        = 0x100,
    Event        = 0x200,
    Interface    = 0x400,
    Parameter    = 0x800,
    Delegate     = 0x1000,
    ReturnValue  = 0x2000,
    All          = 0x3fff,
    ClassMembers = 0x17fc
};

Применение оператора "ИЛИ" также допускается.

Обработка исключений

Обычный механизм проверки возвращаемого значения для выявления ошибок времени выполнения постепенно уходит в прошлое. В CLR ему не нашлось места совсем. В случае возникновения любой нестандартной ситуации компоненты .NET генерируют исключения, и даже при создании обёрток для COM-объектов возвращаемые значения HRESULT преобразуются в исключения типа System::Runtime::InteropServices::COMException.

Все типы исключений в .NET имеют чёткую иерархию и происходят от базового класса System::Exception. Обычный блок try/catch может быть использован для обработки исключений как обычных типов C++, так и управляемых. Генерация исключений оператором throw тоже ничем особенным не отличается, за исключением того, что при использовании value-типов необходимо использовать boxing.

__value struct V { int v; };

void test()
{
    try {
      V v;
      throw __box(v);
    } catch(__box V *ex) {
    }
}

Когда для генерации исключения используется обычный тип C++, CLR создаёт для него обёртку типа System::Runtime::InteropServices::SEHException. Если ближайший подходящий оператор catch имеет неуправляемый тип, эта обёртка разворачивается, и обработка исключения происходит обычным для C++ образом. Это позволяет одновременно обрабатывать исключения как управляемых типов, так и неуправляемых. Но здесь есть один важный момент, если тип SEHExeption или его базовые типы встретятся первыми, то вы никогда не сможете поймать исключения неуправляемого типа. Из этого также следует, что обработчики исключений

catch(Object*)

и

catch(...)

фактически являются идентичными.

Конструкция __finally, которая введена в компилятор Visual C++ как Microsoft Specific для обработки SEH (Structured Exception Handling) исключений, также поддерживается в полном объёме и имеет ту же семантику.

Управляемые операторы

CLR поддерживает операторы, и в MC++ их объявление допустимо. Но, к сожалению, использовать обычную семантику вызова операторов нельзя из-за принятой в MC++ работы с управляемыми объектами через указатели. Тем не менее, такие языки, как C# и VB.NET, лишены этого недостатка и игнорировать такую возможность не стоит. Нельзя также использовать и ключевое слово operator для объявления операторов в управляемых классах, для этого следует пользоваться предопределёнными в CLR именами. Далее приведено соответствие между операторами и их CLR именами.

Унарные операторы

op_Decrement --
op_Increment ++
op_Negation !
op_UnaryNegation -
op_UnaryPlus +

Бинарные операторы

op_Addition +
op_Assign =
op_BitwiseAnd &
op_BitwiseOr |
op_Division /
op_Equality ==
op_ExclusiveOr ^
op_GreaterThan >
op_GreaterThanOrEqual >=
op_Inequality !=
op_LeftShift <<
op_LessThan <
op_LessThanOrEqual <=
op_LogicalAnd &&
op_LogicalOr ||
op_Modulus %
op_Multiply *
op_RightShift >>
op_Subtraction -

Пример:

__value struct V {
    int i;
    static bool op_Equality(V v, int i) { return v.i == i; }
    static bool op_Equality(int i, V v) { return v.i == i; }
};

Кроме арифметических, логических и битовых операторов, CLR поддерживает два оператора преобразования (Conversion Operators): op_Implicit и op_Explicit. Разница между ними лишь в том, что оператор op_Implicit следует применять, когда преобразование идёт без потери информации, в противном случае следует использоваться op_Explicit:

__value struct MyDouble {
    double d;

    MyDouble(int i) { d = (double) i; }

    static MyDouble op_Implicit(int i) {
        return MyDouble(i);
    }
    static int op_Explicit(MyDouble val) {
        return int(val.d);
    }
};

Опции компилятора и препроцессор
Опция компилятора /clr

Для компиляции программы в управляемый код используется опция /clr. Эта опция создаёт управляемый код для всех функций, но не делает ваши классы управляемыми по умолчанию. Для этого необходимо явно использовать модификаторы __gc и __value.

#pragma unmanaged, #pragma managed

Вполне допустимо использование управляемого и неуправляемого кода в одном модуле. Прагма unmanaged заставляет генерировать компилятор неуправляемый, “родной” для используемой платформы код. Естественно, в таком коде вы не можете использовать управляемые объекты.

#pragma unmanaged

void test()
{
    printf("%d", 1)        // ok
    Console::WriteLine(1); // ошибка
}
#pragma managed

_MANAGED

Этот предопределённый макрос устанавливается компилятором в 1, когда используется опция /clr. Интересно, что прагма unmanaged никак не влияет на его значение, т.е. обе следующие функции будут возвращать 1:

int test1()
{
    return _MANAGED;
}
#pragma unmanaged

int test2()
{
    return _MANAGED;
}
#pragma managed

Опция компилятора /FAs

Эта опция не является новой в MC++, но она интересна прежде всего тем, что теперь компилятор может генерировать не только ассемблерный код, но и MSIL, “ассемблер .NET”. В частности, для последнего примера он сгенерировал MSIL для функции test1 и “родной” ассемблер для test2.

Разное

Мы уже достаточно много выяснили об MC++, но есть ещё несколько моментов, о которых следует упомянуть.

__identifier

Это ключевое слово введено в расширение для того, чтобы мы имели возможность использовать любые другие ключевые слова в качестве идентификаторов. В следующем примере мы используем класс с именем “operator”:

#using "operator.dll"

void test()
{
    __identifier(operator) *p = new __identifier(operator)();
}

__abstract

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

__abstract __gc class Foo {};

__sealed

Это ключевое слово запрещает использовать специфицируемый класс в качестве базового класса.

__sealed __gc class Foo {};

__typeof

Оператор __typeof возвращает объект System::Type, с помощью которого можно получить исчерпывающую информацию о заданном управляемом типе.

Статические конструкторы

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

__gc class Foo {
public:
    static Foo() {}
};

Атрибут [ParamArray]

В CLR допустимо объявление функций с переменным числом параметров, но реализация этой возможности отличается от стандартного для C++ способа. На самом деле список аргументов передаётся как один параметр, являющийся управляемым массивом и помеченный атрибутом System::ParamArray. На MC++ это объявление выглядит следующим образом:

using namespace System;

public __gc class Foo {
public:
    void Fun([ParamArray] String *a[])
    {
    }
};

В C# использование атрибута ParamArray встроено в сам язык, и вместо него используется ключевое слово params:

public class Foo
{
    public void Fun(params string[] a)
    {
    }
}

Вызов нашего метода на C# будет выглядеть следующим образом:

Foo f = new Foo();
f.Fun("1","2","3");

Т.е. фактически компилятор C# преобразует список аргументов в массив и затем передаёт его в функцию. C++ такими способностями не обладает, и нам придётся явно создавать массив, явно его инициализировать и явно передавать в функцию:

void test()
{
    Foo *f = new Foo();
    String *a[] = { S"1", S"2", S"3" };
    f->Fun(a);
}

Смешанный код

В отличие от других CLR-языков MC++ позволяет легко смешивать управляемый и неуправляемый код. Это представляет определённый интерес, и далее мы проведём серию смелых экспериментов для выяснения механизмов их взаимодействия. Для примера возьмём следующий текст:

static int var;
void test2();

#pragma unmanaged
extern "C" void test1()
{
    var = 1;
    test2();
}
#pragma managed

void test2()
{
    var = 1;
    test1();
}

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

bss     SEGMENT
_var    DD  01H DUP (?)
bss     ENDS

EXTRN   ?test1@@$$J0YAXXZ:NEAR ; test1()
_test1  PROC NEAR
    push    ebp                ; {
    mov     ebp, esp
    mov     DWORD PTR _var,1   ; var = 1;
    call    ?test2@@YAXXZ      ; test2();
    pop     ebp                ; }
    ret     0
_test1  ENDP

__mep@?test2@@$$FYAXXZ TOKEN 06000004
?test2@@YAXXZ PROC NEAR
    jmp     DWORD PTR __mep@?test2@@$$FYAXXZ
?test2@@YAXXZ ENDP

?test2@@$$FYAXXZ:              ; test2()
    ldc.i.1 1                  ; var = 1;
    stsfld  _var
    call    ?test1@@$$J0YAXXZ  ; test1();
    ret
 .end ?test2@@$$FYAXXZ

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

С вызовами всё в порядке.

При обсуждении делегатов мы рассмотрели объявление неуправляемой функции с помощью атрибута [DllImport], но теперь у нас могут возникнуть вполне законные сомнения в необходимости его применения и следующий пример это наглядно демонстрирует:

#define IServiceProvider IServiceProviderX
#include <windows.h>
void test()
{
    ::MessageBox(0,"1","2",0);
}

Макрос в начале примера необходим из-за конфликта имён, возникающего при подключении файла windows.h.

Идём дальше. Нам удалось успешно вызвать MessageBox из user32.dll. Зададим теперь управляемому коду более сложную задачу – прямое создание и использование COM-объектов в обход всего того, что написано в документации об интеграции .NET и COM. В качестве примера создадим объект XML DOM Document и вызовем пару его методов:

#define IServiceProvider IServiceProviderX
#import <msxml.dll>

void test()
{
    ::CoInitialize(NULL);
    {
        MSXML::IXMLDOMDocumentPtr xml(__uuidof(MSXML::DOMDocument));
        xml->loadXML("<root>123</root>");
        Console::WriteLine((LPCTSTR)xml->documentElement->text);
    }
    ::CoUninitialize();
}

Как и ожидалось, данный код выводит на консоль строчку "123". Интересно то, что это выглядит так же, как и обычная Win32 программа, к тому же она подчиняется тем же правилам. Например, вызов CoInitialize здесь также необходим, как и для любого приложения, являющегося COM-клиентом. Можно опять усомниться в эффективности вызовов между управляемым и неуправляемым кодом, но никто не мешает нам обрамить весь этот текст прагмами unmanaged/managed и сократить накладные расходы до одного вызова неуправляемой функции. Обрамлять в данном случае стоит и саму директиву #import, так как объявление функции в неуправляемой секции заставляет компилятор генерировать для неё неуправляемый код вне зависимости от места её реализации. Например, в следующем примере мы получим ошибку компиляции (как мы знаем, неуправляемый код не может использовать управляемые объекты), хотя сама функция определена в управляемой секции.

#pragma unmanaged
void test();
#pragma managed

void test()
{
    Console::WriteLine(1); // ошибка
}

Заключение

Теперь пришло время ответить на наш главный вопрос: “Что же такое MC++?”. С одной стороны, вы можете смело использовать в своих программах все привычные возможности C++, шаблоны и множественное наследование, перегрузку операторов и прямую работу с памятью. Вся разница лишь в том, что компилятор будет генерировать управляемый код (MSIL) вместо ассемблера. Но! Это касается только обычных типов C++. Если же у вас возникнет необходимость (а она обязательно возникнет) в использовании gc- и value-типов, то вам не придётся заботиться об удалении объектов, CLR будет сама производить начальную инициализацию переменных и проверять допустимость значений аргументов во время исполнения. Платой за это будет следование всем ограничениям управляемой среды. Таким образом, фактически мы имеем два разных языка в одном, которые можно легко смешивать. Единственная проблема – теперь нам придётся постоянно помнить, с каким из них в данный момент мы имеем дело.

Ещё один вопрос касается терминологии. Что такое “управляемый” и “неуправляемый” код? С неуправляемым всё ясно – это обычный “родной” код Windows/Intel. С управляемыми объектами тоже понятно – CLR может их полностью контролировать. Не понятно только, как быть с обычными C++ программами, которые не используют управляемые объекты, но компилируются в MSIL код.

Пусть они тоже будут… управляемыми, хотя мы-то с вами точно знаем, что это не так :o)


Это все на сегодня. Пока!

Алекс Jenter   jenter@rsdn.ru
Duisburg, 2001.    Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки


RLE Banner Network

http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное