complexの引数に暗黙の型変換を走らせるためにTMPした話
競プロやってたらなんか引っかかったので、少しはC++知ってたほうがいいかなとちょっと掘り下げてみた話です。
早速ですが、以下の二つのコードがあり、片方は普通に動くのですが、片方はコンパイルエラーを起こします。
#include <iostream> #include <complex> int main() { std::complex<double> x(1.0, 2.0); x = x*2; std::cout << x.real() << " " << x.imag() << std::endl; }
#include <iostream> #include <complex> int main() { std::complex<double> x(1.0, 2.0); x *= 2; std::cout << x.real() << " " << x.imag() << std::endl; }
普通に動いてくれるのは、下のコードです。上のコードのエラーを見てみると、
prog.cpp: In function ‘int main()’: prog.cpp:6:7: error: no match for ‘operator*’ (operand types are ‘std::complex<double>’ and ‘int’) x = x*2; ~^~ In file included from prog.cpp:2:0: /usr/include/c++/6/complex:404:5: note: candidate: template<class _Tp> std::complex<_Tp> std::operator*(const _Tp&, const std::complex<_Tp>&) operator*(const _Tp& __x, const complex<_Tp>& __y) ^~~~~~~~ /usr/include/c++/6/complex:404:5: note: template argument deduction/substitution failed: prog.cpp:6:8: note: mismatched types ‘const std::complex<_Tp>’ and ‘int’ x = x*2; ^ In file included from prog.cpp:2:0: /usr/include/c++/6/complex:395:5: note: candidate: template<class _Tp> std::complex<_Tp> std::operator*(const std::complex<_Tp>&, const _Tp&) operator*(const complex<_Tp>& __x, const _Tp& __y) ^~~~~~~~ /usr/include/c++/6/complex:395:5: note: template argument deduction/substitution failed: prog.cpp:6:8: note: deduced conflicting types for parameter ‘const _Tp’ (‘double’ and ‘int’) x = x*2; ^ In file included from prog.cpp:2:0: /usr/include/c++/6/complex:386:5: note: candidate: template<class _Tp> std::complex<_Tp> std::operator*(const std::complex<_Tp>&, const std::complex<_Tp>&) operator*(const complex<_Tp>& __x, const complex<_Tp>& __y) ^~~~~~~~ /usr/include/c++/6/complex:386:5: note: template argument deduction/substitution failed: prog.cpp:6:8: note: mismatched types ‘const std::complex<_Tp>’ and ‘int’ x = x*2; ^
とあり、まぁ型があってないねってことっぽいです。complex型について'*'の定義をオーバーロードしている文が3つ見えますが、どれにも該当してないぞと言われています。しかし、intはdoubleに暗黙に型変換してくれるんじゃないのかという気持ちになるんですが*1、「これらの演算子は関数テンプレートであるため、」「暗黙の型変換は行われない」そうです*2。つまり、minとかmaxでintとdouble入れるとエラー吐かれるのと同じやつのようです。
では'*='はなぜ大丈夫なのかという話になりますが、https://cpprefjp.github.io/reference/complex/complex/op_multiply_assign.htmlを参照するとここでの'*='の定義が
complex<T>& operator*=(const T& rhs); // (1) template <class X> complex<T>& operator*=(const complex<X>& rhs); // (2)
となっており、(1)の定義では引数の型がテンプレートではないため(つまり今回では普通にdoubleを受け取る関数)、引数としてintを渡しても暗黙の型変換が普段のように行われます。実際にgdbで走らせながら見てみると、
0x0040149e <+62>: mov %eax,%ecx 0x004014a0 <+64>: call 0x403cf4 <std::complex<double>::operator*=(double)> 0x004014a5 <+69>: sub $0x8,%esp 0x004014a8 <+72>: lea -0x18(%ebp),%eax
となっており、そうなっていることが確認できました。
なぜこの差が生まれたかというと、今はcomplex<T>型とT型の掛け算を許しているわけですが*3、'*'の場合は2引数のうち必ず片方はcomplex<T>型であるため、関数テンプレートで定義をする必要がある一方、'*='の場合は引数がT型のみである場合があるため、関数テンプレートを使う必要がなく、暗黙の型変換が行えるような状況になっています。
ここから先は、関数テンプレートでも、テンプレートパラメータを優先して型を決めればその先は暗黙の型変換が行えるような気がするという発想のもと、試行錯誤するのを学科同期のプロ*4に協力を仰ぎながらしてみたものです。とりあえず、std::complexでの'*'operatorのソースコードを見てみます。
https://code.woboq.org/gcc/libstdc++-v3/include/std/complex.html#_ZStmlRKSt7complexIT_ES3_
template<typename _Tp> inline complex<_Tp> operator*(const complex<_Tp>& __x, const complex<_Tp>& __y) { complex<_Tp> __r = __x; __r *= __y; return __r; } template<typename _Tp> inline complex<_Tp> operator*(const complex<_Tp>& __x, const _Tp& __y) { complex<_Tp> __r = __x; __r *= __y; return __r; } template<typename _Tp> inline complex<_Tp> operator*(const _Tp& __x, const complex<_Tp>& __y) { complex<_Tp> __r = __y; __r *= __x; return __r; }
このコードを少しいじってみて、
template<class _Tp, class _Up> inline complex<_Tp> operator*(const complex<_Tp>& __x, const _Up& __y) { complex<_Tp> __t(__x); __t *= __y; return __t; }
こんな感じにすれば'*='に処理を流すことができるので暗黙の型変換が_Up型についてうまく動きそうです。しかし、これはよくなくて、上記のコードを第一引数と第二引数を逆転させたものも追加したとき以下のようなコードでまずいです。
#include <iostream> #include <complex> template<class _Tp, class _Up> inline std::complex<_Tp> operator^(const std::complex<_Tp>& __x, const _Up& __y) { std::complex<_Tp> __t(__x); __t *= __y; return __t; } template<class _Tp, class _Up> inline std::complex<_Up> operator^(const _Tp& __x, const std::complex<_Up>& __y) { std::complex<_Up> __t(__y); __t *= __x; return __t; } int main() { std::complex<double> x(1.0, 2.0); x = x^x; std::cout << x << std::endl; }
今はわかりやすくするために'*'の代わりに'^'を使っていますが、これはエラーが吐かれます。
g.cpp: In function 'int main()': g.cpp:27:10: error: ambiguous overload for 'operator^' (operand types are 'std::complex<double>' and 'std::complex<double>') x = x^x; ~^~ g.cpp:7:1: note: candidate: std::complex<_Tp> operator^(const std::complex<_Tp>&, const _Up&) [with _Tp = double; _Up = std::complex<double>] operator^(const std::complex<_Tp>& __x, const _Up& __y) ^~~~~~~~ g.cpp:17:1: note: candidate: std::complex<_Up> operator^(const _Tp&, const std::complex<_Up>&) [with _Tp = std::complex<double>; _Up = double] operator^(const _Tp& __x, const std::complex<_Up>& __y) ^~~~~~~~
つまり、複数の関数の引数の型にマッチするためどの関数を呼べばよいのか判断がつかなくなりコンパイルが通りません。ではこれをどうやって解決するかですが、引数の両方がcomplex型の場合と片方だけがcomplex型の場合に分けて適切な関数を呼べればこの問題が解決することがわかります。型で場合分けと聞いてまぁ何が登場するかというとテンプレートメタプログラミングですね。というわけで、C++にお強い例のとんがりくんがほぼ一人で書いてくれました。一応、'*'の代わりに自分で実装した'*'として'^'を使っています。
#include<iostream> #include <complex> #include <type_traits> using namespace std; template<class T, class U> true_type both_complex(complex<T> t, complex<U> u); false_type both_complex(...); template<class T, class U> class complex_check : public decltype(both_complex(declval<T>(), declval<U>())) {}; template<class _Tp, class _Up> inline typename enable_if<!(complex_check<complex<_Tp>, _Up>::value), complex<_Tp> >::type operator^(const complex<_Tp>& __x, const _Up& __y) { complex<_Tp> __t(__x); __t *= __y; return __t; } template<class _Tp, class _Up> inline typename enable_if<!(complex_check<_Tp, complex<_Up>>::value), complex<_Up> >::type operator^(const _Tp& __x, const complex<_Up>& __y) { complex<_Tp> __t(__y); __t *= __x; return __t; } template<class _Tp, class _Up> inline complex<_Tp> operator^(const complex<_Tp>& __x, const complex<_Up>& __y) { complex<_Tp> __t(__x); __t *= __y; return __t; } int main() { complex<int> a(1, 2); complex<double> b(0.99, 2.05); b = b^2; a = a^3.2; cout << a << " " << b << endl; //a = a^b; -> error a = a^a; b = b^b; cout << a << " " << b << endl; return 0; }
declvalの便利さなどが実感できるコードになってます。僕たちが確認した範囲では、ちゃんと動いているように見えました。つまり、両方ともcomplexで型が一致している場合は普通に動き、片方がcomplex
実際これはすごく便利だと思うのでstdでもこう実装してくれていると嬉しいと思うのですが、こうなっていないからには既に何かしらの形で存在しているのか、この方法に問題があるのかだと思うので、有識者の方は教えていただけると嬉しいです。何かわかったら追記するかもです。
参考文献
operator* - cpprefjp C++日本語リファレンス
complex::operator*= - cpprefjp C++日本語リファレンス