[C++]C++20/23時代でも独自の名前付き引数は必要だった。

動機

C++20では指示付き初期化が導入された。これは集成体初期化を行う際にメンバ変数名を明示的に指定しつつ初期化する方法で、ありがたいことに、極めて簡単にC++での疑似名前付き引数として利用できる。
ただし指示付き初期化にも難点が多数ある。まず初期化子の順序が必ず宣言順でなければならないことだ。

struct Params
{
    int a = 1;
    double b = 10.;
    std::string c;
}
void func(Params p)
{
    std::cout << std::format("{} {} {}\n", p.a, p.b, p.c);
}
int main()
{
    func({ .a = 2, .c = "abc" });//ok
    func({ .c = "abc", .a = 2 });//error
}

そして、初期化する集成体が基底クラスを持つ場合に、その初期化が非常に面倒である点も腹立たしい。

struct ParamA
{
    int a;
};
struct ParamsB : ParamA
{
    double b;
};

int main()
{
    // 基底クラスは明示的に{ ... }と指定する必要があり、
    // .a = 1, .b = 4のように初期化できない。
    ParamsB p{ { .a = 1 }, 4 };//ok
    ParamsB p{ .a = 1, .b = 4 };//error
}

参照型引数の扱いも厄介である。クラスが参照型メンバ変数を持つとその変数は必ず初期化しなければならないので、「この引数は参照で受け取るが、与えても与えなくてもいい」といった使い方をする場合に困る。デフォルトではstaticメンバ変数などを参照させるというちょっと際どい手段はあるかもしれないが、危なすぎて私はやりたくない。

そんなわけで、自作ライブラリの中で指示付き初期化を導入しようとあれこれ考えるも断念し、結局自前の名前付き引数を継続して使用することにした。もう何年も前、C++14でメタプログラミングを覚え悪戦苦闘していた頃に作り、使い続けてきた機能だ。とはいえ時代は既にC++20/23なので、コンセプトなどを使って一部改修した。昔使っていたものに比べれば幾分シンプルになった。折角なので記事に残しておく。

実装

#ifndef KEYWORD_ARGS_H
#define KEYWORD_ARGS_H

#include <utility>
#include <tuple>
#include <type_traits>
#include <concepts>

template <class> struct AlwaysTrue {};

//任意の型を受け取ることのできるキーワードを作りたい場合、これを与える。
template <template <class> class Concept = AlwaysTrue>
class AnyTypeKeyword {};

template <class Name_, class Type_, class Tag_>
struct KeywordValue
{
    using Name = Name_;
    using Type = Type_;
    using Tag = Tag_;

    constexpr KeywordValue(Type v) : m_value(std::forward<Type>(v)) {}
    constexpr Type GetValue() { return std::forward<Type>(m_value); }
    template <class T>
    constexpr bool Is() const { return std::is_same<T, Type>::value; }
private:
    Type m_value;
};

template <class Name, class Type, class Tag>
struct KeywordName;
template <class Name_, class Type_, class Tag_>
struct KeywordName
{
    using Name = Name_;
    using Type = Type_;
    using Tag = Tag_;
    using Value = KeywordValue<Name, Type, Tag>;

    constexpr KeywordName() {}
    constexpr Value operator=(Type v) const { return Value(std::forward<Type>(v)); }
};
template <class Name_, template <class> class Concept_, class Tag_>
struct KeywordName<Name_, AnyTypeKeyword<Concept_>, Tag_>
{
    using Name = Name_;
    using Tag = Tag_;

    constexpr KeywordName() {}
    template <class Type, class = Concept_<Type>>
    constexpr KeywordValue<Name, Type&&, Tag> operator=(Type&& v) const
    {
        return KeywordValue<Name, Type&&, Tag>(std::forward<Type>(v));
    }
};
template <class Name_, class Tag_>
struct KeywordName<Name_, bool, Tag_>
{
    using Name = Name_;
    using Type = bool;
    using Tag = Tag_;
    using Value = KeywordValue<Name, bool, Tag>;

    //キーワード名インスタンスのみが与えられている場合、trueとして扱う。
    static constexpr bool GetValue() { return true; }
    constexpr Value operator=(bool v) const { return Value(v); }
};

template <template <class...> class Base, class Derived>
struct IsBaseOf
{
    template <class ...U>
    static constexpr std::true_type check(const Base<U...>*);
    static constexpr std::false_type check(const void*);

    static const Derived* d;
public:
    static constexpr bool value = decltype(check(d))::value;
};

template <template <class...> class Base, class Derived>
inline constexpr bool IsBaseOf_v = IsBaseOf<Base, Derived>::value;

