yohhoyの日記

技術的メモをしていきたい日記

constexpr Two-Step

C++20からサポートされたコンパイル時動的確保データを、コンパイル時定数としてプログラム実行時へと効率的に持ち越すテクニック。

C++23現在の言語仕様では、コンパイルフェーズで動的確保(new)されたデータ領域はコンパイルフェーズにおいて解放(delete)されなければならない(“transient constexpr allocation”)。この制約によりstd::string*1std::vector型の定数初期化はコンパイルエラー(ill-formed)となる。データ領域の動的確保/解放を伴わないstd::array型ではプログラマの期待通りに定数初期化が行われる。

#include <array>
#include <string>
#include <vector>

constexpr std::string get_str()
{ return std::string(42, 'X'); }

constexpr std::vector<int> get_vec()
{ return {1, 2, 3}; }

constexpr std::array<int, 3> get_arr()
{ return {1, 2, 3}; }

static_assert(get_str() != "C++");  // OK
static_assert(get_vec()[0] == 1);   // OK
static_assert(get_arr()[0] == 1);   // OK

constexpr std::string str = get_str();  // NG: ill-formed
constexpr std::vector vec = get_vec();  // NG: ill-formed
constexpr std::array arr = get_arr();   // OK

前掲コードのうちstd::arrayの例が示す通り、コンパイル時に静的確保された領域ならば実行時へと値を持ち越せる。つまりコンパイル時に動的確保したデータを十分な要素サイズもつ静的領域へコピーしておき、後者を実行時に持ち越す実装方式が考えられる。同方式のデメリットとして必要な静的領域サイズを事前計算はできず、大きめの領域を確保する必要があるためにメモリ使用効率が犠牲になってしまう。

C++20/23: constexpr Two-Step技法

コンパイル時から実行時に持ち越す静的領域サイズを必要最小限とするため、0) コンパイル時に動的確保データを構築し、1) まず十分に大きい静的領域へとコピーしてから、2) 必要十分サイズの静的領域へとコピーする(“constexpr Two-Step”)。最後にconsteval関数テンプレートの非型テンプレートパラメータ*2に指定することで、静的領域を実行時にも参照可能とする。

#include <algorithm>
#include <array>
#include <string>
#include <string_view>

// コンパイル時にstd::stringを構築する関数
constexpr std::string my_data();

template <size_t N>
struct Storage {
  std::array<char, N> contents = {};
  size_t length = 0;

  template <class R>
  constexpr Storage(R&& r)
    : length(std::ranges::size(r))
  {
    std::ranges::copy(r, contents.data());
  }

  constexpr auto begin() const -> char const*
    { return contents.data(); }
  constexpr auto end() const -> char const*
    { return contents.data() + length; }
};

template <Storage V>
consteval auto promoted_value() {
  return std::string_view(V);  // C++23
  // C++20: std::string_view(V.begin(), V.end());
}

template <auto F>
constexpr std::string_view promote_to_static() {
  constexpr auto oversized_storage = Storage<255>(F());  // ★
  constexpr auto correctly_sized_storage =
    Storage<oversized_storage.length>(oversized_storage);
  return promoted_value<correctly_sized_storage>();
}

constexpr std::string_view s = promote_to_static<[]{ return my_data(); }>();

上記コード★箇所のバッファサイズ255は、コンパイル時に計算される文字列長よりも十分大きな値であればよい。バッファサイズ不足の場合はコンパイルエラーとして検出される。

C++2c Reflection

C++ Reflection(→id:yohhoy:20250305)の一環としてC++2c(C++26)へ採択*3された提案文書P3491R3では、コンパイル時の確保領域をプログラム実行時に参照可能な領域へと昇格させる関数群を追加する。これによりC++2c以降ではconstexpr Two-Step技法は不要となる。

// C++2c(C++26)
#include <meta>  // Reflection(P2996,P3491,...)

constexpr std::string my_data();

constexpr std::string_view s = std::define_static_string(my_data());  // OK

新しい標準ヘッダ<meta>に追加される関数*4

std::define_static_string
コンパイル時の文字型(CharT)Rangeから生成した文字列リテラルへのポインタ(const CharT*)を返すconsteval関数*5
std::define_static_array
コンパイル時の要素型(T)Rangeから生成した静的配列への参照(std::span<const T>)を返すconsteval関数
std::define_static_object
コンパイル時の構造的型(structural type)Tオブジェクトから生成した静的オブジェクトへのポインタ(const T*)を返すconsteval関数

提案文書P3491によれば、将来的にコンパイル時動的確保データを実行時へ直接持ち越し可能とする(“Non-transient constexpr allocation”)仕様拡張や、std::stringstd::vectorを非型テンプレートパラメータ(non-type template parameter)指定可能に制約緩和されるまでの過渡的ソリューションと位置付けている*6。下記提案がC++2d(C++29)向けて検討中ステータスにある。

関連URL

*1:例示コードのうち get_str 関数で構築する std::string 文字列長が十分短いときはSSO(→id:yohhoy:20120428)が行われ、副次効果としてコンパイル時に構築した文字列を実行時に持ち越せるケースがある。SSO判定閾値C++標準ライブラリの実装依存となるため、この動作仕様に依存するプログラムは可搬性が損なわれる。

*2:採択済み提案文書(PDF)P2841R7により、C++2c仕様では旧来の "non-type template parameter" は "constant template parameter" へと名称変更される。https://github.com/cplusplus/draft/pull/7587

*3:https://github.com/cplusplus/papers/issues/2158

*4:P3491は文字型ポインタが文字列リテラルを指す否かを判定する std::is_string_literal 関数も追加するが、本文趣旨と無関係なためここでは省略。

*5:std::define_static_string 関数が生成する文字列リテラルはNUL文字終端されることを示すため、戻り値型が std::string_view ではなく const CharT* と定義されている。

*6:P3491: §1 "So having facilities to solve these problems until the general language solution arises is very valuable.", §3.5 "Given non-transient allocation and a std::string and std::vector that are usable as non-type template parameters, this paper likely becomes unnecessary."

*7:https://github.com/cplusplus/papers/issues/867

*8:https://github.com/cplusplus/papers/issues/1336

*9:https://github.com/cplusplus/papers/issues/2037