C++ для Java и C# разработчиков | DevsDay.ru

IT-блоги C++ для Java и C# разработчиков


Думаю мы все можем согласиться с идеей о том, что С++ разработчику достаточть легко в последствии освоить C#, Java или еще какой-то более современный язык. Но вот обратное — не совсем так. Многие разработчики начинают плеваться как только слышат про «ручное управление памятью» и подобные вещи. Суть этого поста — попытаться донести для разработчиков во-первых зачем им нужен С++, ну и показать как можно на нем делать те же вещи, к которым они возможно привыкли в других языка. А также объективно рассказать про те проблемы, которые в С++ до сих пор не решены.

Зачем нужен С++

Сегодня существуют три основных индустрии, которым нужен С++. Это

  • Игровая индустрия

  • Embedded development

  • Quant finance

В играх нужна производительность, поэтому С++ помогает ее достичь будучи ближе к железу чем языки основанные на виртуальных машинах. В С++ можно работать с памятью напрямую и даже вставлять в код куски Assembler’а.

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

У нас в quant finance, С++ остался по историческим причинам. Финансовая индустрия весьма консервативна, а современные веяния высокочастотной торговли требуют максимум производительности. На уровне рассчетов моделей есть популярная библиотека QuantLib, но большинство алгоритмов пишут in-house, хотя тоже часто на С++.

Помимо производительности, есть еще несколько причин писать на С++. Одна из них это портативность (внезапно, да?) — на С++ вполне можно писать полностью портативный код. Я вот например использую Intel C++ Compiler (по фичам он «не очень»), который существует как на Windows так и на Linux.

Еще одна причина использовать С++ заключается в том, что это единственный способ амортизировать мощные аппаратные платформы, такие как CUDA и Intel Xeon Phi. Но это в основном для тех кто любит гоняться за пиковой производительностью.

Целочисленные типы

Когда я пишу в C# что-то вроде int x; я четко знаю две вещи:

  • Переменная x – это 32-битное целое число со знаком

  • Изначально, x содержит значение 0 (ноль)

В С++, мы не можем быть уверены ни в одном из этих утверждений. Тип int не определен с той строгостью какой бы хотелось (подразумевалось, что на других платформах — например embedded — у него может быть другой размер), а его дефолтное значение в большинстве случаев не определено, и там может быть что угодно.

Для решения первой проблемы, можно использовать С++ заголовок <cstdint>, который определяет такие типы как uint8_t, int32_t и так далее. Это существенно улучшает портативность кода.

Вторая проблема толком не решена, поэтому вам придется писать int32_t x{0};. К счастью, в С++ с последнее время наконец-то можно инициализировать поля класса прямо в теле, вот так:

class Foo
{
  int32_t bar{0};
  uint32_t baz = 42;
};

Статические поля так инициализировать, к сожалению, все ещё нельзя.

Строки

Со строками в С++ беда. Во-первых, изначально в С++, как и в С, строка была просто указателем на массив байтов (да-да, байтов а не code point’ов) с нулевым байтом \0 на конце. Это значит что вычисление длины строки — это O(n) операция.

Далее в C++ появился тип string, но с ним есть масса проблем. Например, у этого типа есть функции size() и length(), которые делают одно и тоже, но из API это совсем не очевидно. Но еще обиднее что там нет таких очевидных вещей как to_lower/upper() или split(), в результате чего приходится пользоватся сторонними библиотеками вроде Boost.

