Правило 5 - использовать это или нет?

20

Правило 3 ( правило 5 в новом стандарте c ++) гласит:

Если вам нужно явно объявить деструктор, конструктор копирования или оператор копирования, вы, вероятно, должны явно объявить все три из них.

Но, с другой стороны, « чистый код » Мартина советует удалить все пустые конструкторы и деструкторы (стр. 293, G12: Clutter ):

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

Итак, как справиться с этими двумя противоположными мнениями? Должны ли быть реализованы пустые конструкторы / деструкторы?


Следующий пример демонстрирует именно то, что я имею в виду:

#include <iostream>
#include <memory>

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    ~A(){}
    A( const A & other ) : v( new int( *other.v ) ) {}
    A& operator=( const A & other )
    {
        v.reset( new int( *other.v ) );
        return *this;
    }

    std::auto_ptr< int > v;
};
int main()
{
    const A a( 55 );
    std::cout<< "a value = " << *a.v << std::endl;
    A b(a);
    std::cout<< "b value = " << *b.v << std::endl;
    const A c(11);
    std::cout<< "c value = " << *c.v << std::endl;
    b = c;
    std::cout<< "b new value = " << *b.v << std::endl;
}

Компилируется нормально, используя g ++ 4.6.1 с:

g++ -std=c++0x -Wall -Wextra -pedantic example.cpp

Деструктор для struct Aпуст и не очень нужен. Итак, он должен быть там, или он должен быть удален?

BЈовић
источник
15
2 цитаты говорят о разных вещах. Или я полностью скучаю по тебе.
Бенджамин Банье
1
@honk В стандарте кодирования моей команды у нас есть правило всегда объявлять все 4 (конструктор, деструктор, конструкторы копирования). Мне было интересно, если это действительно имеет смысл сделать. Должен ли я всегда объявлять деструкторы, даже если они пусты?
BЈовић
Что касается пустых дескторов, подумайте об этом: codesynthesis.com/~boris/blog/2012/04/04/… . Иначе правило 3 (5) имеет для меня смысл, не знаю, зачем нужно правило 4.
Бенджамин Банье
@honk Следите за информацией, которую вы найдете в сети. Не все верно. Например, virtual ~base () = default;не компилируется (по уважительной причине)
BЈовић
@VJovic, нет, вам не нужно объявлять пустой деструктор, если только вам не нужно сделать его виртуальным. И пока мы находимся на эту тему, вы не должны использовать auto_ptrлибо.
Дима

Ответы:

44

Для начала правило говорит «вероятно», поэтому оно не всегда применимо.

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

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

Что касается вашего примера: в таком случае вы можете не использовать деструктор. Это ничего не делает, очевидно. Использование умных указателей является прекрасным примером того, где и почему правило 3 не выполняется.

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

Торстен Мюллер
источник
При использовании умных указателей деструкторы в большинстве случаев пусты (я бы сказал, что> 99% деструкторов в моей базе кода пустые, потому что почти каждый класс использует идиому pimpl).
BЈовић
Вау, это так много прыщей, что я бы назвал это вонючим. Со многими компиляторами будет сложнее оптимизировать pimpled (например, сложнее встроить).
Бенджамин Баннье
@honk Что вы подразумеваете под "много прыщей компиляторов"? :)
BЈовић
@VJovic: извините, опечатка: «код с прыщами»
Бенджамин Банье
4

Здесь действительно нет никакого противоречия. Правило 3 говорит о деструкторе, конструкторе копирования и операторе присваивания копии. Дядя Боб говорит о пустых конструкторах по умолчанию.

Если вам нужен деструктор, то ваш класс, вероятно, содержит указатели на динамически распределенную память, и вы, вероятно, хотите иметь ctor для копирования и a, operator=()которые делают глубокое копирование. Это полностью ортогонально тому, нужен ли вам конструктор по умолчанию.

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

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

Дима
источник
2

Здесь у вашего потенциального (*) эквивалента одному конструктору / присваиванию / деструктору по умолчанию есть цель: задокументировать тот факт, что у вас есть об этой проблеме, и определить, что поведение по умолчанию было правильным. Кстати, в C ++ 11 все еще недостаточно стабилизировалось, чтобы понять, =defaultможет ли это послужить этой цели.

