C++ で名前付き引数を実現する

  • 39
    Like
  • 0
    Comment
More than 1 year has passed since last update.

NP1.gif

引数の意味と順序がわからない

Date(7, 8);

は 7 月 8 日でしょうか、8 月 7 日でしょうか。

ToString(255, 16, true);

255 を文字列に変換するようですが、16true は何を意味するのでしょうか。

名前付き引数(またはキーワード引数)をサポートする Python や Ruby, C#, Swift などの言語では、このような引数の意味を明快に表現できます。

Date(month: 7, day: 8);

Date(day: 8, month: 7);

ToString(255, radix: 16, upperCase: true);

C++ には名前付き引数に相当する言語機能はありません。
それでも、既存の文法を用いて名前付き引数の代わりを実装できます。

ちなみに、将来の C++ に言語機能として名前付き引数を導入することは提案されています1

NamedParameter を使う

本記事で紹介する C++ 名前付き引数ライブラリ NamedParameter

  • ヘッダオンリーのシンプルな実装
  • 簡潔な宣言と自然な文法
  • 既存のコードを破壊しない
  • デフォルト引数のサポート
  • 参照引数のサポート
  • 名前空間のサポート
  • constexpr のサポート
  • 型の不一致はコンパイルエラーにする

といった特長を持ち、次のようなコードを実現します。

int main()
{
    Date(Arg::month = 12, Arg::day = 31);

    Date(Arg::day = 31, Arg::month = 12);

    ToString(255, Arg::radix = 16, Arg::upperCase = true);
}

使い方

1. ヘッダをインクルード

"NamedParameter.hpp" をインクルードします。

# include <iostream>
# include "NamedParameter.hpp"

void Date(int month, int day)
{
    std::cout << "month: " << month << " day: " << day << '\n';
}

int main()
{

}

2. 名前付き引数に使う名前を用意

名前付き引数に使う名前を NP_MAKE_NAMED_PARAMETER(); マクロで用意します。
名前空間に入れれば、既存の変数名との衝突を防げます。
monthday という名前付き引数を Arg という名前空間に定義しましょう。

# include <iostream>
# include "NamedParameter.hpp"

namespace Arg
{
    NP_MAKE_NAMED_PARAMETER(month);
    NP_MAKE_NAMED_PARAMETER(day);
}

void Date(int month, int day)
{
    std::cout << "month: " << month << " day: " << day << '\n';
}

int main()
{

}

3. 名前付き引数で呼び出す関数を用意

名前付き引数を使って呼び出す関数を定義します。名前_<型> が引数の型になります。
引数の実際の値にアクセスするには .value() を使います。
Arg::month_<int>Arg::day_<int> は型が異なるため、順番を入れ替えたオーバーロードを作れます。

# include <iostream>
# include "NamedParameter.hpp"

namespace Arg
{
    NP_MAKE_NAMED_PARAMETER(month);
    NP_MAKE_NAMED_PARAMETER(day);
}

void Date(int month, int day)
{
    std::cout << "month: " << month << " day: " << day << '\n';
}

void Date(Arg::month_<int> month, Arg::day_<int> day)
{
    Date(month.value(), day.value());
}

void Date(Arg::day_<int> day, Arg::month_<int> month)
{
    Date(month.value(), day.value());
}

int main()
{

}

4.名前付き引数の関数を呼び出す

名前付き引数の関数を呼び出すには、名前 = 値 というスタイルで実引数を記述します。

# include <iostream>
# include "NamedParameter.hpp"

namespace Arg
{
    NP_MAKE_NAMED_PARAMETER(month);
    NP_MAKE_NAMED_PARAMETER(day);
}

void Date(int month, int day)
{
    std::cout << "month: " << month << " day: " << day << '\n';
}

void Date(Arg::month_<int> month, Arg::day_<int> day)
{
    Date(month.value(), day.value());
}

void Date(Arg::day_<int> day, Arg::month_<int> month)
{
    Date(month.value(), day.value());
}

int main()
{
    Date(Arg::month = 12, Arg::day = 31);

    Date(Arg::day = 31, Arg::month = 12);

    Date(12, 31); // 従来通りの呼び出し
}

その他

メンバへのアクセス

引数の実際の値には .value() のほかに operator* でもアクセス可能です。
値がメンバを持つ場合、operator-> でメンバにアクセスできます。
これは std::optional と同じインタフェースです。

void Print(Arg::name_<std::string> name)
{
    std::cout << "name:" << *name << '\n';

    std::cout << "length:" << name->length() << '\n';
}

デフォルト引数

記述は少し長くなりますが、通常の C++ と同様に右にあるパラメータからデフォルト引数を与えられます。

std::string ToString(int n, Arg::radix_<int> radix = (Arg::radix = 10), Arg::upperCase_<bool> = (Arg::upperCase = true));

int main()
{
    ToString(255, Arg::radix = 16);
}

オブジェクトの構築

= の代わりに () でコンストラクタの引数を渡すと、それらの値からオブジェクトを構築します。

void Print(Arg::name_<std::string> name);

int main()
{
    Print(Arg::name(5, 'A')); // "AAAAA"
}

