Инициализация std:: unique_ptr путем передачи адреса указателя

Я создаю класс, который перехватывает некоторый код Windows API, теперь один из указателей, который я должен инициализировать, выполняется путем вызова встроенной функции, которая инициализирует его.

Мои указатели имеют тип std::unique_ptr с пользовательским удалением, который вызывает функцию деаэратора WinAPI, однако я не могу передать unique_ptr с оператором и адресом оператора init. Почему?

Я создал образец, демонстрирующий мою проблему:

#include <memory>

struct foo
{
   int x;
};

struct custom_deleter {};

void init_foo(foo** init)
{
  *init = new foo();
}

int main()
{
   std::unique_ptr<foo, custom_deleter> foo_ptr;

   init_foo(&foo_ptr);
}

Компилятор лает и говорит:

source.cpp: In function 'int main()':
source.cpp:19:21: error: cannot convert 'std::unique_ptr<foo, custom_deleter>*' to 'foo**' for argument '1' to 'void init_foo(foo**)'

Ответы

Ответ 1

Где-то под обложками unique_ptr<foo> имеет член данных типа foo*.

Однако для пользователя класса неправомерно изменять этот элемент данных. Это не обязательно сохранит инварианты класса unique_ptr, в частности, это не освободит значение старого указателя (если оно есть). В вашем специальном случае вам не нужно, чтобы это произошло, потому что предыдущее значение равно 0, но в целом это должно произойти.

По этой причине unique_ptr не предоставляет доступ к элементу данных только к копии его значения (через get() и operator->). Вы не можете получить foo** из unique_ptr.

Вместо этого вы можете написать:

foo *tmp;
init_foo(&tmp);
std::unique_ptr<foo, custom_deleter> foo_ptr(tmp);

Это безопасно для исключений по той же причине, что std::unique_ptr<foo, custom_deleter> foo_ptr(new foo()); безопасно для исключений: unique_ptr гарантирует, что все, что вы передадите его конструктору, в конечном итоге будет удалено с использованием деаэтера.

Btw, не требуется custom_deleter a operator()(foo*)? Или я что-то пропустил?

Ответ 2

Стив уже объяснил, какова техническая проблема, однако основная проблема гораздо глубже: в коде используется полезная информация, когда вы имеете дело с голой стрелкой. Почему этот код выполняет двухэтапную инициализацию (сначала сначала создайте объект, а затем инициализируйте его)? Поскольку вы хотите использовать интеллектуальные указатели, я бы предложил вам тщательно адаптировать код:
foo* init_foo()
{
  return new foo();
}

int main()
{
   std::unique_ptr<foo, custom_deleter> foo_ptr( init_foo() );

}

Конечно, переименование init_foo() до create_foo() и с его возвратом std::unique_ptr<foo> было бы лучше. Кроме того, при использовании двухэтапной инициализации часто рекомендуется использовать класс для переноса данных.

Ответ 3

Вы можете использовать следующий трюк:

template<class T>
class ptr_setter
{
public:
    ptr_setter(T& Ptr): m_Ptr{Ptr} {}
    ~ptr_setter() { m_Ptr.reset(m_RawPtr); }

    ptr_setter(const ptr_setter&) = delete;
    ptr_setter& operator=(const ptr_setter&) = delete;

    auto operator&() { return &m_RawPtr; }

private:
    T& m_Ptr;
    typename T::pointer m_RawPtr{};
};


// Macro will not be needed with C++17 class template deduction.
// If you dislike macros (as all normal people should)
// it possible to replace it with a helper function,
// although this would make the code a little more complex.

#define ptr_setter(ptr) ptr_setter<decltype(ptr)>(ptr)

а затем:

std::unique_ptr<foo, custom_deleter> foo_ptr;
init_foo(&ptr_setter(foo_ptr));