C++で引数名指定もどき

C++では関数を呼び出す際の引数指定で、引数名指定ができません。例えばpythonなどは、

def func(a=1,b=2,c=3):
	print(f'{a},{b},{c}')

func(3,2,1) # 3,2,1
func() # 1,2,3
func(b=1) # 1,1,3
func(c=1,a=3) # 3,2,1

のように、いろいろな指定が出来て、3番目の例のように引数名で指定することができます。

今回はC++でなんとか近い実装を無理矢理できないか、調べてみました。なんとかそれっぽいのが部分的に出来てます(?)が、実際使うか?と言われると難しい感じの代物です。アイデアだけ役に立つこともあるかも程度の話です。

目的

#include <iostream>

using namespace std;

void func(int a=1, int b=2, int c=3) {
    cout << a << "," << b << "," << c << endl;
}

int main() {
    func(3,2,1); // 3,2,1
    func(); // 1,2,3
    // func(b=1); // error
    // func(c=1, a=3); // error
    return 0;
}

上記コードでコメントされているfunc呼び出しをエラーにせず機能を実現すること。

参考にしたページ

実際このページのパクリです。

C++で引数名指定もどき

引数名を型として定義する

#include <iostream>

using namespace std;

template<typename Type, typename Identifier>
struct Argument {
    Type value;
    Argument(const Type& v):value(v){}
    Argument(Type&& v):value(std::move(v)){}
};

using A=Argument<int, struct IdA>;
using B=Argument<int, struct IdB>;
using C=Argument<int, struct IdC>;

void func(A a=A(1), B b=B(2), C c=C(3)) {
    cout << a.value << "," << b.value << "," << c.value << endl;
}

int main() {
    func(3,2,1); // 3,2,1
    func(A(3),B(2),C(1)); // 3,2,1
    func(); // 1,2,3
    // func(b=1); // error
    // func(c=1, a=3); // error
    return 0;
}

Argumentテンプレート構造体を定義することで、A,B,Cという引数をラップする構造体を定義し、引数名を型で区別できるようにしています。main関数内2番目のfunc関数呼び出しは、順番に並ぶだけでなく、引数の名前に基づく型名が記述されることにより、実際に何の引数なのかが読む人にとって分かりやすくなっています。もちろん順番を変えることはできません。

Argumentテンプレート構造体のテンプレート引数Identifierは、同じ型の引数を名前で区別できるようにするための便宜上の型です。実際にこの型を使うことはありません。

見れば分かるとおり、A,B,C,IdA,IdB,IdCなど、引数の倍の数だけ名前空間を汚す仕組みになっています

呼び出し側に型を意識させず、少しだけ見やすくする

次はA,B,Cという型を意識させない方法を追加します。

#include <iostream>

using namespace std;

template<typename Type, typename Identifier>
struct Argument {
    Type value;
    Argument(const Type& v):value(v){}
    Argument(Type&& v):value(std::move(v)){}
    struct Name {
         Argument operator=(Type&& v) const {
            return Argument(std::forward<Type>(v));
         }
    };
};

using A=Argument<int, struct IdA>;
static const A::Name a;
using B=Argument<int, struct IdB>;
static const B::Name b;
using C=Argument<int, struct IdC>;
static const C::Name c;

void func(A a=A(1), B b=B(2), C c=C(3)) {
    cout << a.value << "," << b.value << "," << c.value << endl;
}

int main() {
    func(3,2,1); // 3,2,1
    func(A(3),B(2),C(1)); // 3,2,1
    func(a=3,b=2,c=1); // 3,2,1
    func(); // 1,2,3
    // func(b=1); // error
    // func(c=1, a=3); // error
    return 0;
}

引数の3倍の数だけ名前空間を汚すことで、見た目だけ引数名を等号を付けて指定できるようになっています(main関数内3番目のfunc関数呼び出し)。実際に渡っている引数はA,B,Cの型の構造体なのですが、見た目ではintがそのまま渡っているように見えます。a,b,cは実際に実体を持つ変数になっており、シンボルまで持ちます(構造体のメンバはありませんが)。代入演算子をconstでオーバーロードして、A,B,Cを返すようなトリックになっています。

tupleを使って任意の順に

今度は引数名指定を必須とし、値を順番に並べることができなくなる代わりに、引数の順番を入れ替えられるようにしてみます。

#include <iostream>
#include <tuple>

using namespace std;

template<typename Type, typename Identifier>
struct Argument {
    Type value;
    Argument(const Type& v):value(v){}
    Argument(Type&& v):value(std::move(v)){}
    struct Name {
         Argument operator=(Type&& v) const {
            return Argument(std::forward<Type>(v));
         }
    };
};

using A=Argument<int, struct IdA>;
static const A::Name a;
using B=Argument<int, struct IdB>;
static const B::Name b;
using C=Argument<int, struct IdC>;
static const C::Name c;

template<typename T, typename... Args>
T pick(Args&&... args) {
    return get<T>(make_tuple(forward<Args>(args)...));
}

