Возвращение к C++ — современный C++

С момента своего создания C++ стал одним из наиболее широко используемых языков программирования в мире. Грамотно сконструированные программы на языках C++ быстры и эффективны. Он более гибок, чем другие языки: он может работать с самыми высокими уровнями абстракции, а также на низком аппаратном уровне. C++ предоставляет стандартные библиотеки с высоким уровнем оптимизации. Он обеспечивает доступ к аппаратным функциям низкого уровня, чтобы максимально увеличить скорость и сократить потребление памяти. C++ может создавать практически любой вид программы: игры, драйверы устройств, HPC, облако, настольный компьютер, внедренные и мобильные приложения, а также многое другое. Даже библиотеки и компиляторы для других языков программирования пишутся на C++.

Одно из начальных требований для C++ — обратная совместимость с языком C. В результате программы на C++ всегда можно писать в стиле C: с необработанными указателями, массивами, символьными строками с завершающим нулем и другими функциями. Это может обеспечить высокую производительность, но также может приводить к ошибкам и увеличению сложности. Эволюция C++ концентрируется на возможностях, которые значительно снижают необходимость использования идиом в стиле C. Старые средства программирования C-по-прежнему существуют, когда они нужны. Однако в современном коде C++ их нужно меньше и меньше. Современный код на C++ проще, безопаснее, элегантнее и так же быстр, как и раньше.

В следующих разделах приводятся общие сведения об основных возможностях современного C++. Если не указано иное, перечисленные здесь функции доступны в C++ 11 и более поздних версиях. В компиляторе C++ от Майкрософт с помощью параметра /std можно указать версию стандарта, используемую для проекта.

Ресурсы и интеллектуальные указатели

Одним из основных классов ошибок в программировании в стиле C является утечка памяти. Утечки часто возникают из-за невозможности вызвать delete для памяти, выделенной с помощью new . Современный C++ придерживается принципа получение ресурса есть инициализация (англ. Resource Acquisition Is Initialization (RAII)). Идея проста. Ресурсы (память кучи, дескрипторы файлов, сокеты и т. д.) должны принадлежать объекту. Этот объект создает и получает новый выделенный ресурс в конструкторе и удаляет его в его деструкторе. Принцип RAII гарантирует, что все ресурсы должным образом возвращаются операционной системе, когда объект-владелец выходит за пределы области.

Для поддержки простого внедрения принципов RAII стандартная библиотека языка C++ предоставляет три типа интеллектуальных указателей: std::unique_ptr, std::shared_ptr и std::weak_ptr. Интеллектуальный указатель обрабатывает выделение и удаление памяти, которой он владеет. В следующем примере показан класс с членом-массивом, который выделяется в куче в вызове make_unique(). Вызовы new и delete инкапсулированы в классе unique_ptr. Когда объект widget выходит из области действия, вызывается деструктор unique_ptr и освобождается память, выделенная для массива.

#include <memory>
class widget
{
private:
    std::unique_ptr<int[]> data;
public:
    widget(const int size) { data = std::make_unique<int[]>(size); }
    void do_something() {}
};

void functionUsingWidget() {
    widget w(1000000);  // lifetime automatically tied to enclosing scope
                        // constructs w, including the w.data gadget member
    // ...
    w.do_something();
    // ...
} // automatic destruction and deallocation for w and w.data

По возможности используйте интеллектуальный указатель для управления памятью кучи. Если необходимо явно использовать new операторы и delete операторы, следуйте принципу RAII. Дополнительные сведения см. в разделе Управление временем жизни и ресурсами объекта (RAII).

std::string и std::string_view

Строки в стиле C — это еще один основной источник ошибок. Используя std::string и std::wstring, можно устранить практически все ошибки, связанные со строками в стиле C. Дополнительно вы получаете преимущества функций-членов для поиска, добавления в конец и начало и т. д. Оба эти класса оптимизированы для быстрой работы. При передаче строки в функцию, для которой требуется доступ только для чтения, в C++ 17 можно использовать std::string_view для еще большего выигрыша в производительности.

