最終更新日時:

C++20によるコンパイル時文字列についてのメモ

はじめに

 C++といえばコンパイル時計算であるが、文字列型をテンプレートとして用いるにはちょっとした工夫が必要であった。この面倒さはC++20で改善され、C++標準のstd::stringstd::string_viewで用いることができるわけではないが、お手軽に非型テンプレートとして文字列型を扱うことができるようになった。

 そこで、文字列型をテンプレート引数として扱う際のもっとも基礎となる文字列型に関する実装をメモしておく。

実装

非型テンプレートとして利用可能な文字列型

 非型テンプレートとして利用可能な文字列型については、上で示したサイトの条件を満たすように作成をすればいいため、実装としては以下のようにすればいい。文字列型ということで、加算についての2項演算子をオーバーロードしている。

/// <summary>
/// コンパイル時に計算するための文字列型
/// </summary>
/// <typeparam name="charT">文字型</typeparam>
/// <typeparam name="N">NULL終端を含む文字数</typeparam>
template <class charT, std::size_t N>
struct string {
    charT _str[N] = {};

    using value_type = charT;

    consteval string() {}
    consteval string(const charT(&str)[N]) {
        for (std::size_t i = 0; i < N; ++i) {
            this->_str[i] = str[i];
        }
    };

    constexpr explicit operator std::basic_string<charT>() const {
        return std::basic_string<charT>(this->_str, N);
    }

    constexpr explicit operator std::basic_string_view<charT>() const {
        return std::basic_string_view<charT>(this->_str, N);
    }
};
template <class charT, std::size_t M, std::size_t N>
inline consteval string<charT, M + N - 1> operator+(const string<charT, M>& lhs, const string<charT, N>& rhs) {
    string<charT, M + N - 1> ret;
    for (std::size_t i = 0; i < M; ++i) {
        ret._str[i] = lhs._str[i];
    }
    for (std::size_t i = 0; i < N; ++i) {
        ret._str[M - 1 + i] = rhs._str[i];
    }
    return ret;
}
C++

 ただ、この実装だけでは非型テンプレートとして利用する際に毎回string<char, 4>("abc")のように記載する必要があり非常に面倒なため、以下のような推論補助も与えておく。

template <std::size_t N>
string(const char(&)[N]) -> string<char, N>;
template <std::size_t N>
string(const char8_t(&)[N]) -> string<char8_t, N>;
template <std::size_t N>
string(const char16_t(&)[N]) -> string<char16_t, N>;
template <std::size_t N>
string(const char32_t(&)[N]) -> string<char32_t, N>;
template <std::size_t N>
string(const wchar_t(&)[N]) -> string<wchar_t, N>;
C++

 利用例としては、以下のようになる(クラステンプレートのテンプレート引数推論が超便利)。

// 特にテンプレートテンプレートパラメータのように指定せずに非型テンプレートとして与える
template <string name>
struct test {};

// 以下のような記載をすることで利用可能
using type1 = test<"test">;
using type2 = test<u8"test">;
using type3 = test<u"test">;
using type4 = test<U"test">;
using type5 = test<L"test">;
C++

型文字型を指定して対応する文字列を得る

 文字列型を非型テンプレートとして利用したいとき、割と上記のstringの実装だけでは機能的な不足が多い。代表的なものとしては<algorithm>ヘッダの関数に相当する操作が挙げられるかもしれないが、ほとんどの関数はconstexprに対応しているため別に何も困らない。実際に対応に困るものとなると、例えば以下のようなメタ関数を作成して、結果の文字列(append_test::value)を利用しようとしたとき、文字型がcharの場合しか利用できなくなる。

template <string name>
struct append_test {
    static constexpr auto value = name + string("-test");
};

// 有効
std::cout << (append_test<"str">::value)._str << std::endl;
// コンパイルエラー
std::wcout << (append_test<L"str">::value)._str << std::endl;
C++

 これをどうにかするには、append_testのテンプレート引数からL"-test"という文字列を生成するように分岐をすればいいのだが、文字列を生成する処理は別途用意した方がいいだろう。これを実装する方法について少し悩んだが、以下を参照することでひとまず解決はした。やはり、原理上マクロを利用しないとスマートな記述方法は実現できないようである。

 今回のstringに対してであれば、以下のように実装をすればいいだろう。

template <class charT, class... CharTypes>
inline consteval auto select_type(const CharTypes&... strs) {
    // std::variantを経由してstrsの中からcharTに該当する文字列のインデックスを取得
    constexpr std::variant<std::remove_extent_t<CharTypes>...> v = charT();
    constexpr std::size_t i = v.index();
    // stringを返す
    return std::get<i>(std::make_tuple(string(strs)...));
}

#define PP_STRING_LITERAL(charT, str) select_type<charT>((str), (u8##str), (u##str), (U##str), (L##str))
C++

 これを用いることで、append_testは以下のように実装しなおすことができ、あらゆる文字型に対応させることができる。

template <string name>
struct append_test {
    using name_type = decltype(name)::value_type;
    static constexpr auto value = name + PP_STRING_LITERAL(name_type, "-test");
};

// 有効
std::cout << (append_test<"str">::value)._str << std::endl;
// 有効
std::wcout << (append_test<L"str">::value)._str << std::endl;
C++

おわりに

 これでコンパイル時の文字列操作が型を気にせず自由にできると思う。もちろん、サロゲートペアなど文字コード固有の面倒な扱いはあるが、必要であればその部分を特殊化すればいいだけである。そのうち、いい感じにコンパイル時文字列操作による何かを作りたいところである。