void func(A a=A(1), B b=B(2), C c=C(3)) {
    cout << a.value << "," << b.value << "," << c.value << endl;
}

template<typename... Args>
void func_anyorder(Args&&... args) {
    func(pick<A>(args...), pick<B>(args...), pick<C>(args...));
}

int main() {
    func(3,2,1); // 3,2,1
    func(A(3),B(2),C(1)); // 3,2,1
    func(a=3,b=2,c=1); // 3,2,1
    func(); // 1,2,3
    func(a=3,b=2); // 3,2,3
    // func(c=1, a=3, b=2); // error
    // func(b=1); // error
    // func(c=1, a=3); // error
    
    // func_anyorder(3,2,1); // error
    func_anyorder(A(3),B(2),C(1)); // 3,2,1
    func_anyorder(a=3,b=2,c=1); // 3,2,1
    // func_anyorder(); // error
    // func_anyorder(a=3,b=2); // error
    func_anyorder(c=1, a=3, b=2); // 3,2,1
    return 0;
}

std::make_tupleを使用してtupleを作り、そこから型指定で値を取得するstd::getを使用することで、任意の順番に並ぶ引数から所望の引数を選択する方法です。引数ごとに型を作成する無駄な労力が少し報われています。

ただ、このままでは引数の省略ができず、入れ替えられるようになるメリットがほとんどありません。もちろんC++なら本来できる末尾省略もできません。

任意の引数の省略

最後に引数を省略できるようにしてみました。

#include <iostream>
#include <tuple>

using namespace std;

template<typename T, typename Identifier>
struct Argument {
    typedef T Type;
    Type value;
    Argument(const T& v):value(v){}
    Argument(T&& v):value(std::move(v)){}
    struct Name {
         Argument operator=(T&& v) const {
            return Argument(std::forward<T>(v));
         }
    };
    static const Type default_value;
};

using A=Argument<int, struct IdA>;
static const A::Name a;
template<> const A::Type A::default_value = 1;
using B=Argument<int, struct IdB>;
static const B::Name b;
template<> const B::Type B::default_value = 2;
using C=Argument<int, struct IdC>;
static const C::Name c;
template<> const C::Type C::default_value = 3;

template<typename T, typename Tuple>
struct NotContain {
    static auto get(const Tuple& t) {return T::default_value;}
};

template<typename T, typename Tuple>
struct Contain {
    static auto get(const Tuple& t) {return ::std::get<T>(t);}
};

template <typename T, typename Tuple, typename SubTuple> struct JudgeContain;
template <typename T, typename Tuple> struct JudgeContain<T, Tuple, tuple<>> : NotContain<T, Tuple> {};
template <typename T, typename Tuple, typename U, typename... Tail>
struct JudgeContain<T, Tuple, tuple<U, Tail...>> : JudgeContain<T, Tuple, tuple<Tail...>> {};
template <typename T, typename Tuple, typename... Tail>
struct JudgeContain<T, Tuple, tuple<T, Tail...>> : Contain<T, Tuple> {};

template<typename T, typename... Args>
T pick(Args&&... args) {
    auto t = make_tuple(forward<Args>(args)...);
    return JudgeContain<T,decltype(t),decltype(t)>::get(t);
}

void func(A a=A::default_value, B b=B::default_value, C c=C::default_value) {
    cout << a.value << "," << b.value << "," << c.value << endl;
}

template<typename... Args>
void func_anyorder(Args&&... args) {
    func(pick<A>(args...), pick<B>(args...), pick<C>(args...));
}

int main() {
    func(3,2,1); // 3,2,1
    func(A(3),B(2),C(1)); // 3,2,1
    func(a=3,b=2,c=1); // 3,2,1
    func(); // 1,2,3
    func(a=3,b=2); // 3,2,3
    // func(c=1, a=3, b=2); // error
    // func(b=1); // error
    // func(c=1, a=3); // error
    
    // func_anyorder(3,2,1); // not valid
    func_anyorder(A(3),B(2),C(1)); // 3,2,1
    func_anyorder(a=3,b=2,c=1); // 3,2,1
    func_anyorder(); // 1,2,3
    func_anyorder(a=3,b=2); // 3,2,3
    func_anyorder(c=1, a=3, b=2); // 3,2,1
    func_anyorder(b=1); // 1,1,3
    func_anyorder(c=1, a=3); // 3,2,1
    return 0;
}

std::getは指定型がtupleに含まれていないとコンパイルエラーなので、それを回避するために自前の判定機構を型として用意しています。tupleを先頭から順番に判定していき、所定の型が含まれていればContain派生型になり、含まれていなければNotContain派生型になる仕組みです。これに伴い、指定型が含まれていない場合、指定型のstaticメンバからデフォルト値を得る形に変更しています。

ただし、この判定方法では、余計な引数を指定しても無視されるだけでエラーにならないため、実用は難しいかも知れません。

まとめ

C++でpythonライクな引数指定は(自分には)難しく、似せようとする労力に結果が見合わない。

未分類C++

Posted by first_user