If you can afford to decorate test() overloads in an equivalent way (it's ugly, I know, maybe you can come out with something more beautiful):
struct test_class { param<int> test( int a, param_id<0> ={} ); param<float> test( float a, param_id<1> ={} ); };
then something like this should work ( kind of matching godbolt ):
template<typename T> struct param{ using type = T; }; template<int I> struct param_id{}; template<typename... T> struct type_list{}; struct anything{ template<typename T> operator T&&(); }; template<int I> struct matcher { template<typename T, typename E = std::enable_if_t<std::is_same<T,param_id<I>>::value> > operator T(); }; template<typename T,int I,typename = std::void_t<>,typename... Ts> struct test_types_impl{ using type = type_list<Ts...>; }; template<typename T,int I,typename... Ts> struct test_types_impl<T,I,std::void_t<decltype(std::declval<T>().test( anything{}, matcher<I>{} ))>,Ts...>: test_types_impl<T,I+1,void,Ts...,typename decltype(std::declval<T>().test( anything{}, matcher<I>{} ))::type> { }; template<typename T> struct test_types{ using type = typename test_types_impl<T,0>::type; }; struct test_class { param<int> test( int a, param_id<0> ={} ); param<float> test( float a, param_id<1> ={} ); }; static_assert( std::is_same_v<test_types<test_class>::type, type_list<int,float>> );
this requires at least constructive argument types and C ++ 17 (but I think that it can be made to work in C ++ 11, and with any type).
param_id may be omitted if you manage to get full ordering by the set of allowed parameter types. Maybe we can even omit param<T> somehow, although we are not sure (expecting feedback from the OP for this :))