Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Posts Tagged ‘blackmire

Попытки отражения garbage-collected структур на ужасный неуправляемый С++

Если вы вдруг решили сконвертировать C# в C++, то большинство задач конверсии – тривиальны. int меняется на int32_t, string на std::wstring (хотя на самом деле просто string для большинства людей которым unicode не нужен). Ну и так далее. Различия в большинстве случаев несущественны.

Основной задачей, как ни странно, является конверсия reference типов, т.к. в C# можно создавать массивы, дженерики и просто ref-типы и пускать все на самотек дескать “когда-нибудь освободится”, а вот на C++ так делать нельзя. Напомню, что в С++ есть вот такие варианты:

  • Аллоцировать вещи на стеке. К сожалению, за исключением примитивных типов, в C# никто так не делает. Чтобы C#ный объект который кто-то аллоцировал через new аллоцировать в С++ коде на стеке, нужно быть увереным что объект маленький и что он действительно только вот тут, в локальном scope, что его никто не потребляет. Зато стек почистится по мере выхода их скоупа, что как бы неплохо.

  • Аллоцировать вещи вручную через new/delete: это нереально в принципе т.к. если идти по этому пути, то у каждого типа может быть свой набор ссылок на весь мир, ну и… вы поняли. Получится ад. Не вариант.

  • Использовать умные указатели так, как их нужно использовать. То есть например если у вас есть фабрика которая порождает объекты и выдает их наружу, то вы можете возвращать unique_ptr. И так далее. Одна лишь проблема: как понять какой указатель нужен? Опять нужен глубинный анализ, а это сложно.

  • Использовать везде shared_ptr, т.к. это как раз тот указатель, который можно либерально копировать повсюду и при этом не получить ситуацию когда вы пытаетесь вызвать что-то у объекта, который уже был перемещен куда-то.

Инициализация и присваивание

Отдельной пролемой стоят всякие инициализации и присваивания. Например, представим что вы создаете человека, и передаете строку. Принято ведь писать

class Person
{
  string name;
public:
  Person(const string name) : name(name) {}
};

Опаньки! А мы-то думали что все не-примитивные объекты будут shared_ptr. В результате получается, что

  • Все типы которые мы считаем “маленькими” (а это включает всякие int, float и так далее) передаются by value

  • Строки и прочие типы которые нормально себя ведут в нынешней системе типов мы передаем как const ref

  • А вот все остальное – да, проблуем использовать shared_ptr

Полноценный пример

Давайте типичный пример с человеком у которого есть адрес:

struct Address
{
  int HouseNumber;
  string StreetName;
  Address(int houseNumber, const string streetName)
    : HouseNumber(houseNumber), StreetName(streetName) {}
};

Я намеренно использую struct и C#-ное именование а также не использую properties т.к. автосвойство C# можно смело конвертировать в поле C++ without loss of generality. Так вот, у нас есть адрес который берет const string во втором аргументе и это как бы правильно.

Теперь посмотрим на человека, который обладает адресом:

struct Person
{
  int Age;
  string Name;
  shared_ptr<Address> HomeAddress;
  Person(int age, const string name, shared_ptr<Address> homeAddress)
    : Age(age), Name(name), HomeAddress(homeAddress) {}
};

Вроде тоже неплохо. Сразу понятно что подсовывать Address нам не нужно, и что нам нужна “управляемая” версия. Теперь все это можно потреблять:

auto address = make_shared<Address>(221, "Baker St");
auto person = make_shared<Person>(40, "Sherlock", address);
cout << person->Name << endl;

Более того, мы можем не нервничать насчет удаления address если его “хозяин” надоел и его грохнули:

shared_ptr<Address> addr;
{
  auto address = make_shared<Address>(221, "Baker St");
  auto person = make_shared<Person>(40, "Sherlock", address);
  addr = person->HomeAddress;
}
cout << addr->StreetName << endl;

Ну ок, вроде оно как-то работает. Хотя тут мне подумалось… а ведь по идее можно и адрес было передавать как const shared_ptr<Address>, не так ли?

