Идиома копирования и замены

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

По моему опыту, хотя оптимизация производительности — важная вещь, часто узкое место связано с задержкой. Это связано либо с сетью, либо с базой данных.

Проверив некоторые метрики, мы увидели, что очереди на фронтенде возникают каждый час.

Короче говоря, все дело было в материализованном представлении. Мы внедрили его для повышения производительности, но, похоже, это не помогло.

Что мы могли сделать?

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

Мы нашли настройку, позволяющую обновлять представление вне очереди. Благодаря этому новое представление создавалось в то время, как старое еще использовалось. Затем, когда все было готово, Oracle начал использовать новое представление и удалил старое.

Очередь исчезла.

Мы обменяли немного места на время.

Очевидно, что эта идея не относится исключительно к базам данных. В C++ есть похожая концепция, идиома, называемая copy-and-swap.

Мотивы

Но одинаковы ли мотивы?

Не совсем.

Хотя я могу представить себе ситуацию, когда есть глобальная переменная, которая может использоваться разными потоками, и очень важно ограничить время, затрачиваемое на обновление этой переменной.

Есть кое-что более важное.

Речь идет о безопасности копирующих присваиваний. Что такое присвоение копии? Вы создаете новый объект и присваиваете его уже существующей переменной. Объект, который хранился в существующей переменной, уничтожается.

Таким образом, есть создание и уничтожение. Первое может закончиться неудачей, но разрушение не должно.

Так ли это на самом деле на практике?

Не обязательно.

Часто происходит так, что присваивание выполняется от члена к члену.

class MyClass {
 public:
  MyClass(int x, int y) noexcept : m_x(x), m_y(y) {}

  MyClass& operator=(const MyClass& other) noexcept {

    if (this != &other)
    {
      //Copy member variables
      m_x = other.m_x;
      m_y = other.m_y;
    }

    return *this;
  }

  // ...

 private:
  //Member variables
  int m_x;
  int m_y;
};
Вход в полноэкранный режим Выход из полноэкранного режима

Проблема заключается в том, что делать, если присвоение копии не удается? Здесь мы имеем дело с простыми членами POD, но это легко может быть что-то более сложное. Что-то более подверженное ошибкам. Если копирование не удается, если конструирование любого из этих членов не удается, наш объект, которому мы хотели присвоить значение, остается в непоследовательном состоянии.

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

Стандартная библиотека C++ обеспечивает несколько уровней безопасности исключений (в порядке убывания безопасности):
1) Гарантия отсутствия выброса, также известная как прозрачность отказа: Операциям гарантируется успех и выполнение всех требований даже в исключительных ситуациях. Если возникает исключение, оно будет обработано внутри системы и не будет замечено клиентами.
2) Сильная безопасность исключений, также известная как семантика фиксации или отката: Операции могут быть неудачными, но неудачные операции гарантированно не имеют побочных эффектов, оставляя исходные значения нетронутыми[9].
Базовая безопасность исключений: Частичное выполнение неудачных операций может привести к побочным эффектам, но все инварианты сохраняются. Любые сохраненные данные будут содержать допустимые значения, которые могут отличаться от исходных. Утечки ресурсов (включая утечки памяти) обычно исключаются инвариантом, утверждающим, что все ресурсы учтены и управляются.
3) Отсутствие безопасности исключений: Никаких гарантий не дается.

Если нам нужна надежная защита от исключений, идиома копирования и замены поможет нам достичь этого.

Строительные блоки

Конструкции могут потерпеть неудачу, но разрушение не должно произойти. Поэтому сначала мы должны создать новый объект самостоятельно, а затем поменять его местами со старым. Если строительство не удастся, исходный объект не будет изменен вообще. Мы находимся на безопасной стороне. Затем следует поменять хэндлы местами, и мы знаем, что уничтожение временного объекта со старыми данными не произойдет.

Давайте посмотрим на это на практике.

Для реализации идиомы копирования и замены нам нужны три вещи. Нам нужны конструктор копии и деструктор, что не является очень большими требованиями, а также функция подкачки. Функция swap должна быть способна поменять местами два объекта одного класса, сделать это, член за членом и без выброса исключения.

Мы хотим, чтобы наш оператор присвоения копий выглядел следующим образом:

