, C #, ASP, статті

Чи не правда, назва “Керований 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 дозволяє множинне спадкування інтерфейсів, але це спадкування і самі інтерфейси мають ряд важливих обмежень.

Мета цих обмежень – уникнути класичних конфліктів при множині успадкування, включаючи неоднозначність доступу до даних успадкованих об’єктів, і звести таблицю віртуальних методів до простого лінійного масиву покажчиків на віртуальні функції, реалізація яких буде здійснена в успадкованих класах.

До всього іншого 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, але при бажанні ми можемо використовувати тільки один з них. Оголошення властивості, яке ми розглянули, є скалярним оголошенням і підпорядковується наступним правилам:

Крім того, в CLR допустимо оголошення індексованих властивостей, для яких справедливо наступне:

Метадані

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

Цілком природно, що значна частина Managed Extensions for C + + відповідає за управління генерацією метаданих.

Імпорт метаданих

Програма на MC + + може імпортувати метадані шляхом включення директиви
#using
специфікує файл, яким може бути:

Наступний приклад демонструє імпорт базових класів. 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)

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*