最終更新日時:
が更新

履歴 編集

named requirement

Cpp17Allocator

概要

Cpp17Allocatorは、記憶域の確保と解放を行うアロケータの要件である。

std::vectorなどのコンテナ、std::basic_string、文字列ストリーム、std::match_resultsが、記憶域の確保方法を差し替えるためにこの要件を要求する。

アロケータはstd::allocator_traitsを介して使用される。要件のほとんどは省略でき、省略した項目はstd::allocator_traitsが既定の定義を与える。そのため、自作のアロケータは少数の項目だけを定義すればよい。

要件

以下では、Tを要素型、XをTに対するアロケータ型、Yを別の要素型Uに対するアロケータ型、a・a1・a2をXの左辺値、bをYの値、pをa.allocate()で得られたポインタ、nを要素数とする。

省略できない項目

項目 説明
X::value_type 要素型。Tと同じ型であること
a.allocate(n) n個のTを格納できる記憶域を確保し、その先頭を指すポインタを返す
a.deallocate(p, n) a.allocate(n)で確保した記憶域を解放する
a1 == a2、a == b 一方で確保した記憶域を他方で解放できるかを表す

このほか、XがCpp17CopyConstructible要件を満たすこと、およびYからXを構築できることが要求される。コンテナは内部で要素型とは異なる型 (リストのノードなど) の記憶域を確保するため、要素型を差し替えたアロケータ型から変換できる必要がある。

省略できる項目

項目 既定の定義
X::pointer T*
X::const_pointer pointer_traits<pointer>::rebind<const T>
X::void_pointer pointer_traits<pointer>::rebind<void>
X::const_void_pointer pointer_traits<pointer>::rebind<const void>
X::size_type make_unsigned_t<difference_type>
X::difference_type pointer_traits<pointer>::difference_type
X::rebind<U>::other テンプレート引数をUに差し替えた型
a.allocate(n, y) a.allocate(n)
a.allocate_at_least(n) {a.allocate(n), n} (C++23)
a.max_size() numeric_limits<size_type>::max() / sizeof(value_type)
a.construct(c, args...) construct_at(c, std::forward<Args>(args)...)
a.destroy(c) destroy_at(c)
a.select_on_container_copy_construction() a
X::propagate_on_container_copy_assignment false_type
X::propagate_on_container_move_assignment false_type
X::propagate_on_container_swap false_type
X::is_always_equal is_empty<X>::type

X::pointerなどのポインタ型には、Cpp17NullablePointer要件が要求される。X::pointerとX::const_pointerには、加えてCpp17RandomAccessIterator要件が要求される。

対応する標準コンセプト

規格は、この要件の最小限の部分を表す説明専用コンセプトsimple-allocatorを定義している。

namespace std {
  template<class Alloc>
  concept simple-allocator =
    requires(Alloc alloc, size_t n) {
      { *alloc.allocate(n) } -> same_as<typename Alloc::value_type&>;
      { alloc.deallocate(alloc.allocate(n), n) };
    } &&
    copy_constructible<Alloc> &&
    equality_comparable<Alloc>;
}

この要件との違いは以下である。

  • simple-allocatorは、記憶域の確保・解放、コピー構築、等値比較だけを要求する。要素型を差し替えたアロケータ型からの変換や、省略できる項目の意味は表現しない
  • コンテナが要求するのはこの要件であり、simple-allocatorではない。simple-allocatorはstd::execution::get_allocator()が返す型の適格要件のように、アロケータであることだけを判定する箇所で使用される
  • simple-allocatorはコンパイル時に検査できる

この要件を満たす型はsimple-allocatorのモデルとなる。

例

アロケータを実装する (最小要件を満たす実装)

省略できない項目だけを定義する。省略した項目はstd::allocator_traitsが既定の定義を与える。

#include <iostream>
#include <vector>
#include <new>

// 最小限のアロケータ。省略した項目はstd::allocator_traitsが既定の定義を与える
template <class T>
struct LoggingAllocator {
  // 要素型。省略できない
  using value_type = T;

  LoggingAllocator() = default;

