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. ■

11 responses to “C++ для Java и C# разработчиков”

  1. > С++ разработчику достаточть легко в последствии освоить C#

    Категорически не согласен. Почему-то некоторые задроты думают, что вручную удалив память, они стали какими-то гуру программазма! ДАЛЕКО НЕ. Ровно наоборот – пока “сипиписники” возились в низкоуровневых дебрях, тратя своё время на ЕРУНДУ, “шарповоды” осваивали высокоуровневые вещи типа дженериков, async, TPL и прочее. И когда приходит такой вот “сипиписник” в цэшарп, он выглядит как неандерталец, который не способен использовать тостер. Выглядит жалко.

    А вот и образец “гуманитарной” логики:

    > Одна из них это портативность… Intel C++ Compiler (по фичам он «не очень»)

    Ну то есть существует компилятор С++, который по фичам “очень”, но при этом, очевидно, несовместим с тем, который “не очень”? Тогда знаете что, такая “портативность” не стоит и гроша. Достаточно сосчитать все #ifdef’ы, чтобы понять – С++ реализован у всех по-своему.

    > Со строками в С++ беда.

    Именно. И ЭТО У БАЗОВОГО ТИПА! Кому нужно это “говно с крестами”, если в нём нельзя написать элементарную обработку строк? Вот за это мы и перешли в C# – среду, где нет разбродов хотя бы по элементарным вещам. Конечно, “массивы” там полный отстой (особ. по ср. с Ди), но с самой библиотекой можно работать без особого напряга.

    Карочи, С++ – это не инструмент, а “вынужденная мера”, “зубная боль ИТ”. Кто может – избегает, кто вынужден – учится фигурному катанию на костылях и художественной резьбе по изготовлению берёзовых велосипедов. Бедняги, их жалкие потуги в 21 веке – просто мазохизм!

    1. С++ следует воспринимать как бооольшой такой легаси. Это не плохо и не хорошо, ему можно обучиться. Вот вы летите, например, на самолете, а там во многих местах С++! И там рулит говнокод, там реально все классы вызывают все другие классы, нам в свое время один деятель из Сааб (да, они делают самолеты) показывал. И летают же как-то.

    2. Согласен с Ваней. Иногда бывает, что C++ в качестве инструмента будет самым оптимальным выбором. Это происходит от того, что есть всякие там тулы, либы, экосистемы, специалисты на рынке и так далее. То, что он кому-то из-за этого оптимален, никак не делает его чем-то кроме огромного уродливого монстра, у которого в центре давно сгнивший зомби, а поверх налеплены и пришиты белыми нитями более свежие куски мяса. Самые поверхностные куски даже живые и шевелятся, вау!

  2. Bug: “shared ptr person = make shared(“Dmitri”, 123);” <- underscores поело кажется.
    Спасибо за статью.

    1. Это возможно у вас всё поело, у меня всё видно.

  3. > а еще скоро процесс компиляции существенно ускорится путем введения модулей

    не скоро, судя по результатм последнего шабаша комитета ))

    вообще в целом плюсы по прежнему продолжают тотптаться на месте, зато можно время в нановеках мерить ))

    1. Там идет круговая порука. Компиляторостроители говорят “давайте стандарт” а комитет говорит “мы не готовы это стандартизировать т.к. нет готовых реализаций”. То есть они хотят чтобы все шишки за них набил кто-то другой, они посмотрели, сказали “опа, у тех ребят вроде работает, забираем!” а потом они такие белые и пушистые и никакого нытья про пропущенный make_unique в С++11.

      Тем не менее, Microsoft работает надо модулями как могут, и думаю что-то у них получится. Уже есть куски которые можно пробовать. Но даже без модулей, MSVC очень хитро компилирует, очень инкрементально и в результате быстро. Конечно, с модулями должна закончиться вся чехарда связанная с физическим вложением одного файла в другой, но не будет забывать, что нужно чтобы кто-то после этого взял STL/Boost/Whatever и их тоже оформил через модули.

  4. >Но даже без модулей, MSVC очень хитро компилирует, очень инкрементально и в результате быстро

    ну тут все отсносительно, мой предпоследний проект на плюсах был такого размера, что на обычной машине собирался час, с SSD было что-то ттипа 40 минут. Чтобы сделать жизнь разрабов не такой больной везде был установлен incredibuild, к тому же хваленая инкрментальная сборка периодически давала совершенно необьяснимые AV в рандомных местах, я конечно после перхода на шарпы просто не понимаю как я так мог жить

    > Уже есть куски которые можно пробовать

    я вроде что-то слышал про то, что в clang уже все работает

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

    Я имею в виду, что это может прозвучать так, как будто все пилят игры на C++ целиком. Так делают только наиболее большие, безобразно консервативные и знаменитые (из-за того, что большие) студии. Не стоит забывать, что при всей их знаменитости и богастве, они составляют меньшинство по всем параметрам: количеству/объему/массе человек, доле оборота денег в индустрии, количеству тайтлов и т.д.

    На самом деле, геймдев уже десяток лет старательно минимизирует участие C++. Делается это разделением игры на движок (C++) и скрипты (Lua и др.). Движок часто берется готовый, а если он не подлежит (или не нуждается в) модификации (обычно так и бывает), то в команде не будет программистов C++ совсем.

    1. Спасибо за пояснение. Действительно, на последней GDC, мы как дураки прилетели со своими CLion/R++ а оказалось что в основном людей интересует Unity, то бишь C#. Но в целом, С++ в геймдеве все равно хоть как-то представлен, так что я его включаю в одну из индустрий-пользователей.

  6. Дмитрий, спасибо за статью. Вот уже 8 лет прошло с момента ее публикации. Не планируете написать статью о современном состоянии С++, его будущем и набирающих популярность аналогах?)

Оставить комментарий