template <class T, template <class...> class U>
concept derived_from = IsBaseOf_v<U, T>;

template <class Name>
concept keyword_name = derived_from<std::remove_cvref_t<Name>, KeywordName>;

template <class Option>
concept keyword_value = derived_from<std::remove_cvref_t<Option>, KeywordValue>;
template <class Option>
concept keyword_name_of_bool = keyword_name<Option> && std::same_as<typename std::remove_cvref_t<Option>::Type, bool>;

template <class Option>
concept keyword_arg = keyword_value<Option> || keyword_name_of_bool<Option>;

template <class Option, class ...Tags>
concept keyword_arg_tagged_with = keyword_arg<Option> && (std::same_as<typename std::remove_cvref_t<Option>::Tag, std::remove_cvref_t<Tags>> || ...);

template <class Option, class KeywordName>
concept keyword_arg_named = keyword_name<KeywordName> && keyword_arg<Option> &&
std::same_as<typename std::remove_cvref_t<KeywordName>::Name, typename std::remove_cvref_t<Option>::Name>;

namespace detail
{

class EmptyClass {};
template <class...> struct TypeList {};

template <keyword_name KeywordName>
constexpr bool KeywordExists_impl(KeywordName) { return false; }
template <keyword_name KeywordName, keyword_arg Arg, keyword_arg ...Args>
constexpr bool KeywordExists_impl(KeywordName, Arg&&, [[maybe_unused]] Args&& ...args)
{
    if constexpr (keyword_arg_named<Arg, KeywordName>) return true;
    else return KeywordExists_impl(KeywordName{}, std::forward<Args>(args)...);
}

template <keyword_name KeywordName, class Default>
constexpr decltype(auto) GetKeywordArg_impl(KeywordName, [[maybe_unused]] Default&& dflt)
{
    if constexpr (std::same_as<std::remove_cvref_t<Default>, EmptyClass>) throw std::exception("Default value does not exist.");
    else return std::forward<Default>(dflt);
}
template <keyword_name KeywordName, class Default, keyword_arg Arg, keyword_arg ...Args>
constexpr decltype(auto) GetKeywordArg_impl(KeywordName name, [[maybe_unused]] Default&& dflt, [[maybe_unused]] Arg&& arg, [[maybe_unused]] Args&& ...args)
{
    if constexpr (keyword_arg_named<Arg, KeywordName>) return static_cast<std::remove_cvref_t<Arg>::Type>(arg.GetValue());
    else return GetKeywordArg_impl(name, std::forward<Default>(dflt), std::forward<Args>(args)...);
}

}

template <keyword_name KeywordName, keyword_arg ...Args>
constexpr bool KeywordExists(const KeywordName& name, Args&& ...args)
{
    return detail::KeywordExists_impl(name, std::forward<Args>(args)...);
}

//該当するキーワードから値を取り出して返す。
//同じキーワードが複数与えられている場合、先のもの(左にあるもの)が優先される。
template <keyword_name KeywordName, keyword_arg ...Args>
constexpr decltype(auto) GetKeywordArg(KeywordName name, Args&& ...args)
{
    return detail::GetKeywordArg_impl(name, detail::EmptyClass{}, std::forward<Args>(args)...);
}
template <keyword_name KeywordName, keyword_arg ...Args>
constexpr decltype(auto) GetKeywordArg(KeywordName k, typename KeywordName::Type default_, Args&& ...args)
{
    //該当するキーワードから値を取り出して返す。
    //同じキーワードが複数与えられている場合、先のもの(左にあるもの)が優先される。
    return detail::GetKeywordArg_impl(k, std::forward<typename KeywordName::Type>(default_), std::forward<Args>(args)...);
}

#define DEFINE_KEYWORD_ARG(NAME, TYPE)\
inline constexpr auto NAME = KeywordName<struct _##NAME, TYPE, void>();

#define DEFINE_TAGGED_KEYWORD_ARG(NAME, TYPE, TAG)\
inline constexpr auto NAME = KeywordName<struct _##NAME, TYPE, TAG>();

#endif

使い方はだいたい次のような感じである。

#include <iostream>
#include <vector>
#include <string>
#include "KeywordArgs.h"

