- cpp20[meta cpp]
C++20より、定数式における動的メモリ確保と解放が許可される。それに伴い、std::vector
とstd::string
の全メンバ関数がconstexpr
対応し、定数式で使用できるようになる。
constexpr int test_vector() {
std::vector<int> v = {5, 3, 2, 9, 1, 0, 4};
v.push_back(11);
int s{};
for(auto n : v) {
s += n;
}
return s;
}
constexpr bool check_cpp_file(const std::string& filename) {
return filename.ends_with(".cpp") || filename.ends_with(".hpp");
}
static_assert(test_vector() == 35); // OK
static_assert(check_cpp_file("main.cpp")); // OK
- ends_with[link /reference/string/basic_string/ends_with.md]
これは主に以下の変更によって達成されている。
- デストラクタの
constexpr
対応 new/delete
式のconstexpr
対応std::allocator/std::allocator_traits
のconstexpr
対応
デストラクタにconstexpr
を付加し、デストラクタを定数式で実行する事が可能となる。これはユーザー定義のデストラクタでも同様。
そのようなconstexpr
デストラクタの本体、およびそのクラスの基底クラスと非静的メンバ変数の全てのデストラクタは定数式で実行可能でなくてはならない。
struct C : base {
// constexprデフォルトデストラクタ
constexpr ~C() = default;
// あるいは定義しても良い
constexpr ~C() {
// 何か定数式で可能な処理
// ...
}
// 全ての基底クラスおよび非静的メンバ変数もまた定数式でデストラクト可能でなければならない
int n;
std::string str;
};
default
指定した時の振る舞いは、constexpr
コンストラクタにdefault
指定した時の振る舞いに準ずる。例えば、default
デストラクタ(特に、トリビアルデストラクタ)はその処理が全て定数式で実行可能であるならば、暗黙的にconstexpr
である。
これに伴って、クラス型のリテラル型はconstexpr
デストラクタを持つ事が追加で要求されるようになる。そして、クラス型のconstexpr
変数は、その型がリテラル型で初期化が定数式で可能であり、かつデストラクタが定数式で実行可能でなくてはならなくなる。
C++17までは、クラス型のリテラル型はトリビアルデストラクタを要求されており、そのconstexpr
オブジェクトは初期化が定数式で実行可能であることだけが要求されていた。そのため、C++17までのリテラル型はC++20においてもリテラル型であり、定数式での扱いは変わらない。
なお、クラスが仮想基底クラスを持つ時、デストラクタもコンストラクタもconstexpr
指定することはできない。
定数式では未定義動作を可能な限り検出しコンパイルエラーとしなければならない。operator new/operator delete
やmalloc/free
はその実行に伴ってポインタの再解釈(void*
への/からのキャスト)が必要となるが、ポインタの再解釈は検出しづらい未定義動作に繋がりうるため定数式では禁止されている。
そのため、そのようなポインタの再解釈が発生しない動的メモリ確保機能であるnew/delete
式がコンパイル時の動的メモリ確保・解放の方法として許可される。new/delete
式はoperator new/operator delete
とは異なり、メモリの確保・解放とその領域のオブジェクト構築・破棄を一挙に行う言語機能である。
constexpr int f() {
// 確保と構築
int* p = new int;
*p = 20;
int n = *p;
// 破棄と解放
delete p;
return n;
}
当然ながら、new/delete
式によって動的メモリ確保しようとする型はリテラル型であり、呼び出されるコンストラクタとデストラクタは共に定数式で実行可能でなければならない。
また、コンパイル時に実行されるnew
式はグローバルのオーバーロード可能なoperator new
を呼び出すものでなくてはならない。そうではないnew
式の定数式における評価はコンパイルエラーとなる。
struct S {
int n = 10;
// 仮に定数式で実行可能なように定義されていたとしても
constexpr void* operator new(std::size_t n);
constexpr void operator delete(void* p) noexcept;
};
constexpr int f() {
S* s = new S{}; // NG、ユーザー定義operator newの呼び出し
s->n = 20;
int n = s->n;
delete s;
return n;
}
そして、コンパイル時にnew
式で確保されたメモリ領域は、コンパイル時にdelete
式によって解放されなければならない。その対応が取れていないnew/delete
式の呼び出しは、どちらもコンパイルエラーとなる。
constexpr int f() {
int* p = new int;
*p = 20;
int n = *p;
// 忘れる
//delete p;
return n;
}
int main () {
constexpr int n = f(); // NG、コンパイルエラー
}
したがって、C++20のコンパイル時動的メモリ確保の仕様では、コンパイル時に確保したメモリ領域を実行時へ持ち越すことはできない。
実際には、これらの定数式中のnew
式において呼び出される::operator new()
の評価は常に省略されている。この省略はC++14より許可されている最適化の一環として行われ、スタック領域などのストレージを別途あてがうことで動的メモリ確保を避けるものである。対応するdelete
式における::operator delete()
の呼び出しも同様に省略され、定数式におけるnew/delete
式はメモリの確保と解放が一貫していることのマーカーとしての側面が強くなっている。
constexpr void f() {
// このコードは定数式中で
int* d = new int{2};
delete d;
// たとえば次のようなコードと等価になる
int d{2};
}
実際にはどこのストレージが提供されるかは実装定義である。
標準ライブラリのコンテナ等はnew/delete
式を直接利用するわけではなく、std::allocator_traits
を介してstd::allocator
を使用してメモリ確保・解放とオブジェクト構築・破棄を行う。std::allocator/std::allocator_traits
も見かけ上はポインタの再解釈を必要とせずにそれを行うため、std::allocator/std::allocator_traits
によるメモリ確保周りの機能もまた、コンパイル時の動的メモリ確保・解放の方法として許可される。
std::allocator/std::allocator_traits
ではnew/delete
式とは異なり、メモリの確保・解放(allocate
/deallocate
)とその領域へのオブジェクト構築・破棄(construct
/destroy
)の操作が複合していない。オブジェクト構築・破棄においてはplacement newとpseudo-destructor callが必要となるが、placement newはポインタの再解釈が必要となることから許可されず、そのために不必要であるのでpseudo-destructor callも許可されない。
代わりに、placement newを行うライブラリ機能であるconstruct_at
を追加し、pseudo-destructor callを行うdestroy_at
と共にconstexpr
を付加し定数式で使用可能とする。これらの関数はvoid*
ではなくT*
を取るため、これによってポインタ再解釈を回避しつつplacement newとpseudo-destructor callが定数式で使用可能となる。
そして、std::allocator_traits
のconstruct
とdestroy
はconstruct_at/destroy_at
を呼び出して処理を行うように変更される。なお、これによって実行時の振る舞いが変化することはない。
constexpr int f() {
std::allocator<int> alloc{};
// 確保と構築
int* p = alloc.allocate(1);
p = std::construct_at(p);
*p = 20;
int n = *p;
// 破棄と解放
std::destroy_at(p);
alloc.deallocate();
return n;
}
当然ながら、std::allocator
によって動的メモリ確保しようとする型はリテラル型であり、construct_at/destroy_at
によって呼び出されるコンストラクタとデストラクタは共に定数式で実行可能でなければならない。
また、std::allocator<T>::allocate
が呼び出される場合は必ずその領域はstd::allocator<T>::deallocate
によって解放されていなければならず、deallocate
とconstruct_at
、destroy_at
の引数のT*
のポインタはstd::allocator<T>::allocate
によって確保された領域を指していなければならない。
constexpr int f() {
std::allocator<int> alloc{};
// 確保と構築
int* p = alloc.allocate(1);
p = std::construct_at(p);
*p = 20;
int n = *p;
// 忘れる
//std::destroy_at(p);
//alloc.deallocate();
return n;
}
int main () {
constexpr int n = f(); // NG、コンパイルエラー
}
すなわち、new/delete
式と同様にコンパイル時に確保したメモリ領域を実行時へ持ち越すことはできない。
この規則はまた、std::allocator/std::allocator_traits
によって確保されconstruct_at
によってオブジェクトが構築された領域をdelete
式で解放する事、またはその逆も許可されない事を示している。
constexpr int f() {
std::allocator<int> alloc{};
// 確保と構築
int* p = alloc.allocate(1);
p = std::construct_at(p);
*p = 20;
int n = *p;
// 破棄と解放
delete p; // NG、コンパイルエラー
return n;
}
constexpr int g() {
std::allocator<int> alloc{};
// 確保と構築
int* p = new int;
*p = 20;
int n = *p;
// NG、コンパイルエラー
std::destroy_at(p);
alloc.deallocate();
return n;
}
destroy_at
には類似のファミリとしてdestroy_n
と、それらのrange
版があり(あるいは追加され)、construct_at
もrange
版が同時に追加されるが、それらについてもconstruct_at/destroy_at
と同様の扱いが可能となる。
std::allocator::allocate()
はグローバルの::operator new()
を呼び出すが、この呼び出しはnew
式の時と同様に省略されており、std::allocator::deallocate()
における::operator delete()
の呼び出しも省略されている。この2つもまたnew/delete
式と同様に、メモリの確保と解放が一貫していることのマーカーとしての側面が強くなっている。
結局、C++20のコンパイル時動的メモリ確保は定数式にヒープ領域を導入するものではなく、デフォルトの::operator new
による動的メモリ確保を別の領域をあてがう形に置換することで行われている。
std::vector
をはじめとする可変サイズのコンテナは実行時に非常に有用であるため、同様に定数式においても有用である可能性があり、その必要性がC++コミュニティからも示されいていた(C++Now 2017: Ben Deane & Jason Turner "constexpr ALL the things!"、P0810R0 constexpr in Practiceなど)。
また、静的リフレクション機能の導入にあたっては、コンパイル時に使用可能な可変サイズコンテナおよび可変サイズの文字列型が必要となっていた。例えば、ある型のテンプレート引数をクエリするコードは次のようなものになる
// 型Tのテンプレート引数の情報を取り出す
std::vector<std::metainfo> args = std::meta::get_template_args(reflexpr(T));
※ これは最終的なリフレクション仕様とは異なる可能性がある
これらの流れを受けて、std::vector
とstd::string
を定数式で使用可能とするために、その最大の障壁となっていたメモリの動的確保と解放周りの機能が定数式で使用可能となった。
当初検討されていた仕様では、コンパイル時に確保したメモリ領域を実行時に持ち越すことが可能だった。そのようなメモリ領域の確保と解放はクラス型の内部で閉じている必要はあったが、その条件を満たせば静的ストレージに昇格され実行時環境から参照できるようになる。
しかし、当初のアプローチには2つの問題があった。
実行時に持ち越されるメモリ領域を管理するクラスであってもそのデストラクタでその領域を解放している事が求められていたが、それはコンパイラによるテスト要件であり実行時に領域を持ち越そうとする時、実際にそのデストラクタがコンパイル時に呼ばれることはない。しかしその場合、静的ストレージに昇格される領域の内容はいつどの時点のものが保持されるのかが不透明となる。
当初の仕様ではそれに対処するために、std::mark_immutable_if_constexpr()
という関数を導入し、この関数に領域へのポインタを渡して呼び出すことでコンパイラへのマーカーとし、呼ばれた時点でのメモリ領域を実行時に持ち越すアプローチをとっていた。
template<typename T>
struct sample {
std::allocator<T> m_alloc;
T* m_p;
size_t m_size;
// 非トリビアルconstexprコンストラクタでメモリ領域を確保
template<size_t N>
constexpr sample(T(&p)[N])
: m_alloc{}
, m_p{m_alloc.allocate(N)}
, m_size{N}
{
for(size_t i = 0; i < N; ++i) {
std::construct_at(m_p + i, p[i]);
}
// 実行時に持ち越す領域をコンパイラに伝える
// ここ以降は確保した領域は不変
std::mark_immutable_if_constexpr(m_p);
}
// constexprデストラクタでメモリ領域を解放
constexpr ~sample() {
for(size_t i = 0; i < N; ++i) {
std::destroy_at(m_p + i);
}
m_alloc.deallocate(m_p, m_size);
}
}
constexpr sample<char> str{"Hello."};
// 実行時、strは"Hello"を保持する静的配列を参照するようになる
2つ目の問題は、コンパイル時に確保された領域は実行時にconst
であり書き換えられてはならないが、クラス型のconst
伝播の問題から書き換えが可能となってしまっていたことである。
// 当初の仕様ではOK(unique_ptrがconstexpr対応した場合)
constexpr std::unique_ptr<std::unique_ptr<int>> uui
= std::make_unique<std::unique_ptr<int>>(std::make_unique<int>());
int main() {
std::unique_ptr<int>& ui = *uui; // これができてしまう
ui.reset(); // 静的ストレージの領域をdeleteする?
}
std::unique_ptr
ではそれ自身のconst
性が内部のポインタの参照するオブジェクトまで伝播しないため、コンパイル時に確保されたメモリ領域を参照するようなstd::unique_ptr
からは、可変な参照を取得できてしまう。上記例のようにstd::unique_ptr
がネストしていれば、そのような領域をdelete
することもできてしまっていた。
これらの問題について、std::mark_immutable_if_constexpr()
によるアプローチを標準化委員会が嫌ったことと、2つ目の問題の解決が簡単ではなかった(時間がかかり得た)事から、コンパイル時に確保したメモリを実行時に持ち越すことについてはC++20への導入を見送ることとなった。
- P0784R2 More constexpr containers
- P0784R3 More constexpr containers
- P0784R4 More constexpr containers
- P0784R5 More constexpr containers
- P0784R6 More constexpr containers
- P0784R7 More constexpr containers
- 動的メモリー確保 - 江添亮の入門C++
- N3664 Clarifying Memory Allocation
- P1974R0 Non-transient constexpr allocation using propconst