MyClass& MyClass::operator=(const MyClass& other) noexcept {

  if (this != &other)
  {
    MyClass temp(other);
    swap(*this, temp);
  }

  return *this;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Функция swap должна менять местами, или, другими словами, обмениваться содержимым двух объектов, член за членом. Для этого мы не можем использовать std::swap, потому что для этого нужны и копирование-присвоение, и копирование-конструктор, которые мы пытаемся создать сами. Вот что мы можем сделать вместо этого.

friend void swap(MyClass& iLhs, MyClass& iRhs) noexcept {
    using std::swap;
    swap(iLhs.m_x, iRhs.m_x);
    swap(iLhs.m_y, iRhs.m_y);
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь, вероятно, следует отметить три момента.
1) Мы вызываем swap член за членом.
2) Мы называем swap без оговорок, в то время как мы также используем using std::swap. Импортируя std::swap в наше пространство имен, компилятор может решить, будет ли вызван пользовательский swap или стандартный.
3) Мы сделали swap дружественной функцией. Узнайте здесь о причинах!

На данный момент, нужно ли вам явно писать конструктор копирования и деструктор, зависит от того, какими данными управляет ваш класс. Взгляните на «таблицу Хиннанта»! Поскольку мы написали конструктор и задание копирования, конструктор копирования и деструктор являются дефолтными. Но кто может запомнить таблицу?

Лучше следовать правилу пяти и просто написать все специальные функции, если мы написали одну. Хотя мы можем дефолтить недостающие. Так что давайте найдем решение прямо здесь.

#include <utility>

class MyClass {
 public:
  MyClass(int x, int y) noexcept : m_x(x), m_y(y) {}

  ~MyClass() noexcept = default;
  MyClass(const MyClass&) noexcept = default;
  MyClass(MyClass&&) noexcept = default;
  MyClass& operator=(MyClass&& other) noexcept = default;

  MyClass& operator=(const MyClass& other) noexcept {

    if (this != &other)
    {
      MyClass temp(other);
      swap(*this, temp);
    }

    return *this;
  }

  friend void swap(MyClass& iLhs, MyClass& iRhs) noexcept {
      using std::swap;
      swap(iLhs.m_x, iRhs.m_x);
      swap(iLhs.m_y, iRhs.m_y);
  }


 private:
  int m_x;
  int m_y;
};
Вход в полноэкранный режим Выход из полноэкранного режима

Что насчет членов-указателей?

Если в нашем классе есть член-указатель, конструктор копирования должен быть правильно реализован для выполнения глубокого копирования, и, конечно, деструктор также должен быть правильным, чтобы мы могли избежать утечек. В то же время, оператор присваивания не нужно менять, свопинг по-прежнему корректен.

Приведем небольшой пример, я просто изменил члены int на unique_ptr.

class MyClass {
 public:
  MyClass(int x, int y) noexcept : m_x(std::make_unique<int>(x)), m_y(std::make_unique<int>(y)) {}

  ~MyClass() noexcept = default;
  MyClass(const MyClass& other) noexcept : m_x(std::make_unique<int>(*other.m_x)), m_y(std::make_unique<int>(*other.m_y)) {}
  MyClass(MyClass&&) noexcept = default;
  MyClass& operator=(MyClass&& other) noexcept = default;

  MyClass& operator=(const MyClass& other) noexcept {

    if (this != &other)
    {
      MyClass temp(other);
      swap(*this, temp);
    }

    return *this;
  }

  friend void swap(MyClass& iLhs, MyClass& iRhs) noexcept {
      using std::swap;
      swap(iLhs.m_x, iRhs.m_x);
      swap(iLhs.m_y, iRhs.m_y);
  }


 private:
  std::unique_ptr<int> m_x;
  std::unique_ptr<int> m_y;
};
Вход в полноэкранный режим Выход из полноэкранного режима

Какие-нибудь недостатки?

Реализуя идиому copy-and-swap, мы получаем меньше повторений кода, так как в присваивании копии мы вызываем конструктор копии. Мы также получаем надежную защиту от исключений. Есть ли подвох?

Вы можете получить снижение производительности. В конце концов, мы должны сделать дополнительное выделение в присваивании копии, где мы создаем временную копию. Это может быть или не быть важным в зависимости от вашего случая. Чем сложнее ваш класс и чем чаще вы используете его в контейнере, тем существеннее становится проблема.

В более простых случаях различия могут быть даже оптимизированы, как это произошло с приведенными выше классами. Нельзя просто предполагать. Прежде чем принять решение, измеряйте, измеряйте и измеряйте!

Заключение

Копирование и замена — это идиома в C++, которая обеспечивает сильную безопасность исключений при копировании объектов. Она также устраняет дублирование кода, хотя иногда это может показаться излишеством.

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

Я бы выбрал дополнительную безопасность по умолчанию, в противном случае измеряйте, измеряйте и измеряйте, чтобы вы могли принять взвешенное решение.

Подключайтесь глубже

Если вам понравилась эта статья, пожалуйста

  • нажмите на кнопку «Мне нравится»,
  • подпишитесь на мою рассылку
  • и давайте общаться в Twitter!

Оцените статью
devanswers.ru
Добавить комментарий