LoginSignup
4
1

More than 1 year has passed since last update.

C++20でマクロレス名前付き引数を実現する

Last updated at Posted at 2022-12-22

目標とする名前付き引数の構文

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++のみの機能でやることを目標にした。

解説

必要なもの

  1. _argのユーザ定義リテラル
  2. _argで生成したリテラルはoperator=で渡された右辺の変数をバインドできる構造体でなければならない
  3. 変数バインドした構造体が関数の引数に渡されるので、その型を簡単に書ける型エイリアス

実装

必要なもの(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>;

コード全体

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1