一部のWindows APIコードと相互運用するクラスを作成していますが、初期化する必要があるポインターの1つは、それを初期化するネイティブ関数を呼び出すことによって実行されます。
私のポインターは、提供されているWinAPI削除関数を呼び出すカスタム削除機能付きのstd::unique_ptr
タイプですが、unique_ptrを&address-of演算子と共に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**)'
隠されたどこかで、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
は、コンストラクターに渡されたものはすべて、最終的には削除機能を使用して削除されることを保証します。
ところで、custom_deleter
はoperator()(foo*)
を必要としませんか?それとも私は何かを逃したのですか?
Steveはすでに技術的な問題について説明していますが、根本的な問題はさらに深くなっています。コードは、ネイキッドポインターを扱うときに役立つイディオムを採用しています。なぜこのコードは最初に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>
_を直接返すようにする方が良いでしょう。また、2ステップの初期化を使用する場合、クラスを使用してデータをラップすることを検討することをお勧めします。
次のトリックを使用できます。
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's 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));
私は最終的に、unique_ptrを次のようなコードで初期化できるアプローチを思いつきました。
struct TOpenSSLDeleter { ... }; // Your custom deleter
std::unique_ptr<EVP_MD_CTX, TOpenSSLDeleter> Ctx;
...
Ctx = MakeUnique(EVP_MD_CTX_create()); // MakeUnique() accepts raw pointer
そしてここに解決策があります:
template <class X>
struct TUniquePtrInitHelper {
TUniquePtrInitHelper(X *Raw) noexcept {
m_Raw = Raw;
}
template <class T, class D>
operator std::unique_ptr<T, D>() const noexcept {
return std::unique_ptr<T, D>(m_Raw);
}
private:
X *m_Raw;
};
template <class X>
TUniquePtrInitHelper<X> MakeUnique(X *Raw) noexcept {
return {Raw};
}