Writing a generic input function (1)

Having the <print> header available to us in C++23, we no longer have to rely on using the stream insertion operator with std::cout and other stream output objects. This article attempts to find a way to avoid the need for using the stream extraction operator with std::cin, while still using this stream input object (or other std::istreams).

At the simplest level, we may want to obtain a single typed object (not limited to the built-in types) from an input stream, by default std::cin. (To read a std::string we probably want std::getline(), see later.) A simple generic function which performs this is shown below:

template<typename T>
T get(std::istream& is = std::cin) {
    T input;
    is >> input;
    return input;
}

Since the template type parameter is the return type, and not one of the parameters, the function must be called with this type in angle brackets, as in:

auto n = get<double>();

If an input stream other that std::cin is desired, this must be specified within the parentheses.

In order to obtain more than one entity from the input stream, another possibility is to use a variadic template with a template parameter pack. In a previous article we have discussed C++ fold expressions, and it is such an entity which performs the actual work. The generic function which performs this can be written as follows:

template<typename... Ts>
std::istream& get(std::istream& is, Ts&... inputs) {
    ((is >> inputs), ...);
    return is;
}

This needs to have the input parameters already defined in order to pass them as l-value references, and is called as follows:

double a, b;
char op;
get(std::cin, a, op, b);

Notice that specifying the type(s) explicitly is no longer needed, and that the semantics and action of this function is exactly the same as: std::cin >> a >> op >> b;

So far so good, but we still encounter the bugbear of default C++ stream input, whereby a mis-entered input causes all further stream input to fail. A more robust strategy is to enter-and-process one complete input line at a time, most likely utilizing std::getline() to obtain it. Here is an outline of our previous generic function modified to use line buffering:

template<typename... Ts>
auto getline(std::istream& is, Ts&... inputs) {
    using std::getline;
    std::string inputline;
    getline(is, inputline);
    std::istringstream iss{ inputline };
    ((iss >> inputs), ...);
    return iss.rdstate();
}

This is called in exactly the same way as the multiple-parameter get() function from before:

double a, b;
char op;
auto st = getline(std::cin, a, op, b);

Notice that this function template first reads a complete line of input into a std::string, and then creates an input stringstream (std::istringstream) from this to be read from. The fold expression references this stringstream, the stream state for which are then returned as the return type. This value can then be examined by the client code for the “fail” bit or “end-of-file” bit being set, indicating input error or extra (trailing) input present.

So should we still be overloading operator >> for user-defined types in Modern C++? Yes, absolutely! They’ll still play well with established stream-object-using code, and can be easily adapted to code with different input strategies and requirements. That’s about it for this article, in the next we’ll try to improve performance over use of >> and stream objects.

Resources

Leave a comment