web-dev-qa-db-ja.com

コピー構築の要件を前提として、C ++ 11でステートフルアロケーターをどのように記述できますか?

私の知る限り、STLコンテナで使用するアロケータの要件は、C++ 11標準のセクション17.6.3.5の表28に示されています。

これらの要件のいくつかの間の相互作用について少し混乱しています。タイプXのアロケーターであるタイプT、タイプY、インスタンスUa1、およびaa2の「対応するアロケータークラス」であるタイプX、およびbのインスタンスYは、

  1. a1 == a2は、a1から割り当てられたストレージをa2によって割り当て解除でき、その逆の場合にのみ、trueと評価されます。

  2. X a1(a);は整形式であり、例外を介して終了せず、その後a1 == aはtrueです。

  3. X a(b)は整形式で、例外によって終了せず、その後a == bになります。

私はこれを読んで、すべてのアロケーターはコピーがオリジナルと交換可能であるような方法でコピー構築可能でなければならないと言っています。さらに悪いことに、型の境界を越えても同じことが言えます。これはかなり面倒な要件のようです。私の知る限り、多数のタイプのアロケータは不可能です。

たとえば、解放されたオブジェクトをキャッシュするために、アロケータで使用したいフリーリストクラスがあるとします。 TUのサイズまたは配置が異なり、フリーリストエントリに互換性がないため、何か不足している場合を除いて、アロケータにそのクラスのインスタンスを含めることはできませんでした。

私の質問:

  1. 上記の私の解釈は正しいですか?

  2. C++ 11が「ステートフルアロケーター」のサポートを改善したことをいくつか読んだことがあります。これらの制限を考えると、それはどうですか?

  3. 私がやろうとしているようなことをするための提案はありますか?つまり、アロケータに割り当てられた型固有の状態を含めるにはどうすればよいですか?

  4. 一般に、アロケータを取り巻く言語はだらしのないようです。 (たとえば、表28のプロローグでは、aX&型であると想定していますが、一部の式はaを再定義しています。)また、少なくともGCCのサポートは非​​準拠です。アロケータに関するこの奇妙さの原因は何ですか?あまり使用されない機能ですか?

35
jacobsa

1)上記の私の解釈は正しいですか?

フリーリストはアロケーターに適さないかもしれませんが、複数のサイズ(および配置)を処理できるようにする必要があります。それはフリーリストが解決する問題です。

2)C++ 11が「ステートフルアロケーター」のサポートを改善したことをいくつか読んだことがあります。これらの制限を考えると、それはどうですか?

それは生まれるほど改善されていません。 C++ 03では、標準は実装者を、等しくないインスタンスと実装者をサポートできるアロケータの提供に向けて動かし、ステートフルなアロケータを効果的に移植不可能にしました。

3)私がしようとしているようなことをする方法について何か提案はありますか?つまり、アロケータに割り当てられた型固有の状態を含めるにはどうすればよいですか?

あなたのアロケータ柔軟である必要があるかもしれません、それはあなたがそれが割り当てることになっているメモリ(とどのタイプ)を正確に知る必要がないからです。この要件は、std::liststd::setstd::mapなどのアロケータを使用する一部のコンテナの内部からあなた(ユーザー)を隔離するために必要です。

そのようなアロケータは、std::vectorstd::dequeなどの単純なコンテナで引き続き使用できます。

はい、それは費用のかかる要件です。

4)一般に、アロケータを取り巻く言語はだらしのないようです。 (たとえば、表28のプロローグでは、aがX&型であると想定していますが、一部の式はaを再定義しています。)また、少なくともGCCのサポートは非​​準拠です。アロケータに関するこの奇妙さの原因は何ですか?あまり使用されない機能ですか?

標準は一般に、アロケーターだけでなく、正確に読むのは簡単ではありません。注意する必要があります。

念のため、gccはアロケーターをサポートしていません(コンパイラーです)。 libstdc ++(gccに同梱されている標準ライブラリの実装)について話していると思います。 libstdc ++はoldであるため、C++ 03に合わせて調整されました。これはC++ 11に合わせて調整されていますが、まだ完全に準拠していません(たとえば、文字列にまだコピーオンライトを使用しています)。その理由は、libstdc ++はバイナリー互換性に非常に重点を置いており、C++ 11が必要とする多くの変更により、この互換性が損なわれるためです。したがって、注意深く導入する必要があります。

