Что представляет собой допустимое состояние для "перемещенного" объекта в C++11?
Я пытался обдумать, как семантика перемещения в C++11 должна работать, и у меня возникли большие проблемы с пониманием того, какие условия должен выполнять перемещаемый объект. Глядя на ответ здесь, на самом деле не решаю мой вопрос, потому что не могу понять, как применить его к объектам pimpl разумным способом, несмотря на то, что аргументы, которые перемещают семантику, идеально подходят для pimpl.
Самая простая иллюстрация моей проблемы связана с идиомой pimpl, например:
class Foo {
std::unique_ptr<FooImpl> impl_;
public:
// Inlining FooImpl's constructors for brevity's 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
является разумным инвариантом?
2 ответа
Вы определяете и документируете для своих типов, что такое "допустимое" состояние и какие операции можно выполнять над объектами вашего типа, которые были удалены.
Перемещение объекта стандартного библиотечного типа переводит объект в неопределенное состояние, к которому можно обращаться в обычном порядке для определения допустимых операций.
17.6.5.15 Состояние библиотечных типов, отведенное из-под контроля [lib.types.movedfrom]
Объекты типов, определенных в стандартной библиотеке C++, могут быть перемещены из (12.8). Операции перемещения могут быть явно указаны или неявно сгенерированы. Если не указано иное, такие перемещенные объекты должны быть помещены в допустимое, но неопределенное состояние.
Объект, находящийся в "допустимом" состоянии, означает, что все требования, которые стандарт определяет для типа, все еще остаются в силе. Это означает, что вы можете использовать любую операцию над стандартным типом библиотеки, из которой выполнены предварительные условия.
Обычно состояние объекта известно, поэтому вам не нужно проверять, соответствует ли он предварительным условиям для каждой операции, которую вы хотите выполнить. Единственное отличие от перемещенных объектов состоит в том, что вы не знаете состояние, поэтому вам нужно проверить. Например, вы не должны выполнять pop_back() для строки move-from до тех пор, пока вы не запросите состояние строки, чтобы определить, что предварительные условия pop_back() выполнены.
std::string s = "foo";
std::string t(std::move(s));
if (!s.empty()) // empty has no preconditions, so it's 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 становится недопустимой операцией с неопределенным поведением (посредством разыменования нулевого указателя). Язык разработан таким образом, что перемещение происходит только тогда, когда невозможно что-либо сделать с перемещенным объектом, или когда пользователь очень явно и очень явно указал операцию перемещения, поэтому пользователь никогда не должен удивляться перемещенному объекту. с объекта.
Также обратите внимание, что "понятия", определенные стандартной библиотекой, не учитывают перемещенные объекты. Это означает, что для того, чтобы соответствовать требованиям для любой из концепций, определенных стандартной библиотекой, объекты вашего типа, перенесенные из, должны по-прежнему удовлетворять требованиям концепта. Это означает, что если объекты вашего типа не остаются в допустимом состоянии (как определено соответствующей концепцией), то вы не можете использовать его со стандартной библиотекой (или результатом является неопределенное поведение).
Однако, если я попытаюсь сделать do_stuff с моим 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 не может быть пустым, вы должны реализовать правильную семантику копирования и запретить перемещение. Движение требует, чтобы объект находился в пустом состоянии.