Верно ли, что объявление unique_ptr, в отличие от объявления auto_ptr, хорошо определено, когда тип его шаблона имеет неполный тип?
Я написал эту статью и получил некоторые комментарии по этому поводу, которые смутили меня.
В основном это сводится к тому, что я видел, что T2
используется только как параметр шаблона и ошибочно подскочил к выводу, что поэтому я мог бы воспользоваться возможностью объявления вперед:
struct T2;
struct T1
{
std::auto_ptr<T2> obj;
};
Это вызывает UB, если я не буду определять T2
где-то в одном TU, потому что std::auto_ptr<T2>
вызывает delete
на своей внутренней T2*
и вызов delete
указателя на объект неполного типа, полный тип которого имеет нетривиальный деструктор undefined:
[C++11: 5.3.5/5]:
Если удаляемый объект имеет неполный тип класса в точке удаления, а полный класс имеет нетривиальный деструктор или функцию освобождения, поведение undefined.
Инструментарий GCC, с которым я столкнулся, использовал — v4.3.3 (Sourcery g++ Lite 2009q1-203) — был достаточно любезен, чтобы сообщить мне с примечанием:
note: ни деструктор, ни оператор-оператор класса не будут вызваны, даже если они объявлены при определении класса.
хотя, похоже, трудно получить эту диагностику в других версиях GCC.
Моя проблема заключалась в том, что было бы намного легче обнаружить такую ошибку, если бы delete
указатель на экземпляр неполного типа был плохо сформирован, а не UB, но это похоже на неразрешимую проблему для для решения, поэтому я понимаю, почему это UB.
Но потом мне сказали, что если бы я использовал std::unique_ptr<T2>
, это было бы безопасно и совместимо.
n3035 якобы говорит в 20.9.10.2:
Параметр шаблона T
of unique_ptr
может быть неполным.
Все, что я могу найти в С++ 11, это:
[C++11: 20.7.1.1.1]:
/1 Шаблон класса default_delete
используется как дефолт по умолчанию (политика уничтожения) для шаблона класса unique_ptr
.
/2 Параметр шаблона T
of default_delete
может быть неполным.
Но для default_delete
operator()
требуется полный тип:
[C++11: 20.7.1.1.2/4]:
Если T
является неполным, программа плохо сформирована.
Я полагаю, что мой вопрос таков:
Являются ли комментаторы моей статьи правильными, говоря, что единица перевода, состоящая только из следующего кода, хорошо сформирована и четко определена? Или они ошибаются?
struct T2;
struct T1
{
std::unique_ptr<T2> obj;
};
Если они верны, как ожидается, что компилятор выполнит это, учитывая, что есть веские причины для того, чтобы быть UB, по крайней мере, когда используется std::auto_ptr
?
Ответы
Ответ 1
Согласно Herb Sutter в GOTW # 100, unique_ptr
страдает от той же проблемы, что и auto_ptr
по отношению к неполным типам.
... хотя и unique_ptr, и shared_ptr могут быть созданы с помощью неполный тип, destructor unique_ptrs требует полного типа в чтобы вызвать delete...
Его предложение состоит в том, чтобы объявить деструктор вашего содержащего класса (т.е. T1
) в файле заголовка, затем поместить его определение в блок перевода, в котором T2
является полным типом.
// T1.h
struct T2;
struct T1
{
~T1();
std::unique_ptr< T2 >;
};
// T1.cpp
#include "T2.h"
T1::~T1()
{
}
Ответ 2
Следующий пример - попытка продемонстрировать разницу между std::auto_ptr<T>
и std::unique_ptr<T>
. Сначала рассмотрим эту программу, состоящую из 2 исходных файлов и 1 заголовка:
Заголовок:
// test.h
#ifndef TEST_H
#define TEST_H
#include <memory>
template <class T>
using smart_ptr = std::auto_ptr<T>;
struct T2;
struct T1
{
smart_ptr<T2> obj;
T1(T2* p);
};
T2*
source();
#endif // TEST_H
Первый источник:
// test.cpp
#include "test.h"
int main()
{
T1 t1(source());
}
Второй источник:
// test2.cpp
#include "test.h"
#include <iostream>
struct T2
{
~T2() {std::cout << "~T2()\n";}
};
T1::T1(T2* p)
: obj(p)
{
}
T2*
source()
{
return new T2;
}
Эта программа должна компилироваться (она может компилироваться с предупреждением, но она должна компилироваться). Но во время выполнения он демонстрирует поведение undefined. И это, вероятно, не будет выводиться:
~T2()
который указывает, что деструктор T2
не запущен. По крайней мере, это не в моей системе.
Если я изменю test.h на:
template <class T>
using smart_ptr = std::unique_ptr<T>;
Затем компилятор должен вывести диагностику (ошибку).
То есть, когда вы делаете эту ошибку с помощью auto_ptr
, вы получаете ошибку времени выполнения. Когда вы совершаете эту ошибку с помощью unique_ptr
, вы получаете ошибку времени компиляции. И , что - разница между auto_ptr
и unique_ptr
.
Чтобы исправить ошибку времени компиляции, вы должны указать ~T1()
после завершения T2
. В test2.cpp добавьте после T2
:
T1::~T1() = default;
Теперь он должен компилироваться и выводиться:
~T2()
Вероятно, вы захотите также объявить и очертить элементы перемещения:
T1::T1(T1&&) = default;
T1& T1::operator=(T1&&) = default;
Вы можете сделать эти же исправления с помощью auto_ptr
, и это будет снова правильно. Но опять-таки различие между auto_ptr
и unique_ptr
заключается в том, что с первым вы не обнаружите до тех пор, пока не будет выполнено время отладки (по модулю необязательные предупреждения, которые может дать ваш компилятор). С последним вы гарантированно узнаете во время компиляции.