web-dev-qa-db-ja.com

スマートポインターによって管理されるメモリ上に配置を新しくしても問題ありませんか?

環境

テストのために、ゼロ以外のメモリ上にオブジェクトを構築する必要があります。これは次のようにして行うことができます:

{
    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;
}

ライブデモ

22
YSC

このプログラムは明確に定義されていません。

ルールは、型に トリビアルデストラクタthis を参照)がある場合、それを呼び出す必要がないということです。したがって、この:

return std::shared_ptr<T>(new (memory.release()) T());

ほぼ正しいです。 sizeof(T)std::bytesのデストラクタを省略し、メモリに新しい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の最初の引数として、要求されたスペースの量を割り当て関数に渡します。その引数は、作成されるオブジェクトのサイズ以上でなければなりません。オブジェクトが配列の場合にのみ、作成されるオブジェクトのサイズより大きくなることがあります。 charunsigned char、およびstd::byteの配列の場合、new-expressionの結果と割り当て関数によって返されるアドレスとの差は、次の最も厳しい基本整列要件(6.6.5)の整数倍になります。サイズが作成される配列のサイズ以下のオブジェクトタイプ。 [注:割り当て関数は、基本的な配置で任意のタイプのオブジェクトに適切に配置されたストレージへのポインターを返すと想定されているため、配列割り当てオーバーヘッドに対するこの制約により、後で他のタイプのオブジェクトが配置される文字配列を割り当てる一般的なイディオムが可能になります。 —エンドノート]

§21.6.2[new.delete.array]を読むと、デフォルトのoperator new[]operator delete[]operator newoperator 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::bytesの有効期間を終了し、同じアドレスで新しい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_storagenewに指定されたタイプとして使用し、アラインメントされたストレージが自明な集計であるため、std::byteと同様に削除機能を使用することです。

23
Artyer
_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()から呼び出す必要があります。

15
NathanOliver