It works poorly. It relies on the target type having a constructor that takes an std::istream
.
As many types don't have this, and you cannot add it to something like int
, this is a bad plan.
Instead do this:
template<class T>
auto read_from_stream( std::istream& stream, T* unused_type_tag )
-> typename std::decay<decltype( T{stream} )>::type
{
return {stream};
}
template<class T>
auto read_from_stream( std::istream& stream, T* unused_type_tag, ... )
-> typename std::decay<decltype( T(stream) )>::type
{
return T(stream);
}
template<typename... args>
std::tuple<args...> parse(std::istream& stream) {
return std::tuple<args...>{
read_from_stream(stream, (args*)nullptr)...
};
}
now instead of directly constructing the arguments, we call read_from_stream
.
read_from_stream
has two overloads above. The first tries to directly and implicitly construct our object from an istream
. The second explicitly constructs our object from an istream
, then uses RVO to return it. The ...
ensures that the 2nd one is only used if the 1st one fails.
In any case, this opens up a point of customization. In the namespace of a type X
we can write a read_from_stream( std::istream&, X* )
function, and it will automatically be called instead of the default implementation above. We can also write read_from_stream( std::istream&, int* )
(etc) which can know how to parse integers from an istream
.
This kind of point of customization can also be done using a traits class, but doing it with overloads has a number of advantages: you can inject the customizations adjacent to the type, instead of having to open a completely different namespace. The custom action is also shorter (no class wrapping noise).