namespace args
{

//キーワード引数を定義する。
DEFINE_KEYWORD_ARG(lvec, std::vector<int>&);
DEFINE_KEYWORD_ARG(rvec, std::vector<double>&&);
DEFINE_KEYWORD_ARG(flt, float);
DEFINE_KEYWORD_ARG(str, std::string_view);

//制約付きで任意の型を受け取ることのできるキーワード引数を定義する。
template <class Int> requires std::integral<std::remove_cvref_t<Int>>//完全転送の形で受け取るので、remove_cvref_tを使う。
struct AnyInt {};
DEFINE_KEYWORD_ARG(anyint, AnyTypeKeyword<AnyInt>);//任意の整数型。

//短縮記法。
inline const auto f3 = (flt = 3.0f);
inline const auto sa = (str = "aaaaa");
//inline const auto rv123 = (rvec = std::vector<double>{1, 2, 3});//ダングリング参照に注意。

}

template <keyword_arg ...Args>
void func(Args ...args)
{
    //キーワード引数を受け取る。
    std::vector<int>& lvec = GetKeywordArg(args::lvec, std::forward<Args>(args)...);
    for (auto& i : lvec) std::cout << i << " ";
    std::cout << std::endl;
    for (auto& i : lvec) i = i * 2;
    std::vector<double> rvec = GetKeywordArg(args::rvec, std::forward<Args>(args)...);
    for (auto& i : rvec) std::cout << i << " ";
    std::cout << std::endl;

    //キーワードの有無を確認する。
    if constexpr (KeywordExists(args::flt, args...))
        std::cout << "flt found. ";
    else
        std::cout << "flt not found. default value is used.\n";
    //キーワード引数が与えられていない場合、デフォルト値1.0を使う。
    float flt = GetKeywordArg(args::flt, 1.0f, std::forward<Args>(args)...);
    std::cout << flt << std::endl;

    auto anyint = GetKeywordArg(args::anyint, std::forward<Args>(args)...);
    std::cout << typeid(anyint).name() << ", " << anyint << std::endl;

    std::cout << GetKeywordArg(args::str, std::forward<Args>(args)...) << std::endl;
}

int main()
{
    std::vector<int> lv = { 1, 2, 3 };
    std::vector<double> rv = { 1.1, 2.2, 3.3 };
    func(args::lvec = lv, args::rvec = std::move(rv), args::anyint = 15ll, args::sa);

    for (auto i : lv) std::cout << i << " ";//funcによってlvの要素が2倍されている
    std::cout << std::endl;
    for (auto i : rv) std::cout << i << " ";//rvはムーブされているので空
    std::cout << std::endl;

    return 0;
}

また、キーワード引数にタグを設定し、特定のタグのものだけを受け取るよう範囲調整する機能を設けている。

namespace args
{

struct Param1 {};
struct Param2 {};
DEFINE_TAGGED_KEYWORD_ARG(param1_int, int, Param1);
DEFINE_TAGGED_KEYWORD_ARG(param1_dbl, double, Param1);
DEFINE_TAGGED_KEYWORD_ARG(param2_int, int, Param2);
DEFINE_TAGGED_KEYWORD_ARG(param2_dbl, double, Param2);

struct Param3 {};
DEFINE_TAGGED_KEYWORD_ARG(param3_int, int, Param3);

}

//Param1または2をタグとして持つ引数だけ与えられる。
template <keyword_arg_tagged_with<args::Param1, args::Param2> ...Args>
void func2(Args&& ...args)
{
    std::cout << GetKeywordArg(args::param1_int, std::forward<Args>(args)...) << std::endl;
}

int main_()
{
    func2(args::param1_int = 1);//OK
    //Func2(args::param3_int = 1);//error
    return 0;
}

指示付き初期化と比べると、色々とメリットはある。

  • 順序が任意。
  • タグの継承関係で対応するオプションの範囲を調整できる。
  • 任意の型を受け取ることができる。
  • 参照受け取りに困らない。
  • 短縮記法なども可能(ただしダングリングには注意)。

一方で、実用する上では名前空間で括ることがほぼ必須なので、記述が長たらしくなりがちだし、名前空間を汚染しやすいのもデメリットだ。簡素なプリミティブ型ばかりのパラメータを受け取る場合などは、指示付き初期化のほうがずっと気軽に使えるだろう。

閑話

本機能はGnuplotラッパーの部分改修およびADAPTへの統合のために作り直したものだ。なのであちらのライブラリで必要になった機能しか実装していないし、AnyTypeKeywordなどはかなりいい加減な作りになっている。しかし自分で使う分にはまあ困ることはないだろう。

まあ所詮は、ネット上にゴロゴロ転がっているC++用名前付き引数の亜種に過ぎない。指示付き初期化で満足できない奇特な人の参考になってくれたら幸いである。私は指示付き初期化でもネット上で紹介されている数多の名前付き引数実装でも満足できなかったので自作するしかなかった。C++も悩ましい言語だなぁといつも思う。