std::vector и другие контейнеры стандартной библиотеки

Все контейнеры стандартной библиотеки следуют принципу RAII. Они предоставляют итераторы для безопасного обхода элементов. И они хорошо оптимизированы для повышения производительности, а также тщательно протестированы на отсутствие ошибок. Используя эти контейнеры, можно исключить потенциальные ошибки и неэффективные приемы в пользовательских структурах данных. Вместо необработанных массивов используйте vector в качестве последовательного контейнера в C++.

vector<string> apples;
apples.push_back("Granny Smith");

В качестве ассоциативного контейнера по умолчанию используйте map (не unordered_map). Используйте set, multimap и multiset для вырожденных и множественных операторов выбора.

map<string, string> apple_color;
// ...
apple_color["Granny Smith"] = "Green";

При необходимости оптимизации производительности рассмотрите возможность использования следующих средств.

  • Тип array важен при внедрении, например, как член класса.

  • Неупорядоченные ассоциативные контейнеры, такие как unordered_map. Они имеют меньше издержек на элемент и постоянный по времени поиск, но их сложно использовать правильно и эффективно.

  • Сортированные vector. Дополнительные сведения см. в разделе Алгоритмы.

Не используйте массивы в стиле C. Для более старых API, которым требуется прямой доступ к данным, используйте такие методы доступа, как f(vec.data(), vec.size());. Дополнительные сведения о контейнерах см. в разделе Контейнеры стандартной библиотеки C++.

Алгоритмы стандартной библиотеки

Перед принятием решения о том, что вам нужно написать собственный алгоритм для программы, сначала ознакомьтесь с алгоритмами стандартной библиотеки C++. Стандартная библиотека содержит постоянно увеличивающийся набор различных алгоритмов для многих распространенных операций, таких как поиск, сортировка, фильтрация и рандомизация. Имеется обширная математическая библиотека. В C++17 и более поздних версиях предоставляются параллельные версии многих алгоритмов.

Ниже приведены некоторые важные примеры.

  • for_each, алгоритм обхода по умолчанию (наряду с циклами for на основе диапазона).

  • transform, для изменения элементов контейнера "не на месте".

  • find_if, алгоритм поиска по умолчанию.

  • sort, lower_bound и другие алгоритмы сортировки и поиска по умолчанию.

При написании операторов сравнения по возможности используйте строгие выражения < и именованные лямбда-выражения.

auto comp = [](const widget& w1, const widget& w2)
     { return w1.weight() < w2.weight(); }

sort( v.begin(), v.end(), comp );

auto i = lower_bound( v.begin(), v.end(), widget{0}, comp );

auto вместо явных имен типов

В C++ 11 введено ключевое слово auto для использования в объявлениях переменных, функций и шаблонов. Ключевое слово auto предписывает компилятору определить тип объекта, чтобы не указывать его явным образом. auto особенно полезно, когда выводимый тип является вложенным шаблоном.

map<int,list<string>>::iterator i = m.begin(); // C-style
auto i = m.begin(); // modern C++

Циклы for на основе диапазона

Итерации в стиле C для массивов и контейнеров подвержены ошибкам индексирования, а также достаточно рутинные. Чтобы устранить эти ошибки и сделать код более удобочитаемым, используйте с контейнерами стандартной библиотеки и необработанными массивами циклы for на основе диапазона. Дополнительные сведения см. в разделе Оператор for на основе диапазона.

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v {1,2,3};

    // C-style
    for(int i = 0; i < v.size(); ++i)
    {
        std::cout << v[i];
    }

    // Modern C++:
    for(auto& num : v)
    {
        std::cout << num;
    }
}

Выражения constexpr вместо макросов

Макросы в языках C и C++ являются токенами, которые обрабатываются препроцессором перед компиляцией. Перед компиляцией файла каждый экземпляр токена макроса заменяется определенным значением или выражением. Макросы обычно используются в программировании в стиле C для определения значений констант времени компиляции. Однако макросы подвержены ошибкам и их сложно отлаживать. В современном C++ следует отдавать предпочтение переменным constexpr для констант времени компиляции.

