はじめに
関数の引数が増えると、デフォルト値が指定してあっても、最後の引数だけ指定できない。そのため、結局デフォルト値を調べながら、指定する必要あります。
kotlinなど名前付き引数がある言語なら、個別にパラメータを指定できてうらやましい。まあ、boostにも名前付き引数があるけど、引数名をマクロを使って指定する必要があって手間がかかる。
ということで、なんとかできないか考えみたら、C++11/14で追加された機能を使っていい感じに実装できたので公開します。
*kazatsuyuさんに、C++2aでは"designated initialization"という機能が規格化される予定であることを教えてもらいました。この機能を使うと、より少ない記述量で擬似的な名前付き引数を実現できます。"designated initialization"は、最新のClangやGCCには実装済みですので、それらが使えない場合の過渡期の記述方法と考えてほしいです。なお、本記事の記述方法を使えば、"designated initialization"に準拠した記述方法に、比較的簡単に書き換え可能です。
参考URL https://en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers
(追記 2019/2/9)
C++20版を記事にしました。
https://qiita.com/luftfararen/items/1f8c356711f69151e909
(追記 2020/7/3)
事前準備
まずは、以下のヘッダーファイルを準備。
# if !defined(NAMED_ARGS_H_)
# define NAMED_ARGS_H_
# include "functional"
template<class T>
using init_args = std::function<void(T&)>;
# define ARGS [&](auto& _)
# define INIT_ARGS_DEF = [&](){}
# endif
使い方(サンプルコード)
# include <string>
# include <iostream>
# include <iomanip>
# include <sstream>
# include "named_args.h"
//引数のリストは構造体として定義し、デフォルト値を指定
struct to_str_args
{
int base = 10;
char fill = ' ';
int w=0;
int precision = 5;
bool fixed = false;
};
//関数1
template<class T>
std::string to_str(T v, const to_str_args& a = to_str_args())
{
std::stringstream ss;
ss << std::setfill(a.fill);
ss << std::setbase(a.base);
ss << std::setw(a.w);
if (a.fixed) ss << std::fixed;
ss << std::setprecision(a.precision);
ss << v;
return ss.str();
}
//関数2
template<class T>
std::string to_str(T v, init_args<to_str_args> init)
{
to_str_args args;
init(args);
return to_str(v,args);
}
int main()
{
std::cout << to_str(12.345) << std::endl;
std::cout << to_str(12.345, ARGS{_.w = 6; }) << std::endl;
std::cout << to_str(12.345, ARGS{_.w = 7; _.precision = 3; _.fill = '0'; }) << std::endl;
//"designated initialization"への移行を前提とする場合、以下は非推奨→詳細は解説を参照のこと
std::cout << to_str(12.345, ARGS{_.precision = 3; _.fill = '0'; _.w = 7;}) << std::endl;}
結果
12.345
012.345
00012.3
00012.3
解説
引数は構造体として定義しておく。ポイントは、関数2のようなinit_args<構造体> を引数にもつ関数を定義し、コール時にARGS{_.XXX=NNN;}といった形で、引数名と値を{}内に定義し、これを引数として渡す。これで、擬似的に名前付き引数を実現。{}で値の設定を行わなければ構造体にで指定したデフォルト値が使われます。
ちなみに、関数1のような構造体を引数に持つ関数は必須でないですが、パラメータセットを使い回すときには構造体変数を宣言したほうが便利なので、今回は実装した例をサンプルとしました。関数1を定義しない場合は、関数2の第二引数のデフォルト値にINIT_ARGS_DEFにしておくとよいです。INIT_ARGS_DEFは何もしないラムダ式です。
template<class T>
std::string to_str(T v, init_args<to_str_args> init = INIT_ARGS_DEF)
{
to_str_args args;
init(args);
//実装
}
c++2aで規格化予定の"designated initialization"への移行を前提とする場合は、関数1を定義し、関数コール時には、構造体変数の定義順で引数を指定しておいてください。"designated initialization"には、指定順に制約があります。
さいごに
「構造体を定義するなら、名前付きに引数にする必要ない」じゃんというツッコミは、許して下さい。少しは呼び出し側の記述量が減らせるはず。ただ、本質的には、文法レベルでの対応を標準化してほしいところです。あと、標準でプロパティがほしい。