Сырой указатель (`T*`) не владеет объектом
Причина
Ни стандарт C++, ни большинство кода не утверждают иного; большинство сырых указателей не владеют объектами. Мы хотим, чтобы указатели-владельцы были идентифицированы — это позволит надёжно и эффективно удалять указуемые ими объекты.
Пример
void f()
{
int* p1 = new int{7}; // плохо: сырой указатель-владелец
auto p2 = make_unique<int>(7); // OK: int принадлежит unique_ptr
// ...
}
unique_ptr защищает от утечек, гарантируя удаление объекта (даже при наличии исключений). T* этого не делает.
Пример
template<typename T>
class X {
public:
T* p; // плохо: непонятно, владеет ли p объектом
T* q; // плохо: непонятно, владеет ли q объектом
// ...
};
Проблему можно решить, сделав владение явным:
template<typename T>
class X2 {
public:
owner<T*> p; // OK: p владеет объектом
T* q; // OK: q не владеет объектом
// ...
};
Исключение
Важным классом исключений является устаревший (legacy) код, особенно тот, который должен компилироваться как C или взаимодействовать с C и C-подобным C++ через ABI. Нельзя игнорировать тот факт, что существуют миллиарды строк кода, нарушающих это правило в отношении владеющих T*. Мы хотели бы видеть инструменты трансформации кода, превращающие 20-летний «легаси-код» в современный, мы поощряем разработку, внедрение и использование таких инструментов, мы надеемся, что эти рекомендации помогут их созданию и даже сами участвуем (и участвовали) в исследованиях и разработках в этой области. Однако это займёт время: «легаси-код» создаётся быстрее, чем мы успеваем обновлять старый.
Весь этот код не может быть переписан (даже при наличии хороших инструментов трансформации), особенно в ближайшем будущем. Проблему нельзя решить (в масштабе) простым преобразованием всех указателей-владельцев в unique_ptr и shared_ptr, отчасти потому, что в реализации наших базовых дескрипторов ресурсов нам нужны как владеющие «сырые» указатели, так и простые. Например, типичные реализации vector содержат один владеющий указатель и два не владеющих. Многие ABI (и практически все интерфейсы к C-коду) используют T*, часть из которых являются владеющими. Некоторые интерфейсы не могут быть просто аннотированы owner, поскольку должны оставаться компилируемыми как C (хотя это был бы редкий хороший случай применения макроса, раскрывающегося в owner только в режиме C++).
Примечание
owner<T*> не имеет семантики по умолчанию, отличной от T*. Его можно использовать без изменения кода и без влияния на ABI. Это просто индикатор для программистов и инструментов анализа. Например, если owner<T*> является членом класса, у этого класса должен быть деструктор, вызывающий delete.
Пример (плохой)
Возврат (сырого) указателя создаёт неопределённость управления временем жизни на стороне вызывающего кода: кто удаляет указуемый объект?
Gadget* make_gadget(int n)
{
auto p = new Gadget{n};
// ...
return p;
}
void caller(int n)
{
auto p = make_gadget(n); // не забудьте удалить p
// ...
delete p;
}
Помимо проблемы утечки, это добавляет лишние операции выделения/освобождения памяти и излишнюю многословность. Если Gadget дёшево перемещается из функции (т.е. мал или имеет эффективную операцию перемещения), просто верните его «по значению» (см. «out»-возвращаемые значения):
Gadget make_gadget(int n)
{
Gadget g{n};
// ...
return g;
}
Примечание
Это правило применяется к фабричным функциям.
Примечание
Если необходима семантика указателя (например, тип возврата должен ссылаться на базовый класс иерархии (интерфейс)), возвращайте «умный указатель».
Контроль
- (Простой) Предупреждать о
deleteсырого указателя, не являющегосяowner<T>. - (Умеренный) Предупреждать о невызове
resetили явногоdeleteдля указателяowner<T>на каждом пути выполнения кода. - (Простой) Предупреждать, если результат
newприсваивается сырому указателю. - (Простой) Предупреждать, если функция возвращает объект, выделенный внутри функции, но имеющий конструктор перемещения. Предлагать рассмотреть возврат по значению.