はじめに
C++17 でキーワード引数を実装してみたときの記録です。
GitHub リポジトリは以下にあります。
https://github.com/iorate/cpp-flexargs
使い方
#include <iostream>
#include <utility>
#include "flexargs.hpp"
using namespace flexargs;
// 次のような関数を、キーワード引数で呼び出せるようにします。
// auto greet = [](auto &&name, auto &&out = std::cout) {
// out << "Hello, " << name << "!\n";
// };
// キーワードを定義します。
namespace keywords {
inline constexpr keyword<struct name_> name;
inline constexpr keyword<struct out_> out;
}
// 可変長テンプレートで実引数を受け取ります。
template <class ...Args>
void greet(Args &&...args) {
// match() に仮引数の宣言と実引数を渡し、その戻り値で仮引数を初期化します。
auto [name, out] = match(
parameter(keywords::name), // 仮引数を宣言します。
parameter(keywords::out) = std::cout, // 仮引数は既定値を持つことができます。
std::forward<Args>(args)...
);
// 関数の本体は普通に書きます。
out << "Hello, " << name << "!\n";
}
int main() {
using namespace keywords;
// 引数 name をキーワードで指定します。引数 out は既定値を持つため省略できます。
greet(name = "World");
// 引数の順番を変えることができます。
greet(out = std::cout, name = "World");
// 従来通り、引数を順番で指定することもできます。
greet("Error", std::cerr);
}
動機
久しぶりに C++ を触ったら、C++17 というナウい言語になっていたので、簡単なライブラリを書いてみようと思いました。
設計
関数を呼び出す側が、上記の例のように "自然" な記法 (主観が入りますが) で関数を呼び出せるようにしました。ユーザー定義リテラルによる手法は、キーワードの事前定義が不要になるメリットがありますが、greet("name"_arg = "World")
のような不自然な書き方になるので、今回は使っていません。
また、関数を実装する側が、関数の本体を "普通" に仮引数の名前を使って書けるようにしました。BOOST_PARAMETER_FUNCTION()
のように。ただし今回は、マクロを使わないという縛りをかけています。
最後に、関数を呼び出す側、実装する側、いずれのミスに対しても、できるだけ簡潔なコンパイルエラーが表示されるようにしました。C++ でテンプレートライブラリを使用したときのエラーメッセージはしばしば膨大かつ分かりにくいですが、その理由の一部には、エラーがライブラリの深部で発生すること、その内容からユーザーが何をミスしたか察しにくいことが挙げられます。今回は、できるだけユーザーのコードでコンパイルエラーが発生するようにし、その内容に分かりやすいメッセージを埋め込むようにしました。
実装
match()
関数は、仮引数と実引数を突き合わせて、仮引数の初期値のタプルを返すように実装されています。これは素直に頑張ればできます。
エラーメッセージを簡潔にするため、ユーザーのミスに対して match()
内でコンパイルエラーを発生させないように努め、代わりにエラーを表す型を返すようにしています。雰囲気としては次のようなエラー処理をしています。
#include <cstddef>
#include <type_traits>
struct index_error {};
template <std::size_t N>
inline constexpr std::integral_constant<std::size_t, N> size_c;
// 配列の要素にアクセスする関数
template <class T, std::size_t N, std::size_t I>
constexpr decltype(auto) at(T (&arr)[N], std::integral_constant<std::size_t, I>) {
if constexpr (I >= N) {
return index_error();
} else {
return arr[I];
}
}
int main() {
int arr[] = {1, 2, 3};
// 存在しない 4 番目の要素にアクセスしようとしている
int fourth = at(arr, size_c<3>);
}
$ g++ -std=c++17 at.cpp
at.cpp: In function 'int main()':
at.cpp:22:35: error: cannot convert 'index_error' to 'int' in initialization
int fourth = at(arr, size_c<3>);
^
# 22 行が原因で index error が起きたことが分かる
戻り値型 decltype(auto)
と if constexpr
を組み合わせて、正常時は通常の戻り値型が、エラー時はエラーを表す型が返るようにしています。 エラー時は、ユーザーが戻り値を使おうとした場所で、コンパイルエラーが発生します。
実際のエラーの例としては次のようになります。
#include <iostream>
#include <utility>
#include "flexargs.hpp"
using namespace flexargs;
namespace keywords {
inline constexpr keyword<struct name_> name;
inline constexpr keyword<struct out_> out;
}
template <class ...Args>
void greet(Args &&...args) {
auto [name, out] = match(
parameter(keywords::name),
parameter(keywords::out) = std::cout,
std::forward<Args>(args)...
);
out << "Hello, " << name << "!\n";
}
int main() {
using namespace keywords;
// 引数 name を 2 回指定しようとしている
greet("World", name = "World");
}
$ g++ -std=c++17 error.cpp
error.cpp: In instantiation of 'void greet(Args&& ...) [with Args = {const char (&)[6], flexargs::detail::keyword_argument<keywords::name_, const char (&)[6]>}]':
error.cpp:26:34: required from here
error.cpp:13:10: error: cannot decompose class type 'flexargs::detail::syntax_error<flexargs::detail::duplicate_argument<keywords::name_> >' without non-static data members
auto [name, out] = match(
^~~~~~~~~~~
# 短いエラーメッセージの中に、syntax_error<duplicate_argument<name_>> という型が埋め込まれている
まとめ
C++17 でキーワード引数を実装してみました。
C++17 はいいぞ。