Создание unique_ptr <Base>, когда базовый класс имеет защищенный деструктор - PullRequest
3 голосов
/ 11 января 2020
class Base {
public:
    Base() {}
    virtual void print()const = 0;
protected:
    virtual ~Base() { std::cout << "Base destructor\n\n"; }
};

int main()
{
    //std::vector<std::unique_ptr<Base>> v1; 
    //The line above won't compile because: 'Base::~Base': cannot access protected member declared in class 'Base'
    std::vector<std::shared_ptr<Base>> v2;
    return 0;
}

Что пытается вызвать деструктор, когда я создаю вектор? Почему он не скомпилируется для вектора unique_ptr, но скомпилируется для вектора shared_ptr?

Ответы [ 2 ]

3 голосов
/ 11 января 2020

Локальные переменные v1 и v2 имеют автоматическое c время хранения и будут автоматически уничтожены, когда они go выйдут из области видимости. std::vector здесь не имеет значения: внутри vector::~vector() компилятор сгенерирует код для деструкторов элементов. Даже если вектор всегда пуст (это свойство времени выполнения!), Этот код все равно нужно сгенерировать. Итак, давайте упростим код:

std::unique_ptr<Base> v1;
std::shared_ptr<Base> v2;

Когда v1 выходит из области видимости, его нужно уничтожить. Деструктор, сгенерированный компилятором, сводится к (*):

~unique_ptr() {
    delete ptr;
}

Чтобы сгенерировать код для delete ptr, компилятору необходим доступный деструктор. Он защищен, поэтому компиляция не удалась.

Теперь давайте посмотрим на v2. Компилятор также должен генерировать деструктор. Но shared_ptr имеет удаленное по типу средство удаления для управляемого объекта. Это означает, что деструктор управляемого объекта будет вызываться косвенно - через виртуальную функцию:

struct shared_ptr_deleter_base {
    virtual void destroy() = 0;
    virtual ~shared_ptr_deleter_base() = default;
};

~shared_ptr() {
    // member shared_ptr::deleter has type shared_ptr_deleter_base*
    if (deleter)
        deleter->destroy();
}

Чтобы сгенерировать код для deleter->destroy(), вам вообще не нужен доступ к Base::~Base(). Конструктор по умолчанию shared_ptr просто устанавливает deleter в нулевой указатель:

shared_ptr() {
    deleter = nullptr;
}

Вот почему std::shared_ptr<Base> v2; компилируется: не только Base::~Base() не вызывается во время выполнения, ни один вызов не когда-либо созданный компилятором во время компиляции.

Давайте рассмотрим эту строку:

std::shared_ptr<Base> v2(new Base());

Теперь вызывается следующий конструктор (обратите внимание, что это шаблон с отдельным параметром U может отличаться от T в shared_ptr<T>):

template<class U>
shared_ptr(U* ptr) {
    deleter = new shared_ptr_deleter<U>(ptr);
}

Здесь shared_ptr_deleter - это конкретный класс, производный от shared_ptr_deleter_base:

template<class T>
struct shared_ptr_deleter : shared_ptr_deleter_base {
    T* ptr;
    shared_ptr_deleter(T* p) : ptr(p) {}

    virtual void destroy() {
        delete ptr;
    }
};

Для генерации кода для конструктора, принимающего new Base(), компилятор должен сгенерировать код для shared_ptr_deleter<Base>::destroy(). Теперь это терпит неудачу, потому что Base::~Base() недоступен.

(*) Я представляю только упрощенные определения, просто чтобы продемонстрировать основные идеи c, не вдаваясь во все детали, которые не имеют отношения к пониманию рассматриваемой проблемы.

1 голос
/ 11 января 2020

std::unique_ptr не может получить доступ к деструктору Base, потому что это protected. std::shared_ptr использует средство удаления полиморфи c, поэтому std::shared_ptr требуется доступ к деструктору Base только при создании нового std::shared_ptr.

// this fails because the destructor of Base is inaccessible
std::unique_ptr<Base> a;

// this is ok because the destructor isn't required to instantiate the type
std::shared_ptr<Base> b;

// this fails because make_shared needs the destructor
std::shared_ptr<Base> c = std::make_shared<Base>();

"Polymorphi c deleteter" означает, что std::shared_ptr хранит указатель на функцию, которая уничтожает объект. std::unique_ptr использует "stati c deleter", который уничтожает объект напрямую. Вот некоторый псевдокод:

struct shared_ptr {
  ~shared_ptr() {
     deleter();
  }

  void (*deleter)(); // pointer to function that destroys the object
};

// shared_ptr doesn't try to call the destructor directly so we don't need access
// so this is ok
shared_ptr a;

shared_ptr make_shared() {
  // here we generate (with templates) a function that calls Base::~Base
  // then we set "deleter" to point to that function
  // the destructor has to be accessible for us to do this
}
// so we get an error here
shared_ptr b = make_shared();

struct unique_ptr {
  ~unique_ptr() {
    // unique_ptr calls the Base destructor directly
    // unique_ptr needs access to the destructor to instantiate the type
  }
};

// so we get an error here
unique_ptr c;

В вашей ситуации Base оказывается абстрактным, поэтому вы можете использовать std::shared_ptr<Base>, потому что вам никогда не потребуется писать std::make_shared<Base>(). Пока подклассы Base имеют public деструкторов, std::make_shared будет иметь доступ к ним без ошибок.

...