スタック上に存在する(固定サイズの)バッファーを使用するC++標準ライブラリ準拠のallocator
を使用することが実用的かどうか疑問に思いました。
どういうわけか、この質問はSOでまだこのように尋ねられていないようですが、mayは他の場所で暗黙的に回答されています。
したがって、基本的には、私の検索では、固定サイズのバッファーを使用するアロケーターを作成できるはずです。さて、一見すると、これは、スタック上に「存在する」固定サイズのバッファを使用するアロケータを持つことがまた可能であることを意味するはずです。 、しかし、表示されます、そのような実装は広く普及していません。
私が意味することの例を挙げましょう:
{ ...
char buf[512];
typedef ...hmm?... local_allocator; // should use buf
typedef std::basic_string<char, std::char_traits<char>, local_allocator> lstring;
lstring str; // string object of max. 512 char
}
これはどのように実装できますか?
この他の質問への回答 (R。MartinhoFernandesに感謝)は、クロムソースからのスタックベースのアロケーターにリンクしています: http://src.chromium.org/viewvc/chrome/ trunk/src/base/stack_container.h
ただし、このクラスは非常に特殊なようです。特に、このStackAllocator
にはデフォルトのctorがないため、-と思っていました。 すべてのアロケータクラスにはデフォルトのctorが必要です 。
どうやら、そこに 適合スタックアロケータがあります 1つから ハワード・ヒナント 。
これは、(参照されたarena
オブジェクトを介して)固定サイズのバッファーを使用し、要求されたスペースが多すぎる場合はヒープにフォールバックすることで機能します。
このアロケータにはデフォルトのctorがなく、ハワードは次のように述べています。
この記事を、C++ 11に完全に準拠した新しいアロケーターで更新しました。
アロケーターがデフォルトのctorを持っている必要はないと思います。
間違いなく完全にC++ 11/C++ 14に準拠したスタックアロケータ*を作成することは可能です。ただし、スタック割り当ての実装とセマンティクス、およびそれらが標準コンテナとどのように相互作用するかについて、いくつかの影響を考慮する必要があります。
これは完全にC++ 11/C++ 14に準拠したスタックアロケーターです(これも私の github でホストされています):
_#include <functional>
#include <memory>
template <class T, std::size_t N, class Allocator = std::allocator<T>>
class stack_allocator
{
public:
typedef typename std::allocator_traits<Allocator>::value_type value_type;
typedef typename std::allocator_traits<Allocator>::pointer pointer;
typedef typename std::allocator_traits<Allocator>::const_pointer const_pointer;
typedef typename Allocator::reference reference;
typedef typename Allocator::const_reference const_reference;
typedef typename std::allocator_traits<Allocator>::size_type size_type;
typedef typename std::allocator_traits<Allocator>::difference_type difference_type;
typedef typename std::allocator_traits<Allocator>::const_void_pointer const_void_pointer;
typedef Allocator allocator_type;
public:
explicit stack_allocator(const allocator_type& alloc = allocator_type())
: m_allocator(alloc), m_begin(nullptr), m_end(nullptr), m_stack_pointer(nullptr)
{ }
explicit stack_allocator(pointer buffer, const allocator_type& alloc = allocator_type())
: m_allocator(alloc), m_begin(buffer), m_end(buffer + N),
m_stack_pointer(buffer)
{ }
template <class U>
stack_allocator(const stack_allocator<U, N, Allocator>& other)
: m_allocator(other.m_allocator), m_begin(other.m_begin), m_end(other.m_end),
m_stack_pointer(other.m_stack_pointer)
{ }
constexpr static size_type capacity()
{
return N;
}
pointer allocate(size_type n, const_void_pointer hint = const_void_pointer())
{
if (n <= size_type(std::distance(m_stack_pointer, m_end)))
{
pointer result = m_stack_pointer;
m_stack_pointer += n;
return result;
}
return m_allocator.allocate(n, hint);
}
void deallocate(pointer p, size_type n)
{
if (pointer_to_internal_buffer(p))
{
m_stack_pointer -= n;
}
else m_allocator.deallocate(p, n);
}
size_type max_size() const noexcept
{
return m_allocator.max_size();
}
template <class U, class... Args>
void construct(U* p, Args&&... args)
{
m_allocator.construct(p, std::forward<Args>(args)...);
}
template <class U>
void destroy(U* p)
{
m_allocator.destroy(p);
}
pointer address(reference x) const noexcept
{
if (pointer_to_internal_buffer(std::addressof(x)))
{
return std::addressof(x);
}
return m_allocator.address(x);
}
const_pointer address(const_reference x) const noexcept
{
if (pointer_to_internal_buffer(std::addressof(x)))
{
return std::addressof(x);
}
return m_allocator.address(x);
}
template <class U>
struct rebind { typedef stack_allocator<U, N, allocator_type> other; };
pointer buffer() const noexcept
{
return m_begin;
}
private:
bool pointer_to_internal_buffer(const_pointer p) const
{
return (!(std::less<const_pointer>()(p, m_begin)) && (std::less<const_pointer>()(p, m_end)));
}
allocator_type m_allocator;
pointer m_begin;
pointer m_end;
pointer m_stack_pointer;
};
template <class T1, std::size_t N, class Allocator, class T2>
bool operator == (const stack_allocator<T1, N, Allocator>& lhs,
const stack_allocator<T2, N, Allocator>& rhs) noexcept
{
return lhs.buffer() == rhs.buffer();
}
template <class T1, std::size_t N, class Allocator, class T2>
bool operator != (const stack_allocator<T1, N, Allocator>& lhs,
const stack_allocator<T2, N, Allocator>& rhs) noexcept
{
return !(lhs == rhs);
}
_
このアロケータは、ユーザー提供の固定サイズのバッファをメモリの初期ソースとして使用し、スペースが不足するとセカンダリアロケータ(デフォルトでは_std::allocator<T>
_)にフォールバックします。
考慮事項:
先に進んでスタックアロケータを使用する前に、割り当てパターンを検討する必要があります。まず、スタックでメモリバッファを使用する場合、メモリの割り当てと割り当て解除を正確に行う必要があります意味。
最も単純な方法(および上記で採用した方法)は、割り当てのスタックポインターを単純にインクリメントし、割り当て解除のスタックポインターをデクリメントすることです。このseverelyは、実際にアロケータを使用する方法を制限することに注意してください。正しく使用すれば、たとえば_std::vector
_(単一の連続したメモリブロックを割り当てる)では正常に機能しますが、ノードオブジェクトの割り当てと割り当て解除を行う_std::map
_では機能しません。さまざまな順序。
スタックアロケータがスタックポインタを単純にインクリメントおよびデクリメントする場合、割り当てと割り当て解除がLIFOの順序ではない場合、未定義の動作が発生します。_std::vector
_でも未定義の動作が発生します最初にスタックから単一の連続ブロックを割り当て、次に2番目のスタックブロックを割り当て、次に最初のブロックの割り当てを解除する場合、これは、ベクトルが容量を_stack_size
_よりも小さい値に増やすたびに発生します。これはそのため、事前にスタックサイズを予約する必要があります(ただし、Howard Hinnantの実装については以下の注を参照してください)。
それは私たちに質問をもたらします...
スタックアロケータから何を本当に欲しい?
malloc
を呼び出す代わりに、事前に割り当てられたスタックバッファから取得することを除いて、さまざまなサイズのメモリチャンクをさまざまな順序で割り当ておよび割り当て解除できる汎用アロケータが実際に必要ですか(sbrk
など)。もしそうなら、あなたは基本的にメモリブロックの空きリストを何らかの形で維持する汎用アロケータの実装について話しているので、ユーザーだけが既存のスタックバッファを提供できます。これははるかに複雑なプロジェクトです。 (そして、スペースが不足した場合はどうすればよいですか?_std::bad_alloc
_をスローしますか?ヒープにフォールバックしますか?)
上記の実装では、単にLIFO割り当てパターンを使用し、スペースが不足した場合は別のアロケーターにフォールバックするアロケーターが必要であると想定しています。これは_std::vector
_で正常に機能します。事前に予約できる単一の連続したバッファーを使用します。_std::vector
_がより大きなバッファーを必要とする場合、より大きなバッファーを割り当て、より小さなバッファー内の要素をコピー(または移動)してから、より小さなバッファーの割り当てを解除します。ベクターがより大きなバッファーを要求すると、上記のstack_allocator実装は、単にセカンダリアロケーター(デフォルトでは_std::allocator
_)にフォールバックします。
したがって、たとえば:
_const static std::size_t stack_size = 4;
int buffer[stack_size];
typedef stack_allocator<int, stack_size> allocator_type;
std::vector<int, allocator_type> vec((allocator_type(buffer))); // double parenthesis here for "most vexing parse" nonsense
vec.reserve(stack_size); // attempt to reserve space for 4 elements
std::cout << vec.capacity() << std::endl;
vec.Push_back(10);
vec.Push_back(20);
vec.Push_back(30);
vec.Push_back(40);
// Assert that the vector is actually using our stack
//
assert(
std::equal(
vec.begin(),
vec.end(),
buffer,
[](const int& v1, const int& v2) {
return &v1 == &v2;
}
)
);
// Output some values in the stack, we see it is the same values we
// inserted in our vector.
//
std::cout << buffer[0] << std::endl;
std::cout << buffer[1] << std::endl;
std::cout << buffer[2] << std::endl;
std::cout << buffer[3] << std::endl;
// Attempt to Push back some more values. Since our stack allocator only has
// room for 4 elements, we cannot satisfy the request for an 8 element buffer.
// So, the allocator quietly falls back on using std::allocator.
//
// Alternatively, you could modify the stack_allocator implementation
// to throw std::bad_alloc
//
vec.Push_back(50);
vec.Push_back(60);
vec.Push_back(70);
vec.Push_back(80);
// Assert that we are no longer using the stack buffer
//
assert(
!std::equal(
vec.begin(),
vec.end(),
buffer,
[](const int& v1, const int& v2) {
return &v1 == &v2;
}
)
);
// Print out all the values in our vector just to make sure
// everything is sane.
//
for (auto v : vec) std::cout << v << ", ";
std::cout << std::endl;
_
繰り返しになりますが、これはベクターでは問題なく機能しますが、スタックアロケーターで何をしようとしているのかを自問する必要があります。たまたまスタックバッファから引き出される汎用メモリアロケータが必要な場合は、はるかに複雑なプロジェクトについて話していることになります。ただし、スタックポインタをインクリメントおよびデクリメントするだけの単純なスタックアロケータは、限られたユースケースのセットで機能します。非PODタイプの場合、実際のスタックバッファを作成するにはstd::aligned_storage<T, alignof(T)>
を使用する必要があることに注意してください。
Howard Hinnantの実装 とは異なり、上記の実装では、deallocate()
を呼び出したときに、渡されたポインターが最後に割り当てられたブロックであることを明示的にチェックしないことにも注意してください。渡されたポインタがLIFO順の割り当て解除でない場合、Hinnantの実装は単に何もしません。これにより、アロケータは基本的にignoreベクトルが初期バッファの割り当てを解除しようとするため、事前に予約せずに_std::vector
_を使用できます。しかし、これはアロケーターのセマンティクスも少し曖昧にし、_std::vector
_が機能することが知られている方法にかなり具体的にバインドされている動作に依存しています。私の感じでは、deallocate()
へのポインタを渡すことはではなかった最後の呼び出しto allocate()
は未定義の動作になり、そのままにしておきます。
*最後に-次の警告: 議論の余地がある ポインタがスタックバッファの境界内にあるかどうかをチェックする関数が、標準で定義されている動作であるかどうか。異なるnew
/malloc
'dバッファーからの2つのポインターを順序比較することは、おそらく実装定義の動作であり(_std::less
_でも)、ヒープ割り当てにフォールバックする標準準拠のスタックアロケーター実装を作成することはおそらく不可能です。 (ただし、実際には、MS-DOSで80286を実行していない限り、これは問題になりません。)
**最後に(本当に今)、stack allocatorのWord "stack"が、メモリのsourceの両方を参照するようにオーバーロードされていることにも注意してください(固定-サイズのスタック配列)とメソッドの割り当て(LIFOスタックポインターのインクリメント/デクリメント)。ほとんどのプログラマーがスタックアロケーターが必要だと言うとき、彼らは考えています。後者のセマンティクスを必ずしも考慮せずに前者の意味について、およびこれらのセマンティクスが標準コンテナでのそのようなアロケータの使用をどのように制限するかについて。
スタックベースのSTLアロケータの有用性は非常に限られているため、多くの先行技術が見つかるとは思えません。最初のlstring
をコピーまたは長くしたい場合は、引用した単純な例でさえすぐに爆発します。
連想コンテナ(内部的にツリーベース)や、RAMの単一または複数の連続ブロックを使用するvector
およびdeque
などの他のSTLコンテナの場合、メモリ使用量のセマンティクスはすぐに管理できなくなります。ほぼすべての実際の使用法におけるスタック。
これは実際には非常に便利な方法であり、ゲームなどのパフォーマンス開発でかなり使用されます。スタック上またはクラス構造の割り当て内にメモリをインラインで埋め込むことは、コンテナの速度や管理にとって重要な場合があります。
あなたの質問に答えるために、それはstlコンテナの実装に帰着します。コンテナがインスタンス化されるだけでなく、メンバーとしてアロケータへの参照を保持する場合は、固定ヒープを作成することをお勧めします。これは仕様の一部ではないため、常に当てはまるとは限りません。そうしないと問題になります。 1つの解決策は、コンテナー、ベクター、リストなどを、ストレージを含む別のクラスでラップすることです。次に、アロケータを使用してそこから描画できます。これには、多くのテンプレートマジックケリー(tm)が必要になる可能性があります。
C++ 17以降、実際には非常に簡単に実行できます。これが基づいているので、完全なクレジットは 最も愚かなアロケーター のautorに行きます。
最も馬鹿げたアロケーターは、char[]
リソースを基盤となるストレージとして使用するモノトモイックバンプアロケーターです。元のバージョンでは、そのchar[]
はmmap
を介してヒープに配置されますが、スタック上のchar[]
を指すように変更するのは簡単です。
template<std::size_t Size=256>
class bumping_memory_resource {
public:
char buffer[Size];
char* _ptr;
explicit bumping_memory_resource()
: _ptr(&buffer[0]) {}
void* allocate(std::size_t size) noexcept {
auto ret = _ptr;
_ptr += size;
return ret;
}
void deallocate(void*) noexcept {}
};
これにより、作成時にスタックにSize
バイトが割り当てられます(デフォルトは256
)。
template <typename T, typename Resource=bumping_memory_resource<256>>
class bumping_allocator {
Resource* _res;
public:
using value_type = T;
explicit bumping_allocator(Resource& res)
: _res(&res) {}
bumping_allocator(const bumping_allocator&) = default;
template <typename U>
bumping_allocator(const bumping_allocator<U,Resource>& other)
: bumping_allocator(other.resource()) {}
Resource& resource() const { return *_res; }
T* allocate(std::size_t n) { return static_cast<T*>(_res->allocate(sizeof(T) * n)); }
void deallocate(T* ptr, std::size_t) { _res->deallocate(ptr); }
friend bool operator==(const bumping_allocator& lhs, const bumping_allocator& rhs) {
return lhs._res == rhs._res;
}
friend bool operator!=(const bumping_allocator& lhs, const bumping_allocator& rhs) {
return lhs._res != rhs._res;
}
};
そして、これが実際のアロケータです。リソースマネージャーにリセットを追加して、リージョンの先頭から新しいアロケーターを再度作成できるようにするのは簡単であることに注意してください。また、通常のリスクをすべて伴うリングバッファを実装することもできます。
このようなものが必要な場合は、組み込みシステムで使用します。組み込みシステムは通常、ヒープの断片化にうまく反応しないため、ヒープに反映されない動的割り当てを使用できると便利な場合があります。
それは本当にあなたの要件に依存します、あなたが望むならスタックでのみ動作するアロケーターを作成することができますが、ヒープオブジェクトと同じスタックオブジェクトがプログラムのどこからでもアクセスできないのでそれは非常に制限されます。
この記事はアロケーターを非常によく説明していると思います
http://www.codeguru.com/cpp/cpp/cpp_mfc/stl/article.php/c4079