C++17 でキーワード引数を実装してみた

# はじめに
C++17 でキーワード引数を実装してみたときの記録です。
GitHub リポジトリは以下にあります。
https://github.com/iorate/cpp-flexargs

# 使い方
```greet.cpp
#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()` 内でコンパイルエラーを発生させないように努め、代わりにエラーを表す型を返すようにしています。雰囲気としては次のようなエラー処理をしています。

```at.cpp
#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` を組み合わせて、正常時は通常の戻り値型が、エラー時はエラーを表す型が返るようにしています。 エラー時は、ユーザーが戻り値を使おうとした場所で、コンパイルエラーが発生します。

実際のエラーの例としては次のようになります。

```error.cpp
#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 はいいぞ。