目標とする名前付き引数の構文
void func(Arg<"first", int> first, Arg<"second", int> second); // #1
void func(Arg<"second", int> second, Arg<"first", int> first); // #2
このような感じで欲しい関数をオーバーロードすることにより以下のような呼び出し方を許可したい。
func("first"_arg = 0, "second"_arg = 88); // ok #1が呼ばれる
func("second"_arg = 99, "first"_arg = 0); // ok #2が呼ばれる
なぜC++20からなの?
C++20から、非型テンプレートパラメータとして条件はあるがクラス型が許可された。
そのため、1文字ずつ可変引数テンプレートで渡さずとも、文字列を非型テンプレートで直接受け取ることが可能になった。
template<typename CharT, std::size_t N>
struct basic_string_literal {
CharT data[N];
consteval basic_string_literal(const CharT (&str)[N]) {
for (std::size_t i = 0; i < N; ++i) data[i] = str[i];
}
};
template<basic_string_literal F>
struct S{};
int main() {
S<""> s;
S<u8""> u8_s;
S<u""> u16_s;
S<U""> u32_s;
}
これを使ってやりたい放題したいと思う。Boost Parameter Libraryもあるが、マクロによる構文を必要としているので、プリプロセッサに頼らないC++のみの機能でやることを目標にした。
解説
必要なもの
-
_arg
のユーザ定義リテラル -
_arg
で生成したリテラルはoperator=
で渡された右辺の変数をバインドできる構造体でなければならない - 変数バインドした構造体が関数の引数に渡されるので、その型を簡単に書ける型エイリアス
実装
必要なもの(1)で変数テンプレートに文字列を渡すとchar (&)[N]
の形で渡されるので、文字列リテラルを構造体basic_arg_name
で引き回せるようにしてやる。
template<typename CharT, std::size_t N>
struct basic_arg_name {
CharT data[N];
consteval basic_arg_name(const CharT (&str)[N]) { for (std::size_t i = 0; i < N; ++i) data[i] = str[i]; }
constexpr operator std::basic_string_view<CharT>() const {
return std::basic_string_view<CharT>{ data, N - 1 };
}
};
この構造体basic_arg_name
は文字列を使ったタグ程度としか機能しないので、必要なもの(2)を満たすような構造体arg_storage_builder
を、ユーザー定義リテラル_arg
は返す必要がある。
template<auto Name>
struct arg_storage_builder {
static constexpr auto name = Name;
template<typename T>
constexpr auto operator=(T&& value) const {
return arg_storage<T, name>{std::forward<T>(value)};
}
};
inline namespace literal {
template<basic_arg_name str>
consteval auto operator"" _arg() {
return arg_storage_builder<str>{};
}
}
また、この地点ではarg_storage_builder
は実際に渡される引数の型や確保領域を持っていない。そのためoperator=
の引数に渡されたものの型と値を保持し、別の構造体に保存し関数に渡す必要がある。その構造体がarg_storage
である。
template<typename T, auto Name>
struct arg_storage {
using value_type = T;
static constexpr auto name = Name;
T value;
arg_storage(T v) : value(v){}
template<typename Cast, std::enable_if_t<std::is_convertible_v<T, Cast>, std::nullptr_t> = nullptr>
constexpr operator arg_storage<Cast, Name>()&& {
return arg_storage<Cast, Name>{static_cast<Cast>(std::forward<T>(value))};
}
};
この構造体のvalue
メンバ変数にアクセスすることによって"name"_arg = value
の形で保存された値にアクセスすることができる。また、渡された値の型情報をそのまま保持しているので
int a = 0;
"arg_name"_arg = a;
このようにした場合ではoperator=
はarg_storage<int&, "arg_name">
を返すため、このままだとfunc(arg_storage<int, "arg_name">)
のような型変換が可能なはずの引数に渡すことができない。
そのためarg_storage
に型変換演算子のオーバーロードを追加してやることにより解決をしている。
最後に、引数の型に毎回arg_storage<type, "name">
と書くのは面倒くさいので、Arg<"name", type>
で書けるよう型エイリアスを作る。
template<basic_arg_name Name, typename ArgType>
using Arg = arg_storage<ArgType, Name>;
コード全体