14
Matthieu M.

アロケーターの同等性は、それらがまったく同じ内部状態を持っている必要があることを意味するのではなく、両方がどちらかのアロケーターで割り当てられたメモリの割り当てを解除できなければならないだけです。 クロスタイプタイプaのアロケーターXのアロケーターa == bの等式とタイプbのアロケーターYは、表28で「同じ」と定義されています。 a == Y::template rebind<T>::other(b)」として。言い換えると、aba == bに再バインドすることによってインスタンス化されたアロケータがaによって割り当てられたメモリの割り当てを解除できる場合は、value_typeになります。

フリーリストアロケータは、任意のタイプのノードの割り当てを解除できる必要はありません。FreelistAllocator<T>によって割り当てられたメモリがFreelistAllocator<U>::template rebind<T>::otherによって割り当て解除できることを確認する必要があるだけです。 FreelistAllocator<U>::template rebind<T>::otherは、ほとんどの正常な実装でFreelistAllocator<T>と同じ型であることを考えると、これはかなり簡単に実現できます。

簡単な例( Coliruでのライブデモ ):

template <typename T>
class FreelistAllocator {
    union node {
        node* next;
        typename std::aligned_storage<sizeof(T), alignof(T)>::type storage;
    };

    node* list = nullptr;

    void clear() noexcept {
        auto p = list;
        while (p) {
            auto tmp = p;
            p = p->next;
            delete tmp;
        }
        list = nullptr;
    }

public:
    using value_type = T;
    using size_type = std::size_t;
    using propagate_on_container_move_assignment = std::true_type;

    FreelistAllocator() noexcept = default;
    FreelistAllocator(const FreelistAllocator&) noexcept {}
    template <typename U>
    FreelistAllocator(const FreelistAllocator<U>&) noexcept {}
    FreelistAllocator(FreelistAllocator&& other) noexcept :  list(other.list) {
        other.list = nullptr;
    }

    FreelistAllocator& operator = (const FreelistAllocator&) noexcept {
        // noop
        return *this;
    }

    FreelistAllocator& operator = (FreelistAllocator&& other) noexcept {
        clear();
        list = other.list;
        other.list = nullptr;
        return *this;
    }

    ~FreelistAllocator() noexcept { clear(); }

    T* allocate(size_type n) {
        std::cout << "Allocate(" << n << ") from ";
        if (n == 1) {
            auto ptr = list;
            if (ptr) {
                std::cout << "freelist\n";
                list = list->next;
            } else {
                std::cout << "new node\n";
                ptr = new node;
            }
            return reinterpret_cast<T*>(ptr);
        }

        std::cout << "::operator new\n";
        return static_cast<T*>(::operator new(n * sizeof(T)));
    }

    void deallocate(T* ptr, size_type n) noexcept {
        std::cout << "Deallocate(" << static_cast<void*>(ptr) << ", " << n << ") to ";
        if (n == 1) {
            std::cout << "freelist\n";
            auto node_ptr = reinterpret_cast<node*>(ptr);
            node_ptr->next = list;
            list = node_ptr;
        } else {
            std::cout << "::operator delete\n";
            ::operator delete(ptr);
        }
    }
};

template <typename T, typename U>
inline bool operator == (const FreelistAllocator<T>&, const FreelistAllocator<U>&) {
    return true;
}

template <typename T, typename U>
inline bool operator != (const FreelistAllocator<T>&, const FreelistAllocator<U>&) {
    return false;
}
27
Casey

私はこれを読んで、すべてのアロケーターはコピーがオリジナルと交換可能であるような方法でコピー構築可能でなければならないと言っています。さらに悪いことに、型の境界を越えても同じことが言えます。これはかなり面倒な要件のようです。私の知る限り、多数のタイプのアロケータは不可能です。

アロケータが一部のメモリリソースへの軽量ハンドルである場合、要件を満たすのは簡単です。個別のアロケータオブジェクト内にリソースを埋め込もうとしないでください。

たとえば、解放されたオブジェクトをキャッシュするために、アロケータで使用したいフリーリストクラスがあるとします。 TとUのサイズまたは配置が異なり、フリーリストエントリに互換性がないため、何か不足している場合を除いて、アロケータにそのクラスのインスタンスを含めることはできませんでした。

[allocator.requirements]段落9:

