Анализатор обнаружил класс, в котором реализован конструктор копирования, но не реализован 'operator =', или наоборот, реализован 'operator =', но не реализован конструктор копирования.
Работать с такими классами очень опасно. Другими словами, нарушен "Закон Большой Двойки". Про этот закон, будет рассказано ниже.
Рассмотрим пример опасного класса. Пример длинный. Но сейчас важно только то, что в классе есть оператор присваивания, но нет конструктора копирования.
class MyArray { char *m_buf; size_t m_size; void Clear() { delete [] m_buf; } public: MyArray() : m_buf(0), m_size(0) {} ~MyArray() { Clear(); } void Allocate(size_t s) { Clear(); m_buf = new char[s]; m_size = s; } void Copy(const MyArray &a) { Allocate(a.m_size); memcpy(m_buf, a.m_buf, a.m_size * sizeof(char)); } char &operator[](size_t i) { return m_buf[i]; } MyArray &operator =(const MyArray &a) { Copy(a); return *this; } };
Оставим в стороне практичность и полезность такого класса. Это всего лишь пример. Нам важно, что вот такой код, будет корректно работать:
{ MyArray A; A.Allocate(100); MyArray B; B = A; }
Массив успешно копируется с помощью оператора присваивания.
Следующий фрагмент кода приведёт к неопределенному поведению. Приложение упадёт или его работа будете нарушена как-то ещё.
{ MyArray A; A.Allocate(100); MyArray C(A); }
Дело в том, что в классе не реализован конструктор копирования. При создание объекта 'C', указатель на массив будет просто скопирован. Это приведёт к двоёному освобождению памяти при разрушении объектов A и C.
Аналогичная проблема будет, если реализован конструктор копирования, но нет оператора копирования.
Что-бы исправить класс, следует добавить конструктор копирования:
MyArray &operator =(const MyArray &a) { Copy(a); return *this; } MyArray(const MyArray &a) : m_buf(0), m_size(0) { Copy(a); }
Если анализатор выдал предупреждение V690, то не ленитесь и реализуйте недостающий метод. Сделайте это, даже если код сейчас работает правильно, и вы помните, об особенностях класса. Пройдёт время, забудется отсутствие operator= или конструктора копирования. И вы или ваш коллега допустите ошибку, которую будет сложно найти. Когда поля класса скопированы автоматически, то часто, такой класс "почти работает". Неприятности проявляют себя позже в сосем другом месте программы.
Как было сказано в начале, диагностика V690 находит классы, в которых нарушен "Закон Большой Двойки". Рассмотрим это подробнее. Но начать надо с "Правило трёх". Обратимся к Wikipedia:
Правило трёх (также известное как "Закон Большой Тройки" или "Большая Тройка") — правило в C++, гласящее, что если класс или структура определяет один из следующих методов, то они должны явным образом определить все три метода:
Эти три метода являются особыми членами-функциями, автоматически создаваемыми компилятором в случае отсутствия их явного объявления программистом. Если один из них должен быть определен программистом, то это означает, что версия, сгенерированная компилятором, не удовлетворяет потребностям класса в одном случае и, вероятно, не удовлетворит в остальных случаях.
Поправка к этому правилу заключается в том, что если используется RAII (от англ. Resource Acquisition Is Initialization), то используемый деструктор может остаться неопределённым (иногда упоминается как "Закон Большой Двойки").
Так как неявно определённые конструкторы и операторы присваивания просто копируют все члены-данные класса, определение явных конструкторов копирования и операторов присваивания копированием необходимо в случаях, когда класс инкапсулирует сложные структуры данных или может поддерживать эксклюзивный доступ к ресурсам. А также в случаях, когда класс содержит константные данные или ссылки.
Сам "Закон Большой Двойки" подробно рассматривается в этой статье: The Law of The Big Two.
Как видите, "Закон Большой Двойки" очень важен и поэтому мы реализовали соответствующую диагностику в анализаторе кода.
Начиная с C++11 появилась семантика перемещения, поэтому это правило расширилось до "Большой пятерки". Список методов, которые нужно определить все, если определён, хотя бы один из них:
Поэтому всё, что справедливо для конструктора/оператора копирования, справедливо и для конструктора/оператора перемещения.
Всегда ли диагностика V690 говорит о наличии проблемы? Нет. Иногда никакой ошибки нет, но есть лишняя функция. Рассмотрим пример из реального приложения:
struct wdiff { int start[2]; int end[2]; wdiff(int s1=0, int e1=0, int s2=0, int e2=0) { if (s1>e1) e1=s1-1; if (s2>e2) e2=s2-1; start[0] = s1; start[1] = s2; end[0] = e1; end[1] = e2; } wdiff(const wdiff & src) { for (int i=0; i<2; ++i) { start[i] = src.start[i]; end[i] = src.end[i]; } } };
В этом классе есть конструктор копирования, но нет оператора присваивания. Но это не страшно. Массивы 'start' и 'end' состоят из простых типов 'int'. Они будут корректно скопированы компилятором. Что-бы устранить предупреждение V690 нужно удалить бессмысленный конструктор копирования. Компилятор построит код, копирует элементы класса не медленней, а возможно даже быстрее.
Исправленный вариант:
struct wdiff { int start[2]; int end[2]; wdiff(int s1=0, int e1=0, int s2=0, int e2=0) { if (s1>e1) e1=s1-1; if (s2>e2) e2=s2-1; start[0] = s1; start[1] = s2; end[0] = e1; end[1] = e2; } };
Взгляните на примеры ошибок, обнаруженных с помощью диагностики V690. |