2015-09-14

C++のアロケーター

次のC++標準化委員会の文書が出るまで、アロケーターのことでも書くことにした。

アロケーターとは、ストレージを確保するためのライブラリだ。標準ライブラリには、std::allocator<T>がある。

#include <memory>

int main()
{
    std::allocator< std::string > a ;
    auto raw_ptr = a.allocate( 1 ) ; // std::string一つ分のストレージを確保
    a.deallocate( raw_ptr, 1 ) ; // 解放、確保した個数を引数に渡す
}

独自の方法でストレージの確保をしたい場合、独自のアロケーター要求を満たすクラスを実装して、アロケーターをサポートしているライブラリに渡せばよい。

class custom_allocator ;

int main()
{
    // デフォルト実引数でcustom_allocatorがデフォルト初期化されて渡される    
    std::vector<int, CustomAllocator> v( ) ; 
}

アロケーターをわざわざ書くのは面倒なので、C++11では、allocator_traits<T>が導入された。これは、以下のように使うライブラリだ。

int main()
{
    std::allocator< std::string > a ;
    using t = std::allocator_traits< decltype(a) > ;

    // ストレージ確保
    auto ptr = t::allocate( a, 1 ) ;
    // ストレージ上にオブジェクトを構築
    // Variadic Templatesにより第三引数以下に構築時の実引数を渡せる
    t::construct( a, ptr, "hello" ) ;
    // オブジェクトを破棄
    t::destroy( a, ptr ) ;
    // ストレージ解放
    t::deallocate( a, ptr, 1 ) ;
}

allocator_traitsは、テンプレート実引数にアロケーターを指定すると、そのアロケーターのラッパーとして働く。これにより、独自のアロケーターにネストされた型として、pointerだのconst_pointerだのvoid_ponterだのといった、今まで要求されていた様々なメンバーを実装する必要がなくなった。

また、クラス型でさえあれば何でも指定することができる。アロケータークラスでストレージ確保を実装するのではなく、allocator_traitsの特殊化で実装することも可能だ。

class empty ;

template < >
struct std::allocator_traits< empty >
{
// 実装
} ;

C++11からの標準ライブラリは、アロケーターを直接使わず、allocator_traitsを介して使うよう規定されている。

C++11では、この他にもアロケーター自身に機能追加が行われた。

コンテナのオブジェクトをコピー/ムーブするとき、アロケーターオブジェクトもコピー/ムーブされる。この際のアロケーターのコピー/ムーブの方法を制御できる機能だ。

std::vector<int> a ;
std::vector<int> b = a ; // アロケーターがコピー構築される
b = a ; // アロケーターがコピー代入される
std::vector<int> c = std::move(b) ; // アロケーターがムーブ構築される
b = std::move(c) ; // アロケーターがムーブ代入される

コピー/ムーブと、構築/代入の組み合わせの4種類について、その方法を選べる機能が追加された。

template < typename Allocator >
class Container
{
    Allocator a ;
    using t = std::allocator_traits<Allocator> ;
public :
    // デフォルト初期化
    Container( Allocator & alloc = Allocator() )
        : a( alloc ) { }
} ;

このようなコンテナーがあるとする。

select_on_container_copy_construction

コンテナーがコピー構築されるときは、必ずこの関数が呼ばれる。具体的には、コピーコンストラクターが以下のような実装になる。

template < typename T >
Container::Container( Container const & ref )
    : a( t::t::select_on_container_copy_construction(ref.a) )
{ }

アロケーターは、自分自身を返すか、あるいは別のオブジェクトを返すかを選ぶことができる。

これにより、コピーできないアロケーターを使ったコンテナーでもコピーできるようになる。

propagate_on_container_copy_assignment
propagate_on_container_move_assignment
propagate_on_container_swap

コンテナーをコピー代入、ムーブ代入、swapするときは、アロケーターのオブジェクトも変更される。上の3つの名前のメンバーがアロケーターでstd::false_typeと定義されている場合は、アロケーターは変更しない。

class no_propagate_allocator
{
public :
    using propagate_on_container_copy_assignment = std::false_type ;
    using propagate_on_container_move_assignment = std::false_type ;
    using propagate_on_container_swap = std::false_type ;

    // その他の実装
} ;

template < typename T >
using vec = std::vector<T, no_propagate_allocator > ;

int main()
{
    std::vector<int> a, b ;
    a = b ; // aのアロケーターオブジェクトは変更される。

    vec<int> c, d ;

    c = d ; // cのアロケーターオブジェクトは変更されない。   
} ;

is_always_equal

