動機
C++20では指示付き初期化が導入された。これは集成体初期化を行う際にメンバ変数名を明示的に指定しつつ初期化する方法で、ありがたいことに、極めて簡単にC++での疑似名前付き引数として利用できる。
ただし指示付き初期化にも難点が多数ある。まず初期化子の順序が必ず宣言順でなければならないことだ。
struct Params { int a = 1; double b = 10.; std::string c; } voidfunc(Params p) { std::cout<< std::format("{} {} {}\n", p.a, p.b, p.c); } intmain() { func({ .a = 2, .c = "abc" });//okfunc({ .c = "abc", .a = 2 });//error }
そして、初期化する集成体が基底クラスを持つ場合に、その初期化が非常に面倒である点も腹立たしい。
struct ParamA { int a; }; struct ParamsB : ParamA { double b; }; intmain() { // 基底クラスは明示的に{ ... }と指定する必要があり、// .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_; constexprKeywordValue(Type v) : m_value(std::forward<Type>(v)) {} constexpr Type GetValue() { returnstd::forward<Type>(m_value); } template<class T> constexprboolIs() const { returnstd::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>; constexprKeywordName() {} constexpr Value operator=(Type v) const { returnValue(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_; constexprKeywordName() {} 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として扱う。staticconstexprboolGetValue() { returntrue; } constexpr Value operator=(bool v) const { returnValue(v); } }; template<template<class...> class Base, class Derived> struct IsBaseOf { template<class ...U> staticconstexprstd::true_typecheck(const Base<U...>*); staticconstexprstd::false_typecheck(constvoid*); staticconst Derived* d; public: staticconstexprbool value = decltype(check(d))::value; }; template<template<class...> class Base, class Derived> inlineconstexprbool IsBaseOf_v = IsBaseOf<Base, Derived>::value; template<class T, template<class...> class U> conceptderived_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<typenamestd::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<typenamestd::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<typenamestd::remove_cvref_t<KeywordName>::Name, typenamestd::remove_cvref_t<Option>::Name>; namespace detail { class EmptyClass {}; template<class...> struct TypeList {}; template<keyword_name KeywordName> constexprboolKeywordExists_impl(KeywordName) { returnfalse; } template<keyword_name KeywordName, keyword_arg Arg, keyword_arg ...Args> constexprboolKeywordExists_impl(KeywordName, Arg&&, [[maybe_unused]] Args&& ...args) { ifconstexpr (keyword_arg_named<Arg, KeywordName>) returntrue; elsereturnKeywordExists_impl(KeywordName{}, std::forward<Args>(args)...); } template<keyword_name KeywordName, class Default> constexprdecltype(auto) GetKeywordArg_impl(KeywordName, [[maybe_unused]] Default&& dflt) { ifconstexpr (std::same_as<std::remove_cvref_t<Default>, EmptyClass>) throwstd::exception("Default value does not exist."); elsereturnstd::forward<Default>(dflt); } template<keyword_name KeywordName, class Default, keyword_arg Arg, keyword_arg ...Args> constexprdecltype(auto) GetKeywordArg_impl(KeywordName name, [[maybe_unused]] Default&& dflt, [[maybe_unused]] Arg&& arg, [[maybe_unused]] Args&& ...args) { ifconstexpr (keyword_arg_named<Arg, KeywordName>) returnstatic_cast<std::remove_cvref_t<Arg>::Type>(arg.GetValue()); elsereturnGetKeywordArg_impl(name, std::forward<Default>(dflt), std::forward<Args>(args)...); } } template<keyword_name KeywordName, keyword_arg ...Args> constexprboolKeywordExists(const KeywordName& name, Args&& ...args) { return detail::KeywordExists_impl(name, std::forward<Args>(args)...); } //該当するキーワードから値を取り出して返す。//同じキーワードが複数与えられている場合、先のもの(左にあるもの)が優先される。template<keyword_name KeywordName, keyword_arg ...Args> constexprdecltype(auto) GetKeywordArg(KeywordName name, Args&& ...args) { return detail::GetKeywordArg_impl(name, detail::EmptyClass{}, std::forward<Args>(args)...); } template<keyword_name KeywordName, keyword_arg ...Args> constexprdecltype(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)\inlineconstexprauto NAME = KeywordName<struct _##NAME, TYPE, void>();#define DEFINE_TAGGED_KEYWORD_ARG(NAME, TYPE, TAG)\inlineconstexprauto 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> requiresstd::integral<std::remove_cvref_t<Int>>//完全転送の形で受け取るので、remove_cvref_tを使う。struct AnyInt {}; DEFINE_KEYWORD_ARG(anyint, AnyTypeKeyword<AnyInt>);//任意の整数型。//短縮記法。inlineconstauto f3 = (flt = 3.0f); inlineconstauto sa = (str = "aaaaa"); //inline const auto rv123 = (rvec = std::vector<double>{1, 2, 3});//ダングリング参照に注意。 } template<keyword_arg ...Args> voidfunc(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; //キーワードの有無を確認する。ifconstexpr (KeywordExists(args::flt, args...)) std::cout<< "flt found. "; elsestd::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; } intmain() { 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; return0; }
また、キーワード引数にタグを設定し、特定のタグのものだけを受け取るよう範囲調整する機能を設けている。
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> voidfunc2(Args&& ...args) { std::cout<< GetKeywordArg(args::param1_int, std::forward<Args>(args)...) << std::endl; } intmain_() { func2(args::param1_int = 1);//OK//Func2(args::param3_int = 1);//errorreturn0; }
指示付き初期化と比べると、色々とメリットはある。 * 順序が任意。 * タグの継承関係で対応するオプションの範囲を調整できる。 * 任意の型を受け取ることができる。 * 参照受け取りに困らない。 * 短縮記法なども可能(ただしダングリングには注意)。
一方で、実用する上では名前空間で括ることがほぼ必須なので、記述が長たらしくなりがちだし、名前空間を汚染しやすいのもデメリットだ。簡素なプリミティブ型ばかりのパラメータを受け取る場合などは、指示付き初期化のほうがずっと気軽に使えるだろう。
閑話
本機能はGnuplotラッパーの部分改修およびADAPTへの統合のために作り直したものだ。なのであちらのライブラリで必要になった機能しか実装していないし、AnyTypeKeyword
などはかなりいい加減な作りになっている。しかし自分で使う分にはまあ困ることはないだろう。
まあ所詮は、ネット上にゴロゴロ転がっているC++用名前付き引数の亜種に過ぎない。指示付き初期化で満足できない奇特な人の参考になってくれたら幸いである。私は指示付き初期化でもネット上で紹介されている数多の名前付き引数実装でも満足できなかったので自作するしかなかった。C++も悩ましい言語だなぁといつも思う。