Подтвердить что ты не робот

Почему неявно и явно удаленные конструкторы перемещения обрабатываются по-разному?

Что такое обоснование за разным рассмотрением неявно и явно удаленных конструкторов перемещения в стандарте С++ 11 в отношении неявной генерации конструкторов перемещения содержащих/наследующих классов?

Делает ли С++ 14/С++ 17 что-либо менять? (За исключением DR1402 в С++ 14)

Примечание: я понимаю, что происходит, я понимаю, что это согласно стандартным правилам С++ 11, меня интересует обоснование этих правил, которые подразумевают это поведение (пожалуйста, не просто повторите, что это так оно и есть, потому что стандарт говорит так).


Предположим, что класс ExplicitDelete с явно удаленным движением ctor и явно дефолтной копией ctor. Этот класс не является move constructible, хотя совместимая копия ctor доступна, потому что разрешение перегрузки выбирает конструктор перемещения и выходит из строя во время компиляции из-за его удаления.

Предположим, что класс ImplicitDelete содержит или наследует от ExplicitDelete и ничего не делает. Этот класс будет иметь свое движение, которое неявно объявляется удаленным из-за С++ 11 move ctor rules. Однако этот класс по-прежнему будет move constructible через его копию ctor. (Это последнее утверждение связано с разрешением DR1402?)

Тогда класс Implicit, содержащий/наследующий от ImplicitDelete, будет иметь совершенно прекрасный неявный конструктор перемещения, сгенерированный, который вызывает ImplicitDelete copy ctor.

Итак, в чем же причина, позволяющая Implicit иметь возможность двигаться неявно и ImplicitDelete не иметь возможности двигаться неявно?

На практике, если Implicit и ImplicitDelete имеют некоторые сверхмощные подвижные элементы (думаю, vector<string>), я не вижу причин, чтобы Implicit был значительно превосходен для ImplicitDelete в движении. ImplicitDelete все еще может копировать ExplicitDelete из своего неявного перемещения ctor — точно так же, как Implicit делает с ImplicitDelete.


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

  • Компилятор обрабатывает как неявно, так и явно удаленные переменные перемещения:

    • ImplicitDelete становится не move-constructible, как ExplicitDelete
    • ImplicitDelete удаленное перемещение ctor приводит к удаленному неявному перемещению ctor в Implicit (таким же образом, что ExplicitDelete делает это с ImplicitDelete)
    • Implicit становится не move-constructible
    • Компиляция строки std::move полностью не работает в моем примере кода
  • Или компилятор возвращается к копированию ctor также для ExplicitDelete:

    • ExplicitDelete Конструктор копирования вызывается во всех move s, как и для ImplicitDelete
    • ImplicitDelete получает правильное неявное перемещение ctor
    • (Implicit в этом сценарии не изменяется)
    • Вывод образца кода указывает, что элемент Explicit всегда перемещается.

Вот полный рабочий пример:

#include <utility>
#include <iostream>
using namespace std;

struct Explicit {
    // prints whether the containing class move or copy constructor was called
    // in practice this would be the expensive vector<string>
    string owner;
    Explicit(string owner) : owner(owner) {};
    Explicit(const Explicit& o) { cout << o.owner << " is actually copying\n"; }
    Explicit(Explicit&& o) noexcept { cout << o.owner << " is moving\n"; }
};
struct ExplicitDelete {
    ExplicitDelete() = default;
    ExplicitDelete(const ExplicitDelete&) = default;
    ExplicitDelete(ExplicitDelete&&) noexcept = delete;
};
struct ImplicitDelete : ExplicitDelete {
    Explicit exp{"ImplicitDelete"};
};
struct Implicit : ImplicitDelete {
    Explicit exp{"Implicit"};
};

int main() {
    ImplicitDelete id1;
    ImplicitDelete id2(move(id1)); // expect copy call
    Implicit i1;
    Implicit i2(move(i1)); // expect 1x ImplicitDelete copy and 1x Implicit move
    return 0;
}
4b9b3361

Ответ 1

Итак, в чем же причина, по которой Implicit может двигаться неявно и ImplicitDelete не может двигаться неявно?

Обоснование будет таким: случай, который вы описываете, не имеет смысла.

Смотрите, все это началось из-за ExplicitDelete. По вашему определению этот класс имеет явно удаленный конструктор перемещения, но конструктор копии по умолчанию.

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

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

Вот три факта, как я вижу:

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

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

  • Если тип можно скопировать, его можно перенести. Вот почему существует правило о неявно удаленных конструкторах перемещения, не участвующих в разрешении перегрузки. Следовательно, движение является правильным подмножеством копирования.

Поведение С++ в этом случае противоречиво, потому что ваш код противоречив. Вы хотите, чтобы ваш тип был скопирован, но не был перемещаемым; С++ не позволяет этого, поэтому он ведет себя странно.

Посмотрите, что происходит, когда вы удаляете противоречие. Если вы явно удаляете конструктор копирования в ExplicitDelete, все имеет смысл снова. Конструкторы copy/move ImplicitDelete неявно удаляются, поэтому они неподвижны. И конструкторы Implicit copy/move неявно удаляются, поэтому он тоже неподвижен.

Если вы пишете противоречивый код, С++ не будет вести себя совершенно законным образом.