简体   繁体   中英

Tuple and variadic templates, how does this work?

I have seen people write (on stack overflow itself, asking some even advanced concepts) something along the lines of:

template<typename... args>
std::tuple<args...> parse(istream stream) 
{
    return std::make_tuple(args(stream)...);
}

and use it as

auto tup = parse<int, float, char>(stream);

How does the above code construct the tuple by parsing the stream? Is there any specific requirement on how data is to be put into the stream?

For this to work there must be an implicit conversion from std::istream to all the types specified as template arguments.

struct A {
    A(std::istream&) {} // A can be constructed from 'std::istream'.
};

struct B {
    B(std::istream&) {} // B can be constructed from 'std::istream'.
};

int main() {
    std::istringstream stream{"t1 t2"};
    auto tup = parse<A, B>(stream);
}

It works by expanding the variadic list of types and constructs each type with the supplied std::istream as argument. It is then left to the constructor of each type to read from the stream.

Also be aware that the evaluation order of the constructors is not specified so you can't expect that the first type in the variadic list will read from the stream first etc.

The code as it is does not work with built in types as int , float and char as there is no conversion from std::istream to any of those types.

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).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM