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

Что представляет собой допустимое состояние для объекта "перемещено из" в С++ 11?

Я пытаюсь оборачивать голову тем, как должны работать семантика перемещения на С++ 11, и у меня возникают проблемы с пониманием того, какие условия должен удовлетворять перемещаемый объект. Глядя на ответ здесь, на самом деле не разрешает мой вопрос, потому что не может понять, как применить его к объектам pimpl разумным образом, несмотря на аргументы, что перемещение семантики идеально подходит для pimpls.

Самая простая иллюстрация моей проблемы связана с идиомой pimpl, например:

class Foo {
    std::unique_ptr<FooImpl> impl_;
public:
    // Inlining FooImpl constructors for brevity sake; otherwise it 
    // defeats the point.
    Foo() : impl_(new FooImpl()) {}

    Foo(const Foo & rhs) : impl_(new FooImpl(*rhs.impl_)) {}

    Foo(Foo && rhs) : impl_(std::move(rhs.impl_)) {}

    Foo & operator=(Foo rhs) 
    {
        std::swap(impl_, rhs.impl_);

        return *this;
    }

    void do_stuff () 
    {
        impl_->do_stuff;
    }
};

Теперь, что я могу сделать, как только перейду с Foo? Я могу безопасно уничтожить перемещенный объект, и я могу назначить ему, оба из которых абсолютно необходимы. Однако, если я попытаюсь do_stuff с моим Foo, он взорвется. Прежде чем я добавил семантику перемещения для моего определения Foo, каждый Foo удовлетворял инварианту, что он мог бы do_stuff, и это уже не так. Похоже, что не существует большого количества альтернатив, так как (например), когда переносимый из Foo будет включать новое динамическое распределение, которое частично побеждает цель семантики перемещения. Я мог бы проверить, есть ли impl_ в do_stuff и инициализировать его по умолчанию FooImpl, если он есть, но добавляет (обычно ложную) проверку, и если у меня много методов, это означало бы запоминание делать проверку в каждом.

Должен ли я просто отказаться от идеи, что возможность do_stuff является разумным инвариантом?

4b9b3361

Ответ 1

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

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

17.6.5.15 Перемещение из состояния типов библиотек                                               [Lib.types.movedfrom]

Объекты типов, определенные в стандартной библиотеке С++, могут быть перемещены из (12,8). Операции перемещения могут быть явно указаны или неявно генерироваться. Если не указано иное, такие перемещенные объекты должны быть помещенным в действительное, но неуказанное состояние.

Объект, находящийся в состоянии "valid", означает, что все требования, которые стандарт указывает для типа, по-прежнему сохраняются. Это означает, что вы можете использовать любую операцию в перемещенном из стандартного типа библиотеки, для которого предпосылки сохраняются.

Обычно состояние объекта известно, поэтому вам не нужно проверять, соответствует ли он предварительным условиям для каждой операции, которую вы хотите выполнить. Единственное отличие от перемещенных объектов состоит в том, что вы не знаете состояние, поэтому вам нужно проверить. Например, вы не должны pop_back() в строке с перетаскиванием до тех пор, пока вы не запросите состояние строки, чтобы определить, что выполнены предварительные условия pop_back().

std::string s = "foo";
std::string t(std::move(s));
if (!s.empty()) // empty has no preconditions, so it safe to call on moved-from objects
    s.pop_back(); // after verifying that the preconditions are met, pop_back is safe to call on moved-from objects

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


Поскольку вы несете ответственность не только за спецификацию, но и за реализацию своих типов, вы можете просто указать состояние и устранить необходимость в запросе. Например, было бы совершенно разумно указать, что переход из вашего объекта типа pimpl приводит к тому, что do_stuff становится недопустимой операцией с поведением undefined (путем разыменования нулевого указателя). Язык разработан таким образом, что перемещение происходит только тогда, когда невозможно сделать что-либо с перемещенным объектом или когда пользователь явно и явно указал операцию перемещения, поэтому пользователь никогда не должен удивляться перемещенному объекту, из объекта.


Также обратите внимание, что "понятия", определенные стандартной библиотекой, не дают никаких ограничений для перемещенных объектов. Это означает, что для удовлетворения требований к любой из концепций, определенных стандартной библиотекой, перемещенные объекты из ваших типов должны по-прежнему выполнять требования концепции. Это означает, что если объекты вашего типа не остаются в допустимом состоянии (как определено соответствующей концепцией), вы не можете использовать его со стандартной библиотекой (или результатом является поведение undefined).

Ответ 2

Однако, если я попытаюсь сделать_файл с моим Foo, он взорвется.

Да. Так и будет:

vector<int> first = {3, 5, 6};
vector<int> second = std::move(first);
first.size();  //Value returned is undefined. May be 0, may not

Правило, которое использует стандарт, заключается в том, чтобы оставить объект в действительном (что означает, что объект работает), но неопределенное состояние. Это означает, что единственными функциями, которые вы можете вызвать, являются те, которые не имеют условий для текущего состояния объекта. Для vector вы можете использовать операторы присваивания копирования/перемещения, а также clear и empty и несколько других операций. Итак, вы можете сделать это:

vector<int> first = {3, 5, 6};
vector<int> second = std::move(first);
first.clear();  //Cause the vector to become empty.
first.size(); //Now the value is guaranteed to be 0.

В вашем случае копирование/перемещение (с обеих сторон) должно работать, как и деструктор. Но все другие ваши функции имеют предварительное условие, основанное на состоянии, из которого не было перемещено.

Поэтому я не вижу вашей проблемы.

Если вы хотите убедиться, что ни один экземпляр класса Pimpl'd не может быть пустым, вы бы использовали правильную семантику копирования и запретили перемещение. Для перемещения требуется, чтобы объект находился в пустом состоянии.