Ладно, что там следующее?

Массивы

Массивы – это вообще больное место. В C# есть два разных типа – прямоугольные [,] и угловатые [][], в С++ существует только 2й вариант. Но проблема даже не в этом. Проблема в том, как вообще представить N-мерный массив чего либо через умный указатель.

Начнем с простого: у человека несколько имен и это как-то нужно хранить. Тут, самое безопасное – это vector<string>. В принципе, со строками мы можем себе позволить такой вот расклад когда мы копируем by value. Но как насчет набора адресов?

Одномерный набор адресов в C# подразумевает конверсию как List<T> так и T[] в vector<shared_ptr<T>> – конечно, для тех типов где shared_ptr нужен, т.е. не для для примитивов или string:

struct Person
{
  int Age;
  vector&lt;string&gt; Names;
  typedef vector&lt;shared_ptr&lt;Address&gt;&gt; AddressCollection;
  AddressCollection Addresses;
  Person(int age, const vector&lt;string&gt; names, const AddressCollection addresses)
    : Age(age),
      Names(names),
      Addresses(addresses)  {  }
};

Ну а тут можно воспользоваться любезно предоставленым typedef’ом и проинициализировать все это счастье в стиле С++11:

auto person = make_shared&lt;Person&gt;(23, 
  vector&lt;string&gt;{&quot;Janes&quot;, &quot;Jimmy&quot;},
  Person::AddressCollection{ 
    make_shared&lt;Address&gt;(123, &quot;Hull Rd&quot;), 
    make_shared&lt;Address&gt;(23, &quot;Sesame St.&quot;)});
for (auto addr  : person-&gt;Addresses)
{
  cout &lt;&lt; addr-&gt;StreetName &lt;&lt; endl;
}

А что же насчет std::array или например boost::shared_array? Ну, std::array на самом деле требует (сразу!) известную длину. То есть если у вас в C# написано например что int i = new int[3] то тогда да, вам повезло:

struct Person
{
  array&lt;shared_ptr&lt;Address&gt;, 2&gt; HomeAndWorkAddresses;
  explicit Person(const array&lt;shared_ptr&lt;Address&gt;, 2&gt; home_and_work_addresses)
    : HomeAndWorkAddresses(home_and_work_addresses)
  {
  }
};

От кода выше в принципе уже должно тошнить, но для тех кто не понял в чем соль, вот то, как выглядит инициализация сего счастья:

array&lt;shared_ptr&lt;Address&gt;, 2&gt; addresses = { {
  make_shared&lt;Address&gt;(123, &quot;London Rd&quot;),
  make_shared&lt;Address&gt;(23, &quot;Shirley Ave&quot;)
} };
auto person = make_shared&lt;Person&gt;(addresses);
cout &lt;&lt; &quot;Work: &quot; &lt;&lt; person-&gt;HomeAndWorkAddresses[1]-&gt;StreetName &lt;&lt; endl;

Нда, мне определенно не нравится std::array. В большинстве случаев, мне кажется, лучше всего использовать vector<T>, vector<vector<T>> и так далее.

Полузаключение

На самом деле, то, что я описал – это верхушка айсберга. Например, допустим что мы допускаем хранения vector<string> но ведь по сути дела для передачи этой коллекции куда либо (а мало ли зачем?) мы не можем передавать ее by value (это расточительно!), и с другой стороны, она не shared_ptr<T>, то есть единственный наш вариант – это передавать ее как const vector<T>. Хмм, а вообще это не такая уж и плохая идея.

Вообщем, в этом посте я для себя вывел набор (вполне вероятно ошибочных) правил по тому, как конвертировать reference-конструкты C# в нечно С++ное чем можно хоть как-то управлять. Возможно в последствии построить поверх всего этого какие-то дополнительные оптимизации. Посмотрим…

Written by Dmitri

30 января 2015 at 1:20

Опубликовано в Programming

Tagged with , ,