Когда я давно изучал C ++, мне было настоятельно подчеркнуто, что отчасти C ++ состоит в том, что, как и у циклов, есть «инварианты цикла», у классов также есть инварианты, связанные с временем жизни объекта - вещи, которые должны быть истинными. пока объект жив. Вещи, которые должны быть установлены конструкторами и сохранены методами. Инкапсуляция / контроль доступа помогут вам обеспечить соблюдение инвариантов. RAII - это то, что вы можете сделать с этой идеей.
Начиная с C ++ 11 у нас теперь есть семантика перемещения. Для класса, который поддерживает перемещение, перемещение от объекта формально не заканчивает его время жизни - движение должно оставить его в каком-то «допустимом» состоянии.
При разработке класса, является ли плохой практикой, если вы разрабатываете его так, что инварианты класса сохраняются только до точки, из которой он перемещается? Или это нормально, если это позволит вам сделать это быстрее.
Для конкретности предположим, что у меня есть не копируемый, но перемещаемый тип ресурса, например:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
И по какой-то причине мне нужно сделать копируемую обертку для этого объекта, чтобы ее можно было использовать, возможно, в какой-то существующей диспетчерской системе.
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { o_->mysterious(); }
void operator()(int i) { o_->mysterious(i); }
void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};
В этом copyable_opaque
объекте инвариант класса, созданного при конструировании, состоит в том, что член o_
всегда указывает на допустимый объект, поскольку нет ctor по умолчанию, и единственный ctor, который не является ctor-копией, гарантирует это. Все operator()
методы предполагают, что этот инвариант выполняется, и сохраняют его впоследствии.
Тем не менее, если объект перемещен из, то o_
будет указывать ни на что. И после этого, вызов любого из методов operator()
вызовет сбой UB.
Если объект никогда не перемещается, то инвариант будет сохранен вплоть до вызова dtor.
Давайте предположим, что гипотетически я написал этот класс, и несколько месяцев спустя мой воображаемый коллега испытал UB, потому что в какой-то сложной функции, когда по каким-то причинам было перемешано множество этих объектов, он перешел из одной из этих вещей и позже назвал одну из его методы. В конце концов, это его вина, но этот класс "плохо спроектирован?"
Мысли:
В C ++ обычно плохо создавать объекты-зомби, которые взрываются, если вы к ним прикасаетесь.
Если вы не можете сконструировать какой-либо объект, не можете установить инварианты, тогда выведите исключение из ctor. Если вы не можете сохранить инварианты в каком-либо методе, то как-то сообщите об ошибке и выполните откат. Должно ли это быть другим для перемещенных объектов?Достаточно ли просто документировать «после того, как этот объект был перемещен, незаконно (UB) делать с ним что-либо кроме уничтожения» в заголовке?
Лучше ли постоянно утверждать, что он действителен при каждом вызове метода?
Вот так:
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { assert(o_); o_->mysterious(); }
void operator()(int i) { assert(o_); o_->mysterious(i); }
void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};
Утверждения существенно не улучшают поведение и вызывают замедление. Если ваш проект использует схему «Release build / debug build», а не просто всегда работает с утверждениями, я думаю, это более привлекательно, поскольку вы не платите за проверки в сборке релиза. Если на самом деле у вас нет отладочных сборок, это кажется довольно непривлекательным.
- Лучше ли сделать класс копируемым, но не подвижным?
Это также кажется плохим и приводит к снижению производительности, но решает проблему «инвариантов» простым способом.
Что бы вы посчитали уместными здесь «лучшими практиками»?