(Существует еще одна потенциальная цель: предоставить внешнее определение вместо встроенного по умолчанию, лучше явное документирование, если у вас есть для этого основания).

(*) Потенциал, потому что я не помню случая из реальной жизни, когда правило трех не применимо, если я должен был что-то делать в одном, я должен был что-то делать в других.


Изменить после добавления примера. Ваш пример использования auto_ptr интересен. Вы используете умный указатель, но не тот, который подходит для работы. Я предпочел бы написать тот, который - особенно если ситуация возникает часто - чем делать то, что вы сделали. (Если я не ошибаюсь, ни стандарт, ни надстройка не обеспечивают).

AProgrammer
источник
Пример демонстрирует мою точку зрения. Деструктор на самом деле не нужен, но правило 3 говорит, что оно должно быть там.
BЈовић
1

Правило 5 - это причинно-следственная связь с правилом 3, которое заключается в каутативном поведении и возможном неправильном использовании объекта.

Если вам нужен деструктор, это означает, что вы выполнили «управление ресурсами», отличное от значения по умолчанию (просто создавайте и уничтожайте значения ).

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

Тем не менее, C ++ удаляет копию, если вы определяете движение, и удаляет движение, если вы определяете копию. В большинстве случаев вы должны определить, хотите ли вы эмулировать значение (следовательно, копировать mut, клонировать ресурс, и перемещать не имеет смысла) или менеджера ресурсов (и, следовательно, перемещать ресурс, где копирование не имеет смысла: правило из 3 становится правилом другого 3 )

Случаи, когда вам нужно определить и копировать, и переместить (правило 5), довольно редки: обычно у вас есть «большое значение», которое нужно скопировать, если оно дано отдельным объектам, но его можно переместить, если оно взято из временного объекта (избегая клон затем уничтожить ). Это касается контейнеров STL или арифметических контейнеров.

Случай может быть матрицами: они должны поддерживать копирование, потому что они являются значениями ( a=b; c=b; a*=2; b*=3;не должны влиять друг на друга), но их можно оптимизировать, поддерживая также перемещение ( a = 3*b+4*cимеет значение, +которое принимает два временных значения и генерирует временное: избежать клонирования и удаления можно полезно)

Эмилио Гаравалья
источник
1

Я предпочитаю другую формулировку правила трех, которая кажется более разумной: «если вашему классу нужен деструктор (отличный от пустого виртуального деструктора), ему, вероятно, также понадобятся конструктор копирования и оператор присваивания».

Указание его как односторонних отношений от деструктора проясняет несколько вещей:

  1. Он не применяется в тех случаях, когда вы предоставляете конструктор копирования не по умолчанию или оператор присваивания только в качестве оптимизации.

  2. Причина этого правила в том, что конструктор копирования по умолчанию или оператор присваивания могут испортить ручное управление ресурсами. Если вы управляете ресурсами вручную, вы, вероятно, поняли, что вам нужен деструктор для их освобождения.

Жюль
источник
-3

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

struct A
{
    A( const int value ) : v( new int( value ) ) {}
    virtual ~A(){}
    ...
}

Конструктор должен быть объявлен как виртуальный в базовом классе, чтобы сделать его виртуальным во всех производных классах. Таким образом, даже если вашему базовому классу не нужен деструктор, вы в конечном итоге объявляете и реализуете пустой деструктор.

Если вы включите все предупреждения (-Wall -Wextra -Weffc ++), g ++ предупредит вас об этом. Я считаю хорошей практикой всегда объявлять виртуальный деструктор в любом классе, потому что вы никогда не знаете, станет ли ваш класс базовым классом. Если виртуальный деструктор не нужен, он не причиняет вреда. Если это так, вы экономите время, чтобы найти ошибку.

Lexi
источник
1
Но я не хочу виртуальный конструктор. Если я сделаю это, то каждый вызов любого метода будет использовать виртуальную диспетчеризацию. Кстати, обратите внимание, что в C ++ нет такого понятия, как «виртуальный конструктор». Также я скомпилировал пример с очень высоким уровнем предупреждения.
BЈовић
IIRC, правило, которое gcc использует для своего предупреждения, и правило, которое я в общем и целом придерживаюсь, заключается в том, что в классе должен быть виртуальный деструктор, если есть какие-либо другие виртуальные методы.
Жюль