すべてのアロケーターのオブジェクトが等しい場合、std::true_typeと定義される。たとえば、std::allocatorのis_always_euqalはstd::true_typeである。

これは、アロケータークラスが何らかの異なると比較される状態を持っているかどうかを調べるのに使える。

template < typename Allocator >
void f()
{
    using t = std::allocator_traits<Allocator> ;
    bool b = t::is_always_equal() ; // true
}

allocator_arg

allocator_argは以下のように定義されている。

namespace std {
    struct allocator_arg_t { };
    constexpr allocator_arg_t allocator_arg{};
}

allocator_argは単なるタグとして型情報を利用するためだけのクラスだ。これは一部のコンテナーのオーバーロード解決に使われる。

たとえば、std::tupleは、アロケーターをサポートしている。しかし、tuple自体にアロケーターを渡したい場合もある。

int main()
{
    std::tuple< std::allocator<int> > t( std::allocator<int>{} ) ;
}

オーバーロード解決のために、tupleにアロケーターを渡すときは、allocator_argを渡す。

int main()
{
    std::tuple< std::allocator<int> > t( std::allocator_arg, custom_allocator, std::allocator<int>{} ) ;
}

現在、標準ライブラリでは、std::tuple, std::function, std::promise, std::packaged_taskがstd::allocator_arg_tを引数に取るオーバーロードを提供している。

uses_allocator<T, Alloc> traits

uses_allocator traitsはstd::true_typeかstd::false_typeから派生している。std::true_typeから派生している場合、コンテナーの要素型はコンテナーに渡されたアロケーターのオブジェクトを使って初期化される。例えば、std::queueやstd::stackに渡したアロケーターオブジェクトが、内部のコンテナー型のアロケーターの初期化にも使われる。

scoped_allocator_adaptor

scoped_allocator_adaptorは、コンテナーの要素型のアロケーターまで、アロケーターオブジェクトを指定させる目的で使う。

たとえば、あるアロケータークラスは、オブジェクトごとに内部で状態(ヒープメモリ等)を持っている。

class allocator
{
    std::shared_ptr<heap_area> ptr ;
// その他
} ;

メモリ確保はこのアロケーターのオブジェクトを経由して行わせたい。あるコンテナーに、このアロケーターのあるオブジェクトを使わせたい場合、つまり単一のヒープ領域からストレージを確保させたいは、以下のようにアロケーターを指定すれば事足りるだろうか。

class allocator ;

int main()
{
    allocator a ;
    std::vector< std::string, allocator > v(a) ;
    v.push_back( std::string("hello") ) ;
}

こうすれば、外側のvectorは確かにallocatorを使ってくれるものの、内側のstring(std::basic_string<char, std::char_traits<char>,. std::allocator<char> >)は、依然としてstd::allocatorを使う。

とすれば、以下のようにすべきだろうか。

using str = std::basic_string< char, std::char_traits<char>, allocator > ;
using vec = std::vector< str, allocator > ;

int main()
{
    allocator a ;
    vec v(a) ;
    // 同じヒープ領域から確保させるためには、同じアロケーターのオブジェクトからコピーさせる必要がある。
    v.emplace("hello", a ) ;
}

これは動くが面倒だ。特に、コンテナーのネストが増えるともっと面倒になる。

using str = std::basic_string< char, std::char_traits<char>, allocator > ;
using str = std::basic_string< char, std::char_traits<char>, allocator > ;
template < typename T >
using vec = std::vector< T, allocator > ;

int main()
{
    allocator a ;
    vec< vec<str> > v( a ) ;
    v.emplace( a ) ;
    v[0].emplace( "hello", a ) ;
}

scoped_allocator_adaptorは、アロケーターをラップして、このような内側のコンテナーへのアロケーターに同じオブジェクトを使いまわせる。

というのが、現行の規格の文面とN2554を読む限りのscoped_allocator_adaptorの機能だと思うのだが、どうもうまく動いていない。というのも、内側のコンテナーなり要素なりを外側のコンテナーに追加するとき、insertにしろemplaceにしろ、やはり内側のコンテナーにアロケーターオブジェクトを手動で渡さなければならないことには変わりがないので、scoped_allocator_adaptorを使う利点が全く理解できない。世の中に転がっているサンプルは全部間違っているように思われる。一体どういうことなのだろうか。

ドワンゴ広告

この記事はドワンゴ勤務中に書かれた。

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

1 comment:

Anonymous said...

次のC++標準化委員会の文書はそもそも出るんですかね。
なんかUnicodeのmailing listで、ISOの上部の方針が変わってWGの判断で作業文書を公開することができなくなった(実際JTC1/SC2/WG2のページは更新停止した)という話を見たんですが。