namespace std {
template <class Ptr,
unsigned int BitsRequested = bits-available<element-of<Ptr>>,
class TagT = unsigned int>
class pointer_tag_pair;
}
概要
pointer_tag_pairは、ポインタ値と小さなタグ値のペアを、ポインタ1個分のサイズで保持するクラスである。
型のアライメントによって常に0になることがわかっているポインタの下位ビットへタグ値を詰め込む「ポインタタギング (pointer tagging)」というよく知られたテクニックを、移植可能かつconstexprで使用できる形で提供する。任意の特殊化PTについて、sizeof(PT) == sizeof(Ptr)かつalignof(PT) == alignof(Ptr)であることが保証され、トリビアルコピー可能である。
主な用途には以下がある。
- ポインタへの情報の印付け(アロケータでの由来の記録、小さな参照カウントなど)
- 指す先の多態性の表現(木構造で「次のノードが内部ノードか葉か」をタグで区別するなど)
// 1ビットのタグで、所有権の有無を印付けする例
enum class ownership : unsigned { reference, owning };
std::pointer_tag_pair<int*, 1, ownership> p{ptr, ownership::owning};
static_assert(sizeof(p) == sizeof(int*));
これまでこのテクニックはreinterpret_castとビット演算でしか実装できず、意図がコードに現れず安全でもなかった。本クラスは、タグが収まらない場合をコンパイル時・事前条件で検出でき、定数評価でも使用できる。
テンプレートパラメータ
Ptr: 保持するポインタの型(オブジェクトポインタ型であること。関数ポインタは不可)BitsRequested: タグに要求するビット数。デフォルトは、指す先の型のアライメントから利用できるビット数(説明専用のbits-available。pointer_bits_available(alignof(T))で定義される)TagT: タグ値の型(符号なし整数型、または基底型が符号なし整数型の列挙型であること)
適格要件
PtrはCV修飾されていないオブジェクトポインタ型であること(関数ポインタではないこと)TagTはCV修飾されておらず、符号なし整数型、または基底型が符号なし整数型の列挙型であること。sizeof(TagT) <= sizeof(void*)であることBitsRequested <= max_pointer_bits_availableであること
説明専用エンティティ
本クラスと関連機能の規定では、以下の説明専用のエンティティを使用する。
namespace std {
// 型Tのアライメントからタグ付けに使用できるビット数
template <class T>
constexpr unsigned int bits-available = pointer_bits_available(alignof(T)); // 説明専用
// ポインタ型Uの指す先の型
template <class U>
using element-of = pointer_traits<U>::element_type; // 説明専用
// タグ値vの表現に必要なビット数。
// vが列挙型ならbit_width(to_underlying(v))、そうでなければbit_width(v)
template <class T>
constexpr unsigned int tag-bit-width(T v) noexcept; // 説明専用
}
また、コンストラクタなどの制約には説明専用コンセプトtagging-compatible-pointeeを使用する。
備考
応用事例
ポインタタギングは広く使われている手法である。用途は、タグに何を持たせるかで3つに分かれる。
ひとつめは、ポインタに付随する情報を持たせる使い方である。ポインタは常にポインタであり、タグはそれとは別の意味をもつ小さなデータになる。
- LLVMの
PointerIntPairは、本クラスと同じく、指す先のアライメントから使えるビット数を求め、下位ビットへ小さな整数値を格納する - CPythonのガベージコレクタは、収集対象のオブジェクトをつなぐ双方向リストのポインタにフラグを埋め込む。
_gc_prevの下位2ビットに「収集中か」と「ファイナライズ済みか」を格納し、収集の最中は_gc_nextの最下位ビットに「到達不能と暫定判定されたか」を格納する。オブジェクト1個あたりの追加メモリを増やさずにフラグを持たせるための最適化である std::atomic<std::shared_ptr<T>>のlibstdc++とMSVCの実装は、制御ブロックへのポインタの最下位ビットをスピンロックのフラグに使用する。指す先のオブジェクトへのポインタではなく制御ブロックへのポインタを選ぶのは、制御ブロックの確保をライブラリ側が行うためアライメントを保証できるからである (Inside STL: The atomic shared_ptr - The Old New Thing)- glibcの
mallocは、チャンクヘッダの下位3ビットにPREV_INUSE・IS_MMAPPED・NON_MAIN_ARENAのフラグを格納する。ビットを間借りしているのはポインタではなくサイズのフィールドであり、チャンクサイズが常にアライメントの倍数になることを使っている
ふたつめは、指す先の型を判別する使い方である。ポインタであることは確定しているが、どの型のオブジェクトを指しているかをタグで表す。
- LLVMの
PointerUnionは、複数のポインタ型のいずれかを保持し、どの型であるかを下位ビットで区別する - レイトレーシングのpbrtは、形状やマテリアルなど多数の型を仮想関数なしで扱うために
TaggedPointerを使う。こちらは下位ビットではなく上位7ビット (ビット57以上) に型の番号を格納し、128種類までの型を判別して、型に応じた処理を呼び分ける - GHC (Haskell)は、クロージャへのポインタの下位ビット (64ビット環境で3ビット、32ビット環境で2ビット) に、データ構築子の番号や関数のアリティを格納する。クロージャを辿らずに構築子を判定できるため、間接ジャンプを減らせる (Faster laziness using dynamic pointer tagging)
みっつめは、ポインタか即値かを判別する使い方である。1ワードにポインタと小さな値のどちらかを入れ、どちらであるかをタグで表す。
- Rubyの
VALUEは、最下位ビットが1ならFixnum、下位2ビットが10ならFlonumというように、下位3ビットで即値かどうかを判別する。いずれのビットも立っていなければオブジェクトへのポインタである - OCamlの
valueは、最下位ビットが1なら63ビット (32ビット環境では31ビット) の整数、0ならヒープ上のブロックへのポインタとする - V8 (JavaScript)は、最下位ビットが0ならSmi (small integer)、1ならヒープオブジェクトへのポインタとする
本クラスが保持するのはポインタとタグの組なので、直接あてはまるのはひとつめとふたつめである。みっつめの即値との判別は、ポインタを置く領域に整数そのものを入れるため、本クラスでは表現されない。
タグを埋め込む位置
アライメントによって常に0になる下位ビットを使う方法は、指す先の型がわかれば何ビット空いているかが決まるため、移植しやすい。本クラスが提供するのもこの方法である。
pbrtのように上位ビットを使う方法は、アドレス空間が64ビット全体を使わないことに依存するため、対象とする環境を限定する。上位ビットを無視するハードウェア機構としては、IntelのLAM (linear address masking)、AMDのUAI (upper address ignore)、ARMのTBI (top byte ignore) などがある。
メンバ関数
構築・破棄
| 名前 | 説明 | 対応バージョン |
|---|---|---|
(constructor) |
コンストラクタ | C++29 |
静的メンバ関数
| 名前 | 説明 | 対応バージョン |
|---|---|---|
from_overaligned |
過剰アライメントされたポインタから構築する | C++29 |
from_tagged |
タグ付きポインタ値から復元する | C++29 |
値の取得
| 名前 | 説明 | 対応バージョン |
|---|---|---|
pointer |
ポインタ値を取得する | C++29 |
tag |
タグ値を取得する | C++29 |
tagged_pointer |
タグを埋め込んだままのポインタ値を取得する | C++29 |
入れ替え
| 名前 | 説明 | 対応バージョン |
|---|---|---|
swap |
他のpointer_tag_pairオブジェクトと値を入れ替える |
C++29 |
非メンバ(Hidden friends)関数
比較演算子
| 名前 | 説明 | 対応バージョン |
|---|---|---|
operator<=> |
三方比較を行う | C++29 |
operator== |
等値比較を行う | C++29 |
非メンバ関数
| 名前 | 説明 | 対応バージョン |
|---|---|---|
get |
ポインタ値またはタグ値を取得する | C++29 |
メンバ型
| 名前 | 説明 | 対応バージョン |
|---|---|---|
pointer_type |
ポインタの型Ptr |
C++29 |
element_type |
指す先の型。pointer_traits<Ptr>::element_type |
C++29 |
tagged_pointer_type |
タグを埋め込んだままのポインタの型。PtrのCV修飾を維持したvoid* |
C++29 |
tag_type |
タグの型TagT |
C++29 |
メンバ定数
| 名前 | 説明 | 対応バージョン |
|---|---|---|
static constexpr unsigned int bits_requested |
タグに要求したビット数BitsRequested |
C++29 |
その他
| 名前 | 説明 | 対応バージョン |
|---|---|---|
tuple_size |
pointer_tag_pairでの特殊化(要素数2) |
C++29 |
tuple_element |
pointer_tag_pairでの特殊化(0番目がpointer_type、1番目がtag_type) |
C++29 |
例
基本的な使い方
#include <memory>
#include <iostream>
int main()
{
int x = 42;
// intのアライメントは通常4なので、下位2ビットをタグに使える
std::pointer_tag_pair<int*, 2> p{&x, 0b10u};
static_assert(sizeof(p) == sizeof(int*));
std::cout << *p.pointer() << std::endl;
std::cout << p.tag() << std::endl;
}
出力
42
2
タグが下位ビットに埋め込まれていることを確認する
#include <memory>
#include <iostream>
#include <cstdint>
#include <bit>
int main()
{
int x = 42;
std::pointer_tag_pair<int*, 2> p{&x, 0b10u};
// タグを埋め込んだままの生のポインタ値を整数として観察する。
// 埋め込みの表現は未規定だが、多くの実装ではアライメントによって
// 常に0になる下位ビットへ格納される
auto tagged = std::bit_cast<std::uintptr_t>(p.tagged_pointer());
auto addr = std::bit_cast<std::uintptr_t>(&x);
std::cout << (tagged & 0b11u) << std::endl; // 下位2ビット : タグの値
std::cout << ((tagged & ~std::uintptr_t{0b11u}) == addr) << std::endl; // 残り : 元のアドレス
}
出力例
2
1
所有しているかどうかを印付けするスマートポインタ
#include <memory>
#include <iostream>
// 「所有するポインタ」と「参照するだけのポインタ」の両方になれる型。
// フラグを別メンバに持つ実装と違い、サイズはポインタ1個分で済む
template <typename T>
class maybe_owning_ptr {
enum class ownership : unsigned int { reference, owning };
std::pointer_tag_pair<T*, 1, ownership> ptr_;
public:
explicit maybe_owning_ptr(T*&& p) noexcept : ptr_{p, ownership::owning} {}
explicit maybe_owning_ptr(T& r) noexcept : ptr_{&r, ownership::reference} {}
T& operator*() const noexcept { return *ptr_.pointer(); }
~maybe_owning_ptr() {
if (ptr_.tag() == ownership::owning) {
delete ptr_.pointer();
}
}
};
static_assert(sizeof(maybe_owning_ptr<int>) == sizeof(int*));
int main()
{
int local = 1;
maybe_owning_ptr<int> ref{local}; // 参照するだけ。deleteされない
maybe_owning_ptr<int> own{new int(2)}; // 所有する。デストラクタでdeleteされる
std::cout << *ref << *own << std::endl;
}
出力
12
木構造で内部ノードと葉を区別する
#include <memory>
#include <iostream>
// 二分木のノードへのポインタの下位1ビットに「葉かどうか」を埋め込む。
// ノード側に種別のメンバを持つ必要がなく、葉には子配列も不要になる
struct Leaf {
int value;
};
struct Inner;
using node_ptr = std::pointer_tag_pair<void*, 1, bool>; // タグ : 葉ならtrue
struct Inner {
node_ptr children[2];
};
int sum(node_ptr node)
{
if (node.tag()) {
return static_cast<Leaf*>(node.pointer())->value;
}
auto* inner = static_cast<Inner*>(node.pointer());
return sum(inner->children[0]) + sum(inner->children[1]);
}
int main()
{
Leaf a{1}, b{2}, c{3};
Inner left{{node_ptr{&a, true}, node_ptr{&b, true}}};
Inner root{{node_ptr{&left, false}, node_ptr{&c, true}}};
std::cout << sum(node_ptr{&root, false}) << std::endl;
}
出力
6
バージョン
言語
- C++29
処理系
- Clang: ??
- GCC: ??
- Visual C++: ??