С++ весьма амбивалентентен в плане поддержки Unicode. Под Windows, кодировка строки типа string — ANSI, а вовсе не UTF-8. Есть также тип wstring, который использует UTF16 (ту же кодировку, что C# и Java).

Массивы

Изначально, массивы достались С++ из С, и это означает что массив — это просто указатель на первый элемент, и всё. То есть массив сам не знает какой он длины, и чтобы написать функцию которая обрабатывает массив, нужно 2 аргумента: указатель и длина.

Сейчас у нас есть такие типы как array и vector. Эти типы позволяют хранить либо массивы фиксированной длины (например array<int,3>) или, в случае с vector, иметь динамический массив, который расширяется и сужается по мере добавления в него элементов.

Многомерные массивы в С++ (как и в С) тоже выглядят плохо, т.к. являются всего лишь массивами указателей. Для них тоже стоит использовать array/vector, а еще лучше, если требуются вычисления, использовать специализированную библиотеку вроде Eigen.

Выделение памяти

Адресное пространство поделено на условно 2 области — стэк и куча. Стэк предназначен для временных данных, и имеет ограничения по размеру, в то время как куча — это ваша оперативная память, занимайте хоть всю.

В связи с этим, в отличии от языков со «сборкой мусора», в С++ есть два способа выделения (аллокации) памяти: на стэке и в куче.

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

int i;
Person person;

Другое дело — выделение в куче. Она для более долгосрочного хранения данных, и ответственность за очистку этой памяти лежит на вас. Если забудете, будет «утечка памяти», что очень плохо.

int* i = new int;
Person* p = new Person;
// а потом не забываем удалить
delete i;
delete p;

Как видите, для выделения в куче, мы используем оператор new, и он нам возвращает указатель на выделенную память. Чтобы освободить эту память, нужно использовать оператор delete.

Значения, указатели и ссылки

Давайте начнем с опеределений. Вот есть у вас переменная int32_t x{42}; — под нее выделено 4 байта, и каждый раз когда вы «берете» x, вы получаете значение 42 (ну, пока вы его не поменяли). Запись x = 11; запишет в эту память значение 11.

У нас также есть такая штука как указатель — это переменная, которая содержит адрес другой переменной. То есть можно написать вот так:

int32_t x{42};
int32_t* y = &x;

Тип этой переменной — со звездочкой, и звездочка означает что это указатель. Ее значение — это адрес переменной x, а чтобы взять адрес чего-либо в С++ нужно посдавить амперсанд, вот так: &x.

Хитрость тут в том, что через адрес переменной можно манипулировать самой переменной. То есть вместо x = 42 можно написать *y = 42. Заметьте что тут звездочка стоит перед именем переменной, и означает «следовать за». То есть мы идем туда, куда указывает y и в том месте памяти меняем значение.

Наконец, третий способ доступа к переменной — это ссылка. Ссылка действует так же как и указатель, только синтаксис другой. Вот смотрите:

int32_t x{42};
int32_t& z = x;
z = 11;

В отличии от указателей, тип ссылки заканчивается на &, и ссылке не требуется адрес для присваивания — можно просто подсунуть ей саму переменную. Для доступа по ссылке, в отличии от указателей, тоже не нужно никакого префикса: можно просто использовать ссылку как обычную переменную.

Также, в отличии от C#/Java, в С++ ссылка не может быть пустой (null). Это значит что все ссылки должны быть инициализированы, т.е. ссылаться на что-то. Это с одной стороны хорошо (в управляемых языках многие мечтают о non-nullable objects), но с другой стороны создает много проблем — например, если в классе есть поле-ссылка, вы обязаны инициализировать ее в конструкторе.

Умные указатели

К сожалению, ни один из типов приведенных ранее не ведет себя как ссылка в C# или Java, т.к. любой объект, выделенный в куче, нужно удалять вручную. Но как это сделать если ты не знаешь, кому, когда и на сколько времени понадобится твой объект? Для этого есть умные указатели.

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

shared_ptr<Person> person = make_shared<Person>("Dmitri", 123);

Такой объект можно передавать куда угодно и не бояться его использовать, и он — пожалуй лучший аналог ссылок в C#/Java, хотя и с ограничениями. Одно из ограничений — невозможность рекурсии, то есть нельзя писать вот так:

struct Parent
{
  shared_ptr<Child> child;
}
struct Child
{
  shared_ptr<Parent> parent; // так нельзя
}

Поскольку нет никакого централизованного учета ссылок, подобная конструкция не приведет ни к чему хорошему — в Child вместо shared_ptr придется использовать weak_ptr ну или обычную ссылку.

shared_ptr<> также решает проблему хранения ссылок в таких коллекциях как vector. Проблема в том, что нельзя сделать vector<Foo&>, но вполне можно сделать vector<shared_ptr<Foo>>.

Помимо shared_ptr<> есть несколько других типов умных указателей.

Алгоритмы

С++ не обладает никаким «интерфейсом» который символизирует перечисляемые объекты. Вместо этого он использует итераторы, которые умеют ходить по той или иной структуре. Любой перечисляемый тип может определить функции begin() и end() который возвращают итераторы на первый и за-последним элементы.

С++ также определяет цикл for для обхода коллекций, то есть можно написать:

vector v{1,2,3};
for (auto x : v)
  cout << c << endl;

Код выше также использует ключевое слово auto (аналог var в C#) для вывода типов.

В целом, алгоритмы в С++ это глобальные функции с такими красочными названиями как count_if и всякими прелестями вроде того факта, что remove на контейнере вовсе не удаляет объекты из контейнера, а только сваливает их в конец, а собственно удаляет их функция erase() (очень неинтуитивно).

У контейнеров нет функций-членов с алгоритмами вроде sort() или search(). И добавить их в контейнер нельзя (за исключением наследования) т.к. в С++ нет функций расширения. Алгоритмы специально спроектированы так, чтобы работать на всех возможных контейнерах путем итерирования.

Заключение

Сейчас С++ становится лучше. Улучшается синтаксис языка, добавляются новые библиотеки, но стоит признать, что некоторые проблемы так и не решены, и есть множество недоработок в стандартной библиотеке, некоторые из которых кроме как переписыванием не починить.

В плане компиляторов тоже все лучше и лучше. Есть компилятор Clang (дает вменяемые ошибки при компиляции вместо километров ада), интерпретатор Cling (REPL-среда на базе Clang, рекомендую!), C++ компилятор от Microsoft поддерживает инкрементальную и Edit & Continue компиляцию, а еще скоро процесс компиляции существенно ускорится путем введения модулей.

Что касается инструментов, то ReSharper C++ (если вы пишете под Visual Studio) и CLion (кросс-платформенная IDE) позволяют комфортно писать на этом языке. А такие технологии как CUDA Toolkit и Intel Parallel Studio позволяют амортизировать популярные аппаратные платформы CUDA и Intel Xeon Phi. ■

Источник: Блог Дмитpия Hecтepука

Наш сайт является информационным посредником. Сообщить о нарушении авторских прав.

С++ csharp Java tutorial