参照型

参照型の引数をサポートします。
呼び出し時には std::reference_wrapper を渡します。

void Increment(Arg::n_<int&> n)
{
    ++n.value();
}

size_t Length(Arg::text_<const std::string&> text)
{
    return text.length();
}

int main()
{
    int n = 99;

    Increment(Arg::n = std::ref(n));

    std::cout << n << '\n';


    std::string str = "Hello!";

    std::cout << Length(Arg::text = std::cref(str)) << '\n';
}

名前付き引数の使用の強制

名前付き引数の使用を強制したい場合には、名前付き引数を使用しない関数をオーバーロード候補から外します。

void Date_impl(int month, int day)
{
    std::cout << "month: " << month << " day: " << day << '\n';
}

void Date(Arg::month_<int> month, Arg::day_<int> day)
{
    Date_impl(month.value(), day.value());
}

void Date(Arg::day_<int> day, Arg::month_<int> month)
{
    Date_impl(month.value(), day.value());
}

int main()
{
    Date(Arg::month = 12, Arg::day = 31);

    Date(Arg::day = 31, Arg::month = 12);

    Date(12, 31); // コンパイルエラー
}

Parameter pack

複数の名前付き引数を様々な順番で使用するには、多くのオーバーロードを記述しなければなりません。
Parameter pack を使えば、それを省略できます。

namespace Arg
{
    NP_MAKE_NAMED_PARAMETER(x);
    NP_MAKE_NAMED_PARAMETER(y);
    NP_MAKE_NAMED_PARAMETER(z);
}

template <class... Args>
void F(Args&&... args)
{
    const auto t = std::make_tuple(std::forward<Args>(args)...);

    std::cout << "x: " << std::get<Arg::x_<int>>(t).value() << '\n';

    std::cout << "y: " << std::get<Arg::y_<int>>(t).value() << '\n';

    std::cout << "z: " << std::get<Arg::z_<int>>(t).value() << '\n';
}

int main()
{
    F(Arg::x = 10, Arg::y = 20, Arg::z = 30);

    F(Arg::y = 20, Arg::z = 30, Arg::x = 10);

    F(Arg::z = 30, Arg::x = 10, Arg::y = 20);
}

実行時性能

NamedParameter による名前付き引数は、コンパイラの最適化によって、ほとんどの場合で通常の関数呼び出しと同じ実行コードになります。
gcc 6.2 及び clang 3.9 で -O2 オプションを有効にして次の 2 つのコードをコンパイルしましたが、実行コードは変化しませんでした。

int Add(int a, int b)
{
    return a + b;
}

void Increment(int& a)
{
    ++a;
}

int main()
{
    int a = rand(), b = rand();

    Increment(a);

    return Add(a, b);
}
namespace Arg
{
    NP_MAKE_NAMED_PARAMETER(a);
    NP_MAKE_NAMED_PARAMETER(b);
}

int Add(Arg::a_<int> a, Arg::b_<int> b)
{
    return a.value() + b.value();
}

void Increment(Arg::a_<int&> a)
{
    ++a.value();
}

int main()
{
    int a = rand(), b = rand();

    Increment(Arg::a = std::ref(a));

    return Add(Arg::a = a, Arg::b = b);
}

constexpr サポート

NamedParameterconstexpr に対応しています。
名前付き引数を使用する前にコンパイル時定数式であったコードは、名前付き引数を使用したあともコンパイル時定数式になります。

constexpr int Add(Arg::a_<int> a, Arg::b_<int> b)
{
    return a.value() + b.value();
}

int main()
{
    constexpr int n = Add(Arg::a = 2, Arg::b = 8); // constexpr int n = 10;
}

実装の解説

NamedParameter.hpp は 200 行に満たないライブラリです。
ヘッダのコードを追って原理を理解しても良いでしょう。
ここでは、day という名前付き引数がどのような仕組みで機能するのか、順を追って簡単に説明します。

1.

NP_MAKE_NAMED_PARAMETER(day); マクロにより、次のコードが生成されます。

constexpr auto day = np::NamedParameterHelper<struct day_tag>{};

template <class Type> using day_ = np::NamedParameterHelper<struct day_tag>::named_argument_type<Type>;

2.

NamedParameterHelper<day_tag> 型のオブジェクト day には operator=operator() が定義されています。
day = 31 のように渡された値を NamedParameter<day_tag, int>(31) とラップして返します。
day はコンパイル時定数であり、見かけと違って day の状態は一切変化しないことに注意してください。

auto result = (day = 31); // result は NamedParameter<day_tag, int> 型

3.

名前付き引数を使う関数では、引数の型として day_<int> を記述していることでしょう。
これは NamedParameter<day_tag, int> です。
したがって、2. で生成された NamedParameter を受け取ることができるのです。

void Day(day_<int> d) // d は NamedParameter<day_tag, int> 型
{
    std::cout << "day: " << d.value() << '\n';
}

int main()
{
    auto result = (day = 31); // result は NamedParameter<day_tag, int> 型

    Day(result);
}

ためしてみる

GitHub NamedParameter
オンライン C++ コンパイラで NamedParameter サンプルを実行

参考資料