テストのために、ゼロ以外のメモリ上にオブジェクトを構築する必要があります。これは次のようにして行うことができます:
{
struct Type { /* IRL not empty */};
std::array<unsigned char, sizeof(Type)> non_zero_memory;
non_zero_memory.fill(0xC5);
auto const& t = *new(non_zero_memory.data()) Type;
// t refers to a valid Type whose initialization has completed.
t.~Type();
}
これは面倒で何度も作成されるため、このようなType
インスタンスへのスマートポインターを返す関数を提供したいと思います。私は次のことを思いつきましたが、未定義の行動がどこかに潜んでいるのを恐れています。
次のプログラムは明確に定義されていますか?特に、std::byte[]
が割り当てられましたが、同等のサイズのType
が問題から解放されますか?
#include <cstddef>
#include <memory>
#include <algorithm>
auto non_zero_memory(std::size_t size)
{
constexpr std::byte non_zero = static_cast<std::byte>(0xC5);
auto memory = std::make_unique<std::byte[]>(size);
std::fill(memory.get(), memory.get()+size, non_zero);
return memory;
}
template <class T>
auto on_non_zero_memory()
{
auto memory = non_zero_memory(sizeof(T));
return std::shared_ptr<T>(new (memory.release()) T());
}
int main()
{
struct Type { unsigned value = 0; ~Type() {} }; // could be something else
auto t = on_non_zero_memory<Type>();
return t->value;
}
このプログラムは明確に定義されていません。
ルールは、型に トリビアルデストラクタ ( this を参照)がある場合、それを呼び出す必要がないということです。したがって、この:
return std::shared_ptr<T>(new (memory.release()) T());
ほぼ正しいです。 sizeof(T)
std::byte
sのデストラクタを省略し、メモリに新しいT
を作成します。これにより、shared_ptr
を削除する準備ができると、delete this->get();
、これは誤りです。これは最初にT
を分解しますが、次にstd::byte[]
ではなくT
の割り当てを解除します。これはおそらく(未定義)が機能しません。
C++標準§8.5.2.4p8[expr.new]
New-expressionは、割り当て関数を呼び出すことにより、オブジェクトのストレージを取得できます。 [...]割り当てられた型が配列型の場合、割り当て関数の名前は
operator new[]
です。
(これらすべての「可能性があります」は、実装が隣接する新しい式をマージして、そのうちの1つに対してのみoperator new[]
を呼び出すことが許可されているためです。ただし、new
は一度だけ発生するため(make_unique
で)
そして、同じセクションのパート11:
New-expressionが割り当て関数を呼び出し、その割り当てが拡張されていない場合、new-expressionはタイプ
std::size_t
の最初の引数として、要求されたスペースの量を割り当て関数に渡します。その引数は、作成されるオブジェクトのサイズ以上でなければなりません。オブジェクトが配列の場合にのみ、作成されるオブジェクトのサイズより大きくなることがあります。char
、unsigned char
、およびstd::byte
の配列の場合、new-expressionの結果と割り当て関数によって返されるアドレスとの差は、次の最も厳しい基本整列要件(6.6.5)の整数倍になります。サイズが作成される配列のサイズ以下のオブジェクトタイプ。 [注:割り当て関数は、基本的な配置で任意のタイプのオブジェクトに適切に配置されたストレージへのポインターを返すと想定されているため、配列割り当てオーバーヘッドに対するこの制約により、後で他のタイプのオブジェクトが配置される文字配列を割り当てる一般的なイディオムが可能になります。 —エンドノート]
§21.6.2[new.delete.array]を読むと、デフォルトのoperator new[]
とoperator delete[]
がoperator new
とoperator delete
とまったく同じことをしていることがわかります。問題は、渡されたサイズがわからないことです。 おそらくdelete ((T*) object)
が(サイズを保存するために)呼び出すよりも多い。
Delete-expressionsが何をするかを見る:
§8.5.2.5p8[expr.delete]
[...] delete-expressionは、[...]のデストラクタ(存在する場合)を呼び出し、削除される配列の要素を呼び出します
p7.1
削除するオブジェクトのnew-expressionの割り当て呼び出しが省略されていない場合[...]、delete-expressionは割り当て解除関数(6.6.4.4.2)を呼び出す必要があります。 new-expressionの割り当て呼び出しから返された値は、最初の引数として割り当て解除関数に渡されます。
std::byte
にはデストラクタがないため、deallocate関数(delete[]
)を呼び出す以外は何もしないので、operator delete[]
を安全に呼び出すことができます。これをstd::byte*
に再解釈するだけで、new[]
が返したものを取得できます。
もう1つの問題は、T
のコンストラクターがスローするとメモリリークが発生することです。単純な修正は、メモリがstd::unique_ptr
によってまだ所有されているときにnew
を配置することです。そのため、スローしても、delete[]
が正しく呼び出されます。
T* ptr = new (memory.get()) T();
memory.release();
return std::shared_ptr<T>(ptr, [](T* ptr) {
ptr->~T();
delete[] reinterpret_cast<std::byte*>(ptr);
});
最初の配置new
は、§[6.6(3p5 [basic]に従って、sizeof(T)
std::byte
sの有効期間を終了し、同じアドレスで新しいT
オブジェクトの有効期間を開始します。生活]
プログラムは、オブジェクトが占有しているストレージを再利用するか、重要なデストラクタでクラス型のオブジェクトのデストラクタを明示的に呼び出すことにより、オブジェクトの寿命を終了させることができます。 [...]
次に、それが削除されているとき、T
の存続期間はデストラクタの明示的な呼び出しによって終了し、上記に従って、delete-expressionはストレージの割り当てを解除します。
これは、次の質問につながります。
ストレージクラスがstd::byte
ではなく、簡単に破壊できなかった場合はどうなりますか?たとえば、ストレージとして重要な共用体を使用していました。
delete[] reinterpret_cast<T*>(ptr)
を呼び出すと、オブジェクトではない何かに対してデストラクタが呼び出されます。これは明らかに未定義の動作であり、§6.6.3p6[basic.life]に従っています
オブジェクトの存続期間が開始する前、ただしオブジェクトが占有するストレージが割り当てられた後[...]、オブジェクトが配置される、または配置されたストレージの場所のアドレスを表すポインターは、限られた方法。 [...]次の場合、プログラムは未定義の動作をします。オブジェクトが自明でないデストラクタを持つクラス型であるか、クラス型であり、ポインタが削除式のオペランドとして使用されている
したがって、上記のように使用するには、再度破壊するためだけに構築する必要があります。
デフォルトのコンストラクタはおそらく正常に動作します。通常のセマンティクスは「破壊できるオブジェクトを作成する」であり、これがまさに私たちが望むものです。 std::uninitialized_default_construct_n
を使用してすべてを構築し、すぐに破棄します。
// Assuming we called `new StorageClass[n]` to allocate
ptr->~T();
auto* as_storage = reinterpret_cast<StorageClass*>(ptr);
std::uninitialized_default_construct_n(as_storage, n);
delete[] as_storage;
operator new
およびoperator delete
を自分で呼び出すこともできます。
static void byte_deleter(std::byte* ptr) {
return ::operator delete(reinterpret_cast<void*>(ptr));
}
auto non_zero_memory(std::size_t size)
{
constexpr std::byte non_zero = static_cast<std::byte>(0xC5);
auto memory = std::unique_ptr<std::byte, void(*)(std::byte*)>(
reinterpret_cast<std::byte*>(::operator new(size)),
&::byte_deleter
);
std::fill(memory.get(), memory.get()+size, non_zero);
return memory;
}
template <class T>
auto on_non_zero_memory()
{
auto memory = non_zero_memory(sizeof(T));
T* ptr = new (memory.get()) T();
memory.release();
return std::shared_ptr<T>(ptr, [](T* ptr) {
ptr->~T();
::operator delete(ptr, sizeof(T));
// ^~~~~~~~~ optional
});
}
しかし、これはstd::malloc
およびstd::free
によく似ています。
3番目のソリューションは、_ std::aligned_storage
をnew
に指定されたタイプとして使用し、アラインメントされたストレージが自明な集計であるため、std::byte
と同様に削除機能を使用することです。
_std::shared_ptr<T>(new (memory.release()) T())
_
未定義の動作です。 memory
によって取得されたメモリは_std::byte[]
_用でしたが、_shared_ptr
_の削除機能はdelete
へのポインターでT
を呼び出すために実行しています。ポインターが同じ型ではなくなったので、 [expr.delete]/2 でポインターを削除することはできません。
単一オブジェクトの削除式では、deleteのオペランドの値は、nullポインター値、前のnew-expressionによって作成された非配列オブジェクトへのポインター、またはそのような基本クラスを表すサブオブジェクトへのポインターです。オブジェクト。そうでない場合、動作は未定義です。
_shared_ptr
_に、T
を破棄し、ポインターをソースタイプにキャストして_delete[]
_を呼び出すカスタム削除機能を提供する必要があります。
また、memory
が自明でない破壊を持つ型を割り当てた場合、new (memory.release()) T()
自体は未定義になることにも注意してください。メモリを再利用する前に、まずポインタのデストラクタをmemory.release()
から呼び出す必要があります。