#define SIZE 10 // C-style
constexpr int size = 10; // modern C++

Унифицированная инициализация

В современном C++ можно использовать инициализацию с помощью фигурных скобок для любого типа. Такая форма инициализации особенно удобна при инициализации массивов, векторов и других контейнеров. В следующем примере v2 инициализируется с тремя экземплярами S. v3 инициализируется с тремя экземплярами S, которые сами по себе инициализируются с помощью фигурных скобок. Компилятор выводит тип каждого элемента на основе объявленного типа v3.

#include <vector>

struct S
{
    std::string name;
    float num;
    S(std::string s, float f) : name(s), num(f) {}
};

int main()
{
    // C-style initialization
    std::vector<S> v;
    S s1("Norah", 2.7);
    S s2("Frank", 3.5);
    S s3("Jeri", 85.9);

    v.push_back(s1);
    v.push_back(s2);
    v.push_back(s3);

    // Modern C++:
    std::vector<S> v2 {s1, s2, s3};

    // or...
    std::vector<S> v3{ {"Norah", 2.7}, {"Frank", 3.5}, {"Jeri", 85.9} };

}

Дополнительные сведения см. в разделе Инициализация фигурными скобками.

Семантика перемещения

Современный C++ предоставляет семантику перемещения, что позволяет устранять ненужное копирование памяти. В предыдущих версиях языка в определенных ситуациях копирования нельзя было избежать. Операция перемещения передает владение ресурсом от одного объекта к другому без создания копии. Некоторые классы владеют такими ресурсами, как память кучи, дескрипторы файлов и т. д. При реализации класса, владеющего ресурсами, можно определить для него конструктор перемещения и оператор присваивания перемещения. Компилятор выбирает эти специальные члены класса при разрешении перегрузки в ситуациях, когда копирование не требуется. Типы контейнеров стандартной библиотеки вызывают для объектов конструктор перемещения, если он определен. Дополнительные сведения см. в разделе Конструкторы перемещения и операторы присваивания перемещения (C++).

Лямбда-выражения

В программировании в стиле C функцию можно передать в другую функцию с помощью указателя функции. Указатели функций неудобно поддерживать и сложно понимать. Функция, на которую они ссылаются, может быть определена в любом месте исходного кода, далеко от точки ее вызова. Кроме того, они не являются типобезопасными. Современный C++ предоставляет объекты-функции — классы, переопределяющие оператор operator(), который позволяет вызывать их как функцию. Наиболее удобный способ создания объектов-функций — встроенные лямбда-выражения. В следующем примере показано, как использовать лямбда-выражение для передачи объекта-функции, которую функция find_if будет вызывать для каждого элемента в векторе.

    std::vector<int> v {1,2,3,4,5};
    int x = 2;
    int y = 4;
    auto result = find_if(begin(v), end(v), [=](int i) { return i > x && i < y; });

Лямбда-выражение может быть прочитано как "функция, которая принимает один аргумент типа int и возвращает логическое значение[=](int i) { return i > x && i < y; }, указывающее, больше ли аргумент и x меньшеy". Обратите внимание, что переменные x и y из окружающего контекста можно использовать в лямбда-выражениях. [=] указывает, что эти переменные записываются по значению, то есть лямбда-выражение имеет собственные копии этих значений.

Исключения

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

std::atomic

Используйте структуру и связанные типы std::atomic стандартной библиотеки C++ для механизмов взаимодействия между потоками.

std::variant (C++17)

Объединения обычно используются в программировании в стиле C для экономии памяти, позволяя членам разных типов занимать одно и то же расположение в памяти. Однако объединения не являются типобезопасными и могут быть подвержены ошибкам программирования. В C++ 17 появился класс std::variant в качестве более надежной и безопасной альтернативы объединениям. Функция std::visit может использоваться для доступа к членам типа variant типобезопасным способом.

См. также

Справочник по языку C++
Лямбда-выражения
Стандартная библиотека C++
Соответствие стандартам языка Microsoft C/C++