C++ — один из самых непонятных языков в современной поп-культуре разработчиков программного обеспечения. Люди часто сравнивают его с C, потому что это "низкоуровневый" язык. Следовательно, он получил репутацию эзотерического языка, который интересует только параноиков производительности. Это далеко не так. Я программирую на C++ в качестве основного языка уже некоторое время, и опыт разработчика на самом деле очень хорош — гораздо лучше, чем можно было себе представить.
В этой статье мне хотелось бы развенчать некоторые распространенные мифы о C++, которые я слышал до того, как начал его использовать. Затем расскажу о реальных супервозможностях, которые предоставляет C++ и которых нет у большинства других языков.
Уже некоторое время в C++ существуют умные указатели. Используя их, можно добиться того же поведения, что и с объектами в других языках, таких как Java и Python. В частности, std::shared_ptr работает, оборачивая обычный объект в копируемый и перемещаемый объект с механизмом подсчета ссылок. Таким образом, когда ни один код не ссылается на shared_ptr, он благополучно ликвидируется и освобождается, как в большинстве языков. Простейший способ создания разделяемого указателя выглядит следующим образом:
std::shared_ptr cat(new Pet("cat"));
// or
std::shared_ptr cat = std::make_shared<Pet>("cat");
Хотя это общий шаблон для большинства других языков, C ++ позволяет вам дополнительно контролировать, как осуществляется доступ к объекту, например, используя unique_ptr, но об этом позже.
В целом, с использованием умных указателей, управлять памятью в C++ не сложнее, чем в других языках. Это сделано преднамеренно, поскольку вам необходимо уточнить, каким будет ожидаемое поведение, потому что вы по-прежнему можете создавать и передавать обычные указатели старым добрым (безобразным?) способом.
Как правило, при умелом их использовании вы также вряд ли столкнетесь с ошибками сегментации, которые были очень распространены в C.
auto square = [=](int x) { return x * x; };
Для контекста, Java получила лямбды в 2014 году с выходом Java 8. В C++ лямбды появились с C++11 (2011). Вот так.
И продолжают выходить важные обновления, например, C++20 совсем недавно, которые добавляют еще больше возможностей, чтобы упростить работу с C++. Например, аргументы переменной длины уже давно есть в C++ благодаря вариативным шаблонам. Дженерики также отлично работают в C++, хотя и не так, как вы привыкли в Java. Эти возможности продолжают улучшать способы разработки программного обеспечения.
class Greeting {
public:
Greeting(std::string greeting) : greeting_(greeting) {}
std::string get_greeting() const {
return greeting_;
}
std::string set_greeting(std::string new_) {
greeting_ = new_;
}
private:
std::string greeting_;
};
Теперь вы можете инициализировать этот класс как константу и по-прежнему вызывать геттер. Когда вы попытаетесь изменить состояние класса, компилятор будет ругаться!
const Greeting g("hello");
g.get_greeting(); // returns "hello"
g.set_greeting("hi"); // does not compile
Если быть точным, эти функции не являются полностью чистыми в том смысле, что если вы неправильно напечатаете некоторые из ваших переменных, то возможно искажение ресурсов. Например, если у вас есть указатель const на не const-объект, то изменение указателя невозможно, но возможно изменение объекта, на который он указывает. Однако этих проблем обычно можно избежать, правильно задавая указатель (т.е. сделав его const-указателем на const-объект).
Может показаться, будто я противоречу сам себе, упоминая такой крайний случай в распространенном примере использования. Тем не менее, я не думаю, что это опровергает мое основное утверждение: В C++ трудно ошибиться, если вы знаете, чего хотите, поскольку он дает вам инструменты для выражения в коде именно того, что вам нужно. Хотя такие языки программирования, как Python, могут абстрагировать все это от вас, они обходятся гораздо дороже. Представьте, что вы идете в магазин мороженого, а вам подают только шоколад, потому что большинство людей обычно хотят именно его - это и есть Python. В зависимости от того, как вы на это посмотрите, конечно, в некотором смысле с шоколадом сложнее ошибиться, но в целом не магазин, а сам пользователь должен определять, что ему нужно.
Хорошая const-ность — это большой плюс, но есть еще несколько вещей, которые позволяет делать C++ и которые предотвращают появление багов в продакшене больших проектов. Он позволяет вам настраивать семантику перемещения/копирования/удаления для классов, которые вы разрабатываете, если вам это необходимо. Позволяет передавать данные по значению и использовать такие расширенные возможности, как множественное наследование. Все эти вещи делают C++ менее строгим.
auto x = 1;
x = 2; // works
x = "abc"; // compilation error
Вы также можете использовать auto в типах возвращаемого значения:
auto func(int x) { return x * x; }
Можно использовать его в циклах, например, для перебора карт:
for (auto& [key, value]: hashmap) {...}
auto не означает, что типы являются динамическими — они все еще выводятся во время компиляции и после присвоения не могут быть изменены. Возможно, это и к лучшему. На практике для больших кодовых баз, пожалуй, также помогает удобочитаемость, если вместо auto вводить полный тип. Тем не менее, такая возможность существует, если это вам потребуется.
Вы также можете указывать псевдонимы типов и/или пространств имен, как в Python. Например, можно сделать что-то вроде:
using tf = tensorflow;
// Now you can use tf::Example instead of tensorflow::Example.
Шаблоны C++ (похожие на Java Generics, но при этом довольно разные) также помогают значительно сократить дублирование кода и могут оказаться элегантным решением для многих случаев использования.
В целом, C++ определенно более многословен, чем большинство новых языков программирования, таких как Kotlin и Python. Однако он не намного больше, чем C#, Java или даже JavaScript, и их многословность не сильно повлияла на популярность этих языков.
Использование библиотек само по себе может быть сложной задачей в C++ с необходимостью поддерживать файлы CMake, хотя во многом они мало чем отличаются от файлов gradle или package.json в других языках. Однако, опять же, инструмент сборки Bazel от Google с открытым исходным кодом чрезвычайно упрощает работу даже с кросс-языковыми сборками. К нему нужно привыкнуть, но он обеспечивает действительно быструю сборку и в целом очень удобен для разработчиков.
Сверхвозможности
Итак, развеяв все эти распространенные мифы о C++, вот некоторые вещи в C++, которые многие другие языки не позволяют вам делать:
Настройте семантику ваших классов
Предположим, у вас есть класс, который содержит большие данные. Вы бы предпочли, чтобы они не копировались, а всегда передавались по ссылке. Как разработчик интерфейса вы можете это реализовать. Более того, при необходимости вы можете настроить, как именно будет происходить копирование данных.
Что насчет перемещения объектов — вместо того, чтобы копировать все данные из предыдущего блока памяти в новый, а затем удалять старый, может быть вы можете оптимизировать это, просто переключая местоположение указателей.
Уничтожение объектов — при выходе объекта из области видимости, возможно, вы автоматически хотите освободить некоторые ресурсы (например, мьютексы, которые автоматически освобождаются в конце функции). Это работает примерно так же, как функция defer в Go.
Как дизайнер интерфейса, вы можете настроить каждый маленький аспект того, как пользователи будут использовать ваш класс. В большинстве случаев в этом нет необходимости, однако, если возникает такая потребность, C++ позволяет вам полностью выразить свои потребности. Это очень мощный инструмент, который может сэкономить вашей компании многие часы потраченного времени на крупных проектах.
Оптимизируйте процесс доступа к памяти
Выше я вкратце упомянул об умных указателях. Помимо shared_ptr, вы также можете использовать unique_ptr, который гарантирует, что только один объект может обладать ресурсом. Наличие одного владельца данных упрощает организацию и обсуждение крупных проектов. На самом деле, хотя shared_ptr наиболее близко имитирует Java и другие языки OOP, в целом лучше использовать unique_ptr. Это также повышает безопасность языка.
Вы также можете задать константы времени компиляции, позволяющие компилятору выполнять больше работы во время сборки, чтобы двоичный файл в целом работал быстрее.
Строгая типизация
В целом, я заметил, что работу с типизированными объектами в C++ намного легче отлаживать, чем в других языках. Например, после нескольких часов отладки проекта на JavaScript я обнаружил, что ошибка возникала из-за того, что я передавал 1 аргумент в функцию с 2 аргументами. При этом JavaScript не выдавал никаких ошибок и просто производил нежелательные результаты. Я бы предпочел, чтобы ошибка возникла при компиляции, а не во время выполнения.
Однако существует множество типизированных языков, поэтому использование этой суперспособности может показаться излишним. Но C++ делает больше. Во-первых, C++ позволяет передавать что-либо по ссылке, по указателю или по значению. Это означает, что вы можете передать ссылку на целое число и попросить функцию изменить его вместо того, чтобы использовать возвращаемое значение (в некоторых случаях это может быть удобно). Это также означает, что при необходимости вы можете выполнять операции в памяти, используя указатель на что угодно. Обычно, однако, вы хотите передавать объекты в виде постоянной ссылки (например, const A&), что не приведет к копированию и защитит ваш объект от непреднамеренного мутирования. Это сильные гарантии, и благодаря им ваш код становится намного проще для понимания. В TypeScript, например, вы не можете переназначить const-объект, но можете мутировать его.
Предостережения
Итак, C++ - это здорово и все такое, но есть очевидные ограничения относительно его использования. На нем можно легко писать микросервисы (например, на gRPC), но я бы, скорее всего, не стал использовать C++ для реального веб-сервера (для этого лучше применить TypeScript). Несмотря на его скорость, я бы не стал использовать C++ для анализа данных (для этого я бы, скорее всего, выбрал pandas). Есть некоторые вещи, для которых C++ подходит отлично, а для других он просто не годится. В конечном счете, вы все равно должны выбрать правильный инструмент для той работы, которую собираетесь выполнить. Надеюсь, эта статья сделала C++ немного более привлекательным в ваших глазах, чем вы привыкли его видеть.
В этой статье мне хотелось бы развенчать некоторые распространенные мифы о C++, которые я слышал до того, как начал его использовать. Затем расскажу о реальных супервозможностях, которые предоставляет C++ и которых нет у большинства других языков.
Миф 1: запомни это, запомни то
Мы все знаем, что C печально известен ручным управлением памятью, например, с помощью malloc и free. С ними трудно работать, они приводят к множеству ошибок, которые нужно проверять вручную, и в целом являются настоящим кошмаром. Когда люди слышат, что C++ также отличается высокой производительностью, то думают, что это происходит за счет того, что все особенности распределения памяти такие же, как и в C, следовательно, из этого делается вывод, что и здесь всё будет ужасно. Это категорически неверно.Уже некоторое время в C++ существуют умные указатели. Используя их, можно добиться того же поведения, что и с объектами в других языках, таких как Java и Python. В частности, std::shared_ptr работает, оборачивая обычный объект в копируемый и перемещаемый объект с механизмом подсчета ссылок. Таким образом, когда ни один код не ссылается на shared_ptr, он благополучно ликвидируется и освобождается, как в большинстве языков. Простейший способ создания разделяемого указателя выглядит следующим образом:
std::shared_ptr cat(new Pet("cat"));
// or
std::shared_ptr cat = std::make_shared<Pet>("cat");
Хотя это общий шаблон для большинства других языков, C ++ позволяет вам дополнительно контролировать, как осуществляется доступ к объекту, например, используя unique_ptr, но об этом позже.
В целом, с использованием умных указателей, управлять памятью в C++ не сложнее, чем в других языках. Это сделано преднамеренно, поскольку вам необходимо уточнить, каким будет ожидаемое поведение, потому что вы по-прежнему можете создавать и передавать обычные указатели старым добрым (безобразным?) способом.
Как правило, при умелом их использовании вы также вряд ли столкнетесь с ошибками сегментации, которые были очень распространены в C.
Миф 2: он старый и неактуальный
C++ очень активно поддерживается, и новые возможности продолжают регулярно появляться. Я думаю, что одной из самых распространенных "новых" функций во многих языках, которыми люди восхищаются, являются лямбды. Конечно, в C++ нет лямбд, верно? Ошибаетесь. Лямбда-функция в C++ может быть определена как:auto square = [=](int x) { return x * x; };
Для контекста, Java получила лямбды в 2014 году с выходом Java 8. В C++ лямбды появились с C++11 (2011). Вот так.
И продолжают выходить важные обновления, например, C++20 совсем недавно, которые добавляют еще больше возможностей, чтобы упростить работу с C++. Например, аргументы переменной длины уже давно есть в C++ благодаря вариативным шаблонам. Дженерики также отлично работают в C++, хотя и не так, как вы привыкли в Java. Эти возможности продолжают улучшать способы разработки программного обеспечения.
Миф 3: легко ошибиться
Напротив, несмотря на то, что изучение некоторых его особенностей может занять некоторое время, в C++ очень сложно сделать так, чтобы ваш код сделал нежелательные вещи. Например, во многих объектно-ориентированных языках нет поддержки "чистых" функций, то есть функций, которые гарантированно неизменяемы. В C++ вы можете пометить методы класса как const, если они не изменяют его состояние. Затем эти методы также можно вызывать для постоянных экземпляров класса. Вот пример:class Greeting {
public:
Greeting(std::string greeting) : greeting_(greeting) {}
std::string get_greeting() const {
return greeting_;
}
std::string set_greeting(std::string new_) {
greeting_ = new_;
}
private:
std::string greeting_;
};
Теперь вы можете инициализировать этот класс как константу и по-прежнему вызывать геттер. Когда вы попытаетесь изменить состояние класса, компилятор будет ругаться!
const Greeting g("hello");
g.get_greeting(); // returns "hello"
g.set_greeting("hi"); // does not compile
Если быть точным, эти функции не являются полностью чистыми в том смысле, что если вы неправильно напечатаете некоторые из ваших переменных, то возможно искажение ресурсов. Например, если у вас есть указатель const на не const-объект, то изменение указателя невозможно, но возможно изменение объекта, на который он указывает. Однако этих проблем обычно можно избежать, правильно задавая указатель (т.е. сделав его const-указателем на const-объект).
Может показаться, будто я противоречу сам себе, упоминая такой крайний случай в распространенном примере использования. Тем не менее, я не думаю, что это опровергает мое основное утверждение: В C++ трудно ошибиться, если вы знаете, чего хотите, поскольку он дает вам инструменты для выражения в коде именно того, что вам нужно. Хотя такие языки программирования, как Python, могут абстрагировать все это от вас, они обходятся гораздо дороже. Представьте, что вы идете в магазин мороженого, а вам подают только шоколад, потому что большинство людей обычно хотят именно его - это и есть Python. В зависимости от того, как вы на это посмотрите, конечно, в некотором смысле с шоколадом сложнее ошибиться, но в целом не магазин, а сам пользователь должен определять, что ему нужно.
Хорошая const-ность — это большой плюс, но есть еще несколько вещей, которые позволяет делать C++ и которые предотвращают появление багов в продакшене больших проектов. Он позволяет вам настраивать семантику перемещения/копирования/удаления для классов, которые вы разрабатываете, если вам это необходимо. Позволяет передавать данные по значению и использовать такие расширенные возможности, как множественное наследование. Все эти вещи делают C++ менее строгим.
Миф 4: он многословен
Хорошо, здесь есть доля истины. Придя из Python, где люди так устали от ввода numpy, что все коллективно решили импортировать его как np, ввод более 2 букв действительно кажется многословным. Но современный C++ гораздо менее многословен, чем раньше! Например, вывод типов, как в Go, стал доступен в C++ с введением ключевого слова auto.auto x = 1;
x = 2; // works
x = "abc"; // compilation error
Вы также можете использовать auto в типах возвращаемого значения:
auto func(int x) { return x * x; }
Можно использовать его в циклах, например, для перебора карт:
for (auto& [key, value]: hashmap) {...}
auto не означает, что типы являются динамическими — они все еще выводятся во время компиляции и после присвоения не могут быть изменены. Возможно, это и к лучшему. На практике для больших кодовых баз, пожалуй, также помогает удобочитаемость, если вместо auto вводить полный тип. Тем не менее, такая возможность существует, если это вам потребуется.
Вы также можете указывать псевдонимы типов и/или пространств имен, как в Python. Например, можно сделать что-то вроде:
using tf = tensorflow;
// Now you can use tf::Example instead of tensorflow::Example.
Шаблоны C++ (похожие на Java Generics, но при этом довольно разные) также помогают значительно сократить дублирование кода и могут оказаться элегантным решением для многих случаев использования.
В целом, C++ определенно более многословен, чем большинство новых языков программирования, таких как Kotlin и Python. Однако он не намного больше, чем C#, Java или даже JavaScript, и их многословность не сильно повлияла на популярность этих языков.
Миф 5: трудно делать простые вещи
Здесь снова не все так однозначно. Обычные операции, такие как объединение строк разделителем, сложнее, чем нужно. Однако эта проблема довольно легко решается с помощью библиотек с открытым исходным кодом, таких как Abseil от Google, содержащих тысячи вспомогательных функций, с которыми очень легко работать. Помимо строковых утилит, Abseil также содержит специальные реализации хэшмапов, хелперов параллелизма и инструментов отладки. Другие библиотеки, такие как Boost, облегчают работу с BLAS-функциями (например, точечными продуктами, матричными умножениями и т.д.) и отличаются высокой производительностью.Использование библиотек само по себе может быть сложной задачей в C++ с необходимостью поддерживать файлы CMake, хотя во многом они мало чем отличаются от файлов gradle или package.json в других языках. Однако, опять же, инструмент сборки Bazel от Google с открытым исходным кодом чрезвычайно упрощает работу даже с кросс-языковыми сборками. К нему нужно привыкнуть, но он обеспечивает действительно быструю сборку и в целом очень удобен для разработчиков.
Сверхвозможности
Итак, развеяв все эти распространенные мифы о C++, вот некоторые вещи в C++, которые многие другие языки не позволяют вам делать:
Настройте семантику ваших классов
Предположим, у вас есть класс, который содержит большие данные. Вы бы предпочли, чтобы они не копировались, а всегда передавались по ссылке. Как разработчик интерфейса вы можете это реализовать. Более того, при необходимости вы можете настроить, как именно будет происходить копирование данных.
Что насчет перемещения объектов — вместо того, чтобы копировать все данные из предыдущего блока памяти в новый, а затем удалять старый, может быть вы можете оптимизировать это, просто переключая местоположение указателей.
Уничтожение объектов — при выходе объекта из области видимости, возможно, вы автоматически хотите освободить некоторые ресурсы (например, мьютексы, которые автоматически освобождаются в конце функции). Это работает примерно так же, как функция defer в Go.
Как дизайнер интерфейса, вы можете настроить каждый маленький аспект того, как пользователи будут использовать ваш класс. В большинстве случаев в этом нет необходимости, однако, если возникает такая потребность, C++ позволяет вам полностью выразить свои потребности. Это очень мощный инструмент, который может сэкономить вашей компании многие часы потраченного времени на крупных проектах.
Оптимизируйте процесс доступа к памяти
Выше я вкратце упомянул об умных указателях. Помимо shared_ptr, вы также можете использовать unique_ptr, который гарантирует, что только один объект может обладать ресурсом. Наличие одного владельца данных упрощает организацию и обсуждение крупных проектов. На самом деле, хотя shared_ptr наиболее близко имитирует Java и другие языки OOP, в целом лучше использовать unique_ptr. Это также повышает безопасность языка.
Вы также можете задать константы времени компиляции, позволяющие компилятору выполнять больше работы во время сборки, чтобы двоичный файл в целом работал быстрее.
Строгая типизация
В целом, я заметил, что работу с типизированными объектами в C++ намного легче отлаживать, чем в других языках. Например, после нескольких часов отладки проекта на JavaScript я обнаружил, что ошибка возникала из-за того, что я передавал 1 аргумент в функцию с 2 аргументами. При этом JavaScript не выдавал никаких ошибок и просто производил нежелательные результаты. Я бы предпочел, чтобы ошибка возникла при компиляции, а не во время выполнения.
Однако существует множество типизированных языков, поэтому использование этой суперспособности может показаться излишним. Но C++ делает больше. Во-первых, C++ позволяет передавать что-либо по ссылке, по указателю или по значению. Это означает, что вы можете передать ссылку на целое число и попросить функцию изменить его вместо того, чтобы использовать возвращаемое значение (в некоторых случаях это может быть удобно). Это также означает, что при необходимости вы можете выполнять операции в памяти, используя указатель на что угодно. Обычно, однако, вы хотите передавать объекты в виде постоянной ссылки (например, const A&), что не приведет к копированию и защитит ваш объект от непреднамеренного мутирования. Это сильные гарантии, и благодаря им ваш код становится намного проще для понимания. В TypeScript, например, вы не можете переназначить const-объект, но можете мутировать его.
Предостережения
Итак, C++ - это здорово и все такое, но есть очевидные ограничения относительно его использования. На нем можно легко писать микросервисы (например, на gRPC), но я бы, скорее всего, не стал использовать C++ для реального веб-сервера (для этого лучше применить TypeScript). Несмотря на его скорость, я бы не стал использовать C++ для анализа данных (для этого я бы, скорее всего, выбрал pandas). Есть некоторые вещи, для которых C++ подходит отлично, а для других он просто не годится. В конечном счете, вы все равно должны выбрать правильный инструмент для той работы, которую собираетесь выполнить. Надеюсь, эта статья сделала C++ немного более привлекательным в ваших глазах, чем вы привыкли его видеть.
C++ — это замечательно, и вот почему
C++ — один из самых непонятных языков в современной поп-культуре разработчиков программного обеспечения. Люди часто сравнивают его с C, потому что это "низкоуровневый" язык. Следовательно, он получил...
habr.com