  // コンテナが内部で別の要素型のアロケータを必要とするため、変換できるようにする
  template <class U>
  LoggingAllocator(const LoggingAllocator<U>&) {}

  // n個分の記憶域を確保する
  T* allocate(std::size_t n)
  {
    std::cout << "allocate " << n << std::endl;
    return static_cast<T*>(::operator new(n * sizeof(T)));
  }

  // allocate()で確保した記憶域を解放する
  void deallocate(T* p, std::size_t n)
  {
    std::cout << "deallocate " << n << std::endl;
    ::operator delete(p);
  }

  // 一方で確保した記憶域を他方で解放できる場合に等値とする
  template <class U>
  bool operator==(const LoggingAllocator<U>&) const
  {
    return true;
  }
};

int main()
{
  std::vector<int, LoggingAllocator<int>> v;
  v.reserve(2);
  v.push_back(1);
}

出力

allocate 2
deallocate 2

アロケータを実装する (要件を完全に満たす実装)

省略できる項目もすべて明示的に定義すると、以下のようになる。各項目には既定の定義と同じ内容を書いているため、動作は最小要件を満たす実装と変わらない。

#include <iostream>
#include <list>
#include <memory>
#include <limits>
#include <new>
#include <type_traits>
#include <utility>

// すべての項目を明示的に定義したアロケータ
template <class T>
struct FullAllocator {
  using value_type = T;

  // ポインタ型。既定ではvalue_typeから導出される
  using pointer = T*;
  using const_pointer = const T*;
  using void_pointer = void*;
  using const_void_pointer = const void*;

  // サイズと差分の型。既定ではポインタ型から導出される
  using size_type = std::size_t;
  using difference_type = std::ptrdiff_t;

  // コンテナのコピー代入・ムーブ代入・交換でアロケータを伝播させるか
  // 既定ではいずれもfalse_type
  using propagate_on_container_copy_assignment = std::false_type;
  using propagate_on_container_move_assignment = std::true_type;
  using propagate_on_container_swap = std::false_type;

  // 同じ型のアロケータがつねに等値であるか。既定ではstd::is_empty<X>::type
  using is_always_equal = std::true_type;

  // 別の要素型に対応するアロケータ型。既定ではテンプレート引数を差し替えた型
  template <class U>
  struct rebind {
    using other = FullAllocator<U>;
  };

  FullAllocator() = default;

  template <class U>
  FullAllocator(const FullAllocator<U>&) {}

  pointer allocate(size_type n)
  {
    return static_cast<pointer>(::operator new(n * sizeof(T)));
  }

  // ヒントを指定した確保。既定ではallocate(n)が使用される
  pointer allocate(size_type n, const_void_pointer)
  {
    return allocate(n);
  }

  // 要求した数以上の記憶域を確保する。既定ではallocate(n)が使用される
  std::allocation_result<pointer, size_type> allocate_at_least(size_type n)
  {
    return {allocate(n), n};
  }

  void deallocate(pointer p, size_type)
  {
    ::operator delete(p);
  }

  // 確保できる最大の要素数。既定では要素型のサイズから算出される
  size_type max_size() const
  {
    return std::numeric_limits<size_type>::max() / sizeof(T);
  }

  // 要素の構築と破棄。既定ではstd::construct_at()とstd::destroy_at()が使用される
  template <class U, class... Args>
  void construct(U* p, Args&&... args)
  {
    std::construct_at(p, std::forward<Args>(args)...);
  }

  template <class U>
  void destroy(U* p)
  {
    std::destroy_at(p);
  }

  // コンテナのコピー構築時に、コピー先が使用するアロケータ。既定では*this
  FullAllocator select_on_container_copy_construction() const
  {
    return *this;
  }

  template <class U>
  bool operator==(const FullAllocator<U>&) const
  {
    return true;
  }
};

int main()
{
  // listはノードを確保するため、rebindによって要素型を差し替えたアロケータを使用する
  std::list<int, FullAllocator<int>> ls = {1, 2, 3};

  for (int x : ls) {
    std::cout << x << std::endl;
  }
}

出力

1
2
3

関連項目