アロケータは、インスタンス化できる型と、そのconstructメンバーを呼び出すことができる引数を制約する場合があります。特定のアロケーターで型を使用できない場合、アロケータークラスまたはconstructの呼び出しはインスタンス化に失敗する可能性があります。

アロケータが、特定のタイプT以外のメモリを割り当てることを拒否しても問題ありません。これにより、独自の内部ノードタイプを割り当てる必要がある_std::list_などのノードベースのコンテナーで使用されなくなります(コンテナーの_value_type_だけでなく)。ただし、_std::vector_では正常に機能します。

これは、アロケータが他の型にリバインドされないようにすることで実行できます。

_class T;

template<typename ValueType>
class Alloc {
  static_assert(std::is_same<ValueType, T>::value,
    "this allocator can only be used for type T");
  // ...
};

std::vector<T, Alloc<T>> v; // OK
std::list<T, Alloc<T>> l;   // Fails
_

または、sizeof(T)に収まるタイプのみをサポートできます。

_template<typename ValueType>
class Alloc {
  static_assert(sizeof(ValueType) <= sizeof(T),
    "this allocator can only be used for types not larger than sizeof(T)");
  static_assert(alignof(ValueType) <= alignof(T),
    "this allocator can only be used for types with alignment not larger than alignof(T)");

  // ...
};
_
  1. 上記の私の解釈は正しいですか?

完全にではありません。

  1. C++ 11が「ステートフルアロケーター」のサポートを改善したことをいくつか読んだことがあります。これらの制限を考えると、それはどうですか?

C++ 11以前の制限はさらに悪化しました!

アロケーターがコピーおよび移動されたときにコンテナー間でどのように伝播するか、およびアロケーターインスタンスが元のインスタンスと比較できない別のインスタンスに置き換えられた場合のさまざまなコンテナー操作の動作が明確に指定されました。それらの明確化なしでは、例えば、 2つのコンテナーをステートフルアロケーターと交換しました。

  1. 私がやろうとしているようなことをするための提案はありますか?つまり、アロケータに割り当てられた型固有の状態を含めるにはどうすればよいですか?

アロケータに直接埋​​め込むのではなく、個別に保存し、アロケータにポインタ(リソースのライフタイム管理の設計方法によってはスマートポインタの場合もある)で参照させるようにします。実際のアロケータオブジェクトは、メモリの外部ソース(アリーナ、プール、フリーリストを管理するものなど)への軽量ハンドルである必要があります。同じソースを共有するアロケーターオブジェクトは同等に比較する必要があります。これは、値の型が異なるアロケーターにも当てはまります(以下を参照)。

1つだけをサポートする必要がある場合は、すべてのタイプの割り当てをサポートしないことをお勧めします。

  1. 一般に、アロケータを取り巻く言語はだらしのないようです。 (たとえば、表28のプロローグでは、aはX&型であると想定していますが、一部の式はaを再定義しています。)

はい、 https://github.com/cplusplus/draft/pull/334 で報告したように(ありがとう)。

また、少なくともGCCのサポートは不適合です。

100%ではありませんが、次のリリースで提供される予定です。

アロケータに関するこの奇妙さの原因は何ですか?あまり使用されない機能ですか?

はい。そして、多くの歴史的な手荷物があり、広く役立つように指定することは困難です。私の ACCU 2012プレゼンテーション にはいくつかの詳細があります。読んだ後、それをより簡単にすることができると思うと、私は非常に驚きます;-)


アロケーターが等しい場合は、以下を考慮してください。

_MemoryArena m;
Alloc<T> t_alloc(&m);
Alloc<T> t_alloc_copy(t_alloc);
assert( t_alloc_copy == t_alloc ); // share same arena
Alloc<U> u_alloc(t_alloc);
assert( t_alloc == u_alloc ); // share same arena
MemoryArena m2
Alloc<T> a2(&m2);
assert( a2 != t_alloc ); // using different arenas
_

アロケータの等価性の意味は、オブジェクトが互いのメモリを解放できることです。そのため、_t_alloc_からメモリを割り当て、_(t_alloc == u_alloc)_がtrueの場合、そのメモリの割り当てを解除できることを意味します_u_alloc_。それらが等しくない場合、_u_alloc_は_t_alloc_からのメモリの割り当てを解除できません。

他のフリーリストにメモリを追加できるフリーリストがある場合、すべてのアロケータオブジェクトは互いに等しいと考えられます。

9
Jonathan Wakely