Так сложилось, что по мере рабочей необходимости мне приходится интенсивно использовать C++/CLI и, соответственно, довольно часто объяснять новичкам, что это, как работает, как использовать и, зачем вообще надо. Так что со временем появилось желание написать статью с обзором языка, ответами на некоторые распространенные вопросы и показать места, где могут удачно лечь грабли.
Когда Microsoft создавала платформу .Net, то она решила дать программистам писать под нее на нескольких уже существующих языках, в синтаксис которых были внесены некоторые изменения — VB, C++. Речь пойдет именно о последнем. Точнее, если мне не изменяет память, в первой редакции язык назывался C++ with managed extensions. Название как бы само намекает на сущность — вот мы вам дали плюсы с расширениями и теперь вы сможете разрабатывать под .Net на уже известных C++, при этом оставив всю мощь исходного языка. Собственно, первая версия расширения синтаксиса была ужасна чуть более чем полностью, и делала код похожим на попытку закодировать и отправить в космос диалог Жириновского и Черномырдина:
К тому же, в этом синтаксисе не было отличий между указателем на нативный тип и на управляемый (в обоих случаях использовалась «*»), не было ключевого слова для обозначения нулевого указателя и прочее. Это и толкнуло Microsoft на создание новой ревизии языка, о которой и пойдет речь в данной статье.
Замечание: эти две версии синтаксиса называются, как ни странно, «old syntax» и «new syntax», и какую именно использовать можно выбирать в настройках компиляции проекта. Впрочем, при создании новых сборок лучше использовать новый синтаксис, так как старый помечен как устаревший и просто плох.
1) С помощью данного языка сферический программист в вакууме сможет разрабатывать полноценное .Net-приложение на любимых плюсах. Честно говоря, мне сложно представить этого извращенца, да и Microsoft такому подходу явно не способствует хотя бы тем, что не делает под C++ дизайнера визуальных компонент. Собственно, правильно делает, ибо для таких целей есть более выразительные языки, тот же C#. Так что это возможность скорее теоретическая.
2) Можно вызывать уже написанный на плюсах код. Действительно, поскольку у нас остались все возможности обычного C++, то можно создавать управляемые обертки для существующих классов на нативных плюсах. Это дает намного большие возможности по вызову неуправляемого кода, нежели PInvoke, который с классами работать не умеет.
3) Можно писать на C++ CLI модули, где производительность критична. Действительно, среди всего зоопарка языков под .Net С++ уникален тем, что на нем можно писать код, который напрямую использует выделение и освобождение памяти, работу с указателями. Например, так:
Все очень просто. При компиляции кода на С++/СLI получается сборка, содержащая как MSIL код, так и машинные команды, в которые превратились строки, написанные на «чистых» плюсах. «Но как же быть с кроссплатформеностью?» — спросите вы, и будете совершенно правы. Никак. В частности, это означает, что не получится собрать один и тот же бинарник для 32 и 64 битных версий (собрать все под «Any CPU»). Такова расплата за использование всех возможностей С++. Естественно, это относится к тому варианта, когда используется микс из управляемого и неуправляемого кода. Всего есть несколько вариантов компиляции:
• /clr — поддержка управляемого и нативного кода с использованием нового синтаксиса.
• /сlr:pure — нативный код не поддерживается. Однако при этом можно использовать небезопасный ) код, в той мере, как это можно делать, к примеру, в С#-сборках при использовании директивы unsafe.
• /clr:safe — Только управляемый безопасный код. Аналог — компиляция C#-сборки без использования unsafe.
• /clr:oldSyntax — аналог /clr, только используется старый синтаксис.
Вот примеры сравнения основных конструкций для С# и C++/CLI.
C#:
C++/CLI:
C#:
C++/CLI:
C#:
C++/CLI:
C#:
C++/CLI:
C#:
C++/CLI:
В C++/CLI для обозначения ссылок на управляемые объекты используется «^» вместо «*». Это очень удобно чтобы различать объекты, которые потом надо удалить и те, которые не надо. Также при создании локального ссылочного объекта можно использовать семантику стека:
Object obj();
Это имеет смысл либо при использовании объектов, реализующих IDisposable (речь об этом пойдет позже) либо для value-типов. Заметим, что в плане хранения и использования value-типов С++/CLI дает большую свободу, чем C#, поскольку программист может сам выбирать — использовать ссылку или значение. Таким образом вполне можно в некоторых ситуация сэкономить на количестве boxing/unboxing операций.
C#:
C++/CLI:
Неуправляемые массивы при этом создаются как обычно.
C#:
C++/CLI:
Как видно из этого примера, если «^» — это аналог «*» из обычного C++, то «%» — это аналог «&». Причем аналогия весьма точная и прослеживается не только при передаче параметров, но и при получении ссылки например:
C#:
C++/CLI
С#:
C++/CLI:
В C++/CLI часть шаблона Disposable за нас реализует компилятор, и это с виду отличается от типичной реализация интерфейса. Более того, все тот же компилятор не позволит реализовать IDisposable напрямую. Впрочем, когда к этому привыкаешь, то понимаешь, что такое поведение довольно логично — избавляет от необходимости писать кучу повторяющегося кода. Так же в своем стремлении сделать освобождение ресурсов похожим на обычный С++ создатели языка пошли еще дальше и явный вызов Dispose можно сделать двумя способами:
и
Так же можно использовать семантику стека для гарантированной очистки ресурсов:
C++/CLI:
C#:
Понятно, что в одну статью все поместить не удалось. Не рассмотренными остались такие вопросы как:
• Синтаксис делегатов, свойств, методов расширения, foreach и прочее
• Жонглирование из управляемого в неуправляемый и обратно объектами, массивами и прочим
• Что поддерживается и нет из того, что есть в С# и в обычном C++
• Особености компиляции приложений со сборками С++/CLI
• Вопросы производительности. Когда и в чем можно получить выигрыш? Где можно внезапно потерять?
1. Сравнение синтаксиса С# и C++: www.cyberforum.ru/cpp-cli/thread83662.html
2. Основные специфичные конструкции языка: msdn.microsoft.com/en-us/library/xey702bw.aspx
3. Миграция на C++/CLI: msdn.microsoft.com/en-us/library/ms235289.aspx
Среди всех остальных языков под .Net C++/CLI является довольно специфичным. Вряд ли на нем имеет смысл разрабатывать стандартное .Net приложение, зато в качестве коммуникации с существующим C++ кодом или для задач оптимизации – самое оно.
Что это?
Когда Microsoft создавала платформу .Net, то она решила дать программистам писать под нее на нескольких уже существующих языках, в синтаксис которых были внесены некоторые изменения — VB, C++. Речь пойдет именно о последнем. Точнее, если мне не изменяет память, в первой редакции язык назывался C++ with managed extensions. Название как бы само намекает на сущность — вот мы вам дали плюсы с расширениями и теперь вы сможете разрабатывать под .Net на уже известных C++, при этом оставив всю мощь исходного языка. Собственно, первая версия расширения синтаксиса была ужасна чуть более чем полностью, и делала код похожим на попытку закодировать и отправить в космос диалог Жириновского и Черномырдина:
//объявляем управляемый класс
public __gc class Class1
{
public:
// метод, принимающий int по ссылке и управляемый массив
int Method1(__gc int &refValue, __gc int[] managedArr);
};
* This source code was highlighted with Source Code Highlighter.
К тому же, в этом синтаксисе не было отличий между указателем на нативный тип и на управляемый (в обоих случаях использовалась «*»), не было ключевого слова для обозначения нулевого указателя и прочее. Это и толкнуло Microsoft на создание новой ревизии языка, о которой и пойдет речь в данной статье.
Замечание: эти две версии синтаксиса называются, как ни странно, «old syntax» и «new syntax», и какую именно использовать можно выбирать в настройках компиляции проекта. Впрочем, при создании новых сборок лучше использовать новый синтаксис, так как старый помечен как устаревший и просто плох.
Зачем нужно?
1) С помощью данного языка сферический программист в вакууме сможет разрабатывать полноценное .Net-приложение на любимых плюсах. Честно говоря, мне сложно представить этого извращенца, да и Microsoft такому подходу явно не способствует хотя бы тем, что не делает под C++ дизайнера визуальных компонент. Собственно, правильно делает, ибо для таких целей есть более выразительные языки, тот же C#. Так что это возможность скорее теоретическая.
2) Можно вызывать уже написанный на плюсах код. Действительно, поскольку у нас остались все возможности обычного C++, то можно создавать управляемые обертки для существующих классов на нативных плюсах. Это дает намного большие возможности по вызову неуправляемого кода, нежели PInvoke, который с классами работать не умеет.
3) Можно писать на C++ CLI модули, где производительность критична. Действительно, среди всего зоопарка языков под .Net С++ уникален тем, что на нем можно писать код, который напрямую использует выделение и освобождение памяти, работу с указателями. Например, так:
//управляемый класс, который может потом использоваться любой .Net–сборкой
public ref Class1
{
public:
//этот метод так же можно использовать во всех .Net-сборках
void Method1();
{
BYTE *buff = new BYTE[100];
//do smth
delete[] buff;
}
};
* This source code was highlighted with Source Code Highlighter.
Как работает?
Все очень просто. При компиляции кода на С++/СLI получается сборка, содержащая как MSIL код, так и машинные команды, в которые превратились строки, написанные на «чистых» плюсах. «Но как же быть с кроссплатформеностью?» — спросите вы, и будете совершенно правы. Никак. В частности, это означает, что не получится собрать один и тот же бинарник для 32 и 64 битных версий (собрать все под «Any CPU»). Такова расплата за использование всех возможностей С++. Естественно, это относится к тому варианта, когда используется микс из управляемого и неуправляемого кода. Всего есть несколько вариантов компиляции:
• /clr — поддержка управляемого и нативного кода с использованием нового синтаксиса.
• /сlr:pure — нативный код не поддерживается. Однако при этом можно использовать небезопасный ) код, в той мере, как это можно делать, к примеру, в С#-сборках при использовании директивы unsafe.
• /clr:safe — Только управляемый безопасный код. Аналог — компиляция C#-сборки без использования unsafe.
• /clr:oldSyntax — аналог /clr, только используется старый синтаксис.
Как выглядит?
Вот примеры сравнения основных конструкций для С# и C++/CLI.
Объявление класса
C#:
public sealed class Class1 : Class2
* This source code was highlighted with Source Code Highlighter.
C++/CLI:
public ref class Class1 sealed : Class2
* This source code was highlighted with Source Code Highlighter.
Объявление структуры
C#:
public struct Class1 : IEquatable
* This source code was highlighted with Source Code Highlighter.
C++/CLI:
public value class Class1 : IEquatable
* This source code was highlighted with Source Code Highlighter.
Объявление интерфейса
C#:
public interface ISomeInterface
* This source code was highlighted with Source Code Highlighter.
C++/CLI:
public interface class ISomeInterface
* This source code was highlighted with Source Code Highlighter.
Объявление перечисления
C#:
public enum Enum1
{
Val1,
Val2
}
* This source code was highlighted with Source Code Highlighter.
C++/CLI:
public enum class Enum1
{
Val1,
Val2
}
* This source code was highlighted with Source Code Highlighter.
Создание управляемого объекта
C#:
object obj = new object();
* This source code was highlighted with Source Code Highlighter.
C++/CLI:
Object ^obj = gcnew Object();
* This source code was highlighted with Source Code Highlighter.
В C++/CLI для обозначения ссылок на управляемые объекты используется «^» вместо «*». Это очень удобно чтобы различать объекты, которые потом надо удалить и те, которые не надо. Также при создании локального ссылочного объекта можно использовать семантику стека:
Object obj();
Это имеет смысл либо при использовании объектов, реализующих IDisposable (речь об этом пойдет позже) либо для value-типов. Заметим, что в плане хранения и использования value-типов С++/CLI дает большую свободу, чем C#, поскольку программист может сам выбирать — использовать ссылку или значение. Таким образом вполне можно в некоторых ситуация сэкономить на количестве boxing/unboxing операций.
Создание управляемого массива
C#:
object[] arr = new object[100];
* This source code was highlighted with Source Code Highlighter.
C++/CLI:
array<Object ^> ^arr = gcnew array<Object ^>();
* This source code was highlighted with Source Code Highlighter.
Неуправляемые массивы при этом создаются как обычно.
Передача параметров в метод
C#:
void Method(int byValue, ref int byRef, out int outValue);
* This source code was highlighted with Source Code Highlighter.
C++/CLI:
void Method(int byValue, int %byRef, [out] int %outValue);
* This source code was highlighted with Source Code Highlighter.
Как видно из этого примера, если «^» — это аналог «*» из обычного C++, то «%» — это аналог «&». Причем аналогия весьма точная и прослеживается не только при передаче параметров, но и при получении ссылки например:
void Method(ValueType val)
{
ValueType ^ref = %val;
}
* This source code was highlighted with Source Code Highlighter.
Переопределение метода
C#:
override void Method();
* This source code was highlighted with Source Code Highlighter.
C++/CLI
virtual void Method() override;
* This source code was highlighted with Source Code Highlighter.
Реализация шаблона IDisposable
С#:
class Class1 : Disposable
{
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
//release managed resources
}
//release unmanaged resources
}
~Class1()
{
this.Dispose(false);
}
}
* This source code was highlighted with Source Code Highlighter.
C++/CLI:
ref class Class1
{
public:
//Эквивалент Dispose
~Class1()
{
//release managed resources
//call finalizer
this->!Class1();
}
//Аналог финализатора
!Class1()
{
//release unmanaged resources
}
}
* This source code was highlighted with Source Code Highlighter.
В C++/CLI часть шаблона Disposable за нас реализует компилятор, и это с виду отличается от типичной реализация интерфейса. Более того, все тот же компилятор не позволит реализовать IDisposable напрямую. Впрочем, когда к этому привыкаешь, то понимаешь, что такое поведение довольно логично — избавляет от необходимости писать кучу повторяющегося кода. Так же в своем стремлении сделать освобождение ресурсов похожим на обычный С++ создатели языка пошли еще дальше и явный вызов Dispose можно сделать двумя способами:
obj->~Class1();
* This source code was highlighted with Source Code Highlighter.
и
delete obj;
* This source code was highlighted with Source Code Highlighter.
Так же можно использовать семантику стека для гарантированной очистки ресурсов:
C++/CLI:
{
A();
Class1 obj;
B();
}
* This source code was highlighted with Source Code Highlighter.
соответствуетC#:
{
A();
using (Class1 obj = new Class1())
{
B();
}
}
* This source code was highlighted with Source Code Highlighter.
Что осталось за кадром?
Понятно, что в одну статью все поместить не удалось. Не рассмотренными остались такие вопросы как:
• Синтаксис делегатов, свойств, методов расширения, foreach и прочее
• Жонглирование из управляемого в неуправляемый и обратно объектами, массивами и прочим
• Что поддерживается и нет из того, что есть в С# и в обычном C++
• Особености компиляции приложений со сборками С++/CLI
• Вопросы производительности. Когда и в чем можно получить выигрыш? Где можно внезапно потерять?
Что почитать?
1. Сравнение синтаксиса С# и C++: www.cyberforum.ru/cpp-cli/thread83662.html
2. Основные специфичные конструкции языка: msdn.microsoft.com/en-us/library/xey702bw.aspx
3. Миграция на C++/CLI: msdn.microsoft.com/en-us/library/ms235289.aspx
Заключение
Среди всех остальных языков под .Net C++/CLI является довольно специфичным. Вряд ли на нем имеет смысл разрабатывать стандартное .Net приложение, зато в качестве коммуникации с существующим C++ кодом или для задач оптимизации – самое оно.