Правильно ли CppCoreGuidelines C.21?

При чтении Bjarne Stroustrup CoreCppGuidelines я нашел руководство, которое противоречит моему опыту.

C.21 требует следующее:

Если вы определяете или =delete любую операцию по умолчанию, определите или =delete все они

По следующей причине:

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

По моему опыту, две наиболее распространенные ситуации переопределения операций по умолчанию следующие:

# 1: определение виртуального деструктора с телом по умолчанию для разрешения наследования:

class C1
{
...
    virtual ~C1() = default;
}

# 2: Определение конструктора по умолчанию, выполняющего некоторую инициализацию элементов, типизированных для RAII:

class C2
{
public:
    int a; float b; std::string c; std::unique_ptr<int> x;

    C2() : a(0), b(1), c("2"), x(std::make_unique<int>(5))
    {}
}

Все другие ситуации были редкостью в моем опыте.

Что вы думаете об этих примерах? Являются ли они исключением из правила C.21 или лучше определить все операции по умолчанию здесь? Существуют ли какие-либо другие частые исключения?

Ответ 1

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

Скажем, у вас есть пользовательский класс, похожий на std::complex<double>, или std::chrono::seconds. Это всего лишь тип значения. Он не имеет никаких ресурсов, он должен быть простым. Предположим, что у него есть конструктор неспециального члена.

class SimpleValue
{
    int value_;
public:
    explicit SimpleValue(int value);
};

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

class SimpleValue
{
    int value_;
public:
    SimpleValue();
    explicit SimpleValue(int value);
};

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

class SimpleValue
{
    int value_;
public:
    ~SimpleValue() = default;
    SimpleValue();
    SimpleValue(const SimpleValue&) = default;
    SimpleValue& operator=(const SimpleValue&) = default;

    explicit SimpleValue(int value);
};

Хм... Мне не нужно перемещать участников, но мне нужно бездумно следовать тому, что сказали мне мудрые, поэтому я просто удалю их:

class SimpleValue
{
    int value_;
public:
    ~SimpleValue() = default;
    SimpleValue();
    SimpleValue(const SimpleValue&) = default;
    SimpleValue& operator=(const SimpleValue&) = default;
    SimpleValue(SimpleValue&&) = delete;
    SimpleValue& operator=(SimpleValue&&) = delete;

    explicit SimpleValue(int value);
};

Я боюсь, что CoreCppGuidelines C.21 приведет к тонне кода, который выглядит именно так. Почему это плохо? Несколько причин:

1. Это гораздо труднее прочитать, чем эта правильная версия:

class SimpleValue
{
    int value_;
public:
    SimpleValue();
    explicit SimpleValue(int value);
};

2. сломан. Вы узнаете, как первый раз вы попытаетесь вернуть SimpleValue из функции по значению:

SimpleValue
make_SimpleValue(int i)
{
    // do some computations with i
    SimpleValue x{i};
    // do some more computations
    return x;
}

Это не будет компилироваться. В сообщении об ошибке будет сказано о доступе к удаленному элементу SimpleValue.

У меня есть несколько лучших рекомендаций:

1. Знайте, когда компилятор выполняет дефолт или удаляет для вас специальные члены, и что будут делать члены, не выполнившие действие по умолчанию.

Эта диаграмма может помочь с этим:

enter image description here

Если эта диаграмма слишком сложная, я понимаю. Это сложно. Но когда вам это объясняется немного, с ним гораздо легче справиться. Я надеюсь, что обновить этот ответ в течение недели со ссылкой на видео, которое я объясняю этой диаграммой. Вот ссылка на объяснение после более длительной задержки, чем мне хотелось бы (мои извинения): https://www.youtube.com/watch?v=vLinb2fgkHk

2. Всегда определять или удалять специальный член, когда неявное действие компилятора неверно.

3. Не зависит от устаревшего поведения (красные квадраты в приведенной выше таблице). Если вы объявляете любого из деструктора, конструктора копирования или оператора присваивания копии, тогда объявляйте как конструктор копирования, так и оператор назначения копирования.

4. Никогда не удаляйте элементы перемещения. Если вы это сделаете, то в лучшем случае это будет излишним. В худшем случае это сломает ваш класс (как в примере выше SimpleValue). Если вы удаляете перемещаемые элементы, и это избыточный случай, то вы вынуждаете своих читателей постоянно просматривать ваш класс, чтобы убедиться, что это не сломанный случай.

5. Нежно заботясь о каждом из 6 специальных членов, даже если результат должен позволить компилятору обработать его для вас (возможно, путем запрета или удаления их неявно).

6. Поместите ваши специальные члены в последовательном порядке вверху вашего класса (только те, которые вы хотите явно объявить), чтобы ваши читатели не могли их искать. У меня есть мой любимый заказ, если ваш предпочтительный порядок отличается, отлично. Мой предпочтительный порядок - это то, что я использовал в примере SimpleValue.

Ответ 2

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

Мне интересно, может ли этот слайд помочь с вашим первым примером:

Специальные участники

Вот слайды: https://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf

EDIT: для получения дополнительной информации о первом случае я с этого момента обнаружил следующее: С++ 11 виртуальных деструкторов и автоматическое создание специальных функций перемещения