Writing a generic input function (2)

This article continues the theme of writing functions that parse user input, in this case numerical only (both integral and floating-point). In another article the use of from_chars() (in Modern C++’s Standard Library) was discussed and it is this function which we will use. One benefit is that it is suited to computing tasks where better performance (compared to use of stream input functions) is needed.

The template function from_chars() is defined in header <charconv> and takes two const char* pointers (start and one-past-end of input C-string) and a reference to a built-in numeric type where the result is stored. (Integral parsers can also take a further, optional, numerical base parameter, while floating-point parsers have an optional format parameter, neither of which we use in the code for this article.)

The return type of from_chars() is a struct with two fields, a numerical error code (actually an enum class) ec and a const char* pointer ptr. The error code can be tested against a default-constructed std::errc{} object to test for a conversion error having occurred, while the pointer provides the next unread character of input in the provided range. In order to read multiple numbers from a single input string, it is therefore necessary to call from_chars() again with the first parameter being the next non-whitespace character after the pointer returned by the previous (successful) call.

Translating this into C++ code begins with creating an exception class (which will throw if the error code is set in the return value) and a generic wrapper for from_chars() which we have called get_from_chars():

class FromCharsException : public std::exception {
    std::errc ec;
public:
    FromCharsException() = delete;
    FromCharsException(std::errc error_code)
        : ec{ error_code } {}
    const char* what() const noexcept override final {
        return "from_chars() exception";
    }
    auto get() const noexcept { return ec; }
};

template<typename T>
std::pair<T,const char*> get_from_chars(std::string_view s) {
    using std::from_chars;
    T value;
    auto rc = from_chars(s.data(), s.data() + s.size(), value);
    if (rc.ec != std::errc{}) {
        throw FromCharsException(rc.ec);
    }
    return std::pair{ value, rc.ptr };
}

The exception class inherits from std::exception in the usual way providing the necessary constructor and correctly attributed what() function override. Also provided is a public get() member function which allows extraction of the numerical error code by the exception-catching code. The function get_from_chars() attempts to instantiate std::from_chars() with a variable value of generic type T, which will only succeed to compile if it matches one of the specializations in the Standard Library. It then returns the input value and updated pointer to next input as a std::pair.

For a better compile-time experience a requires clause (discussed in a previous article) could be employed to catch incompatible types—alternatively it would even be possible to write your own from_chars() template for any user-defined types you need to input (but it shouldn’t be put in the std namespace).

A generic get() function which obtains a single value from an input stream looks a lot like the previous one we wrote, the difference is that we first obtain a std::string from the std::istream (which stops at any whitespace encountered) and then pass this to our get_from_chars() function, keeping only the first member returned (recall this is the value, the pointer is not used):

template<typename T>
T get(std::istream& is = std::cin) {
    std::string str;
    is >> str;
    return get_from_chars<T>(str).first;
}

This function is invoked in exactly the same way as before, such as: auto i = get<int>()

More interestingly, we want to be able to input several numerical values, separated by whitespace, on the same input line, with maximum performance. We’ll continue to use the name getline(), and also utilize std::getline() for convenience, but any string-reading function would be equally good, such as fgets() from the C Standard Library:

template<typename... Ts>
std::istream& getline(std::istream& is, Ts&... values) {
    using std::getline;
    std::string str;
    getline(is, str);
    std::string_view sv{ str };
    auto get_number = [&sv](auto& value){
        std::string_view ws{ " \t\f" };
        while (ws.find(sv.front()) != std::string::npos) {
            sv.remove_prefix(1);
        }
        auto result = get_from_chars<std::remove_reference_t<decltype(value)>>(sv);
        value = result.first;
        sv.remove_prefix(result.second - sv.data());
    };
    ((get_number(values)), ...);
    return is;
}

This function takes an input stream and a parameter pack by reference, before obtaining a std::string_view from this input (an alternative could be using a std::string_view as the first parameter directly). The fold expression at the end of the function invokes a lambda for each of the items in the parameter pack in turn—this lambda has to perform a few tricks:

  • Capture the input variable sv from the enclosing function, and take a generic (auto) parameter by reference.
  • Skip preceding whitespace by repeatedly pattern matching the first character against another std::string_view containing only whitespace characters, and adjusting the start of sv.
  • Calling get_from_chars() and preserving both the value and the pointer.
  • Setting the value parameter for the caller, and adjusting the start of sv by the change in the pointer.

This second getline() template is invoked in the same way as before (parameter variables defined in advance):

int main() {
    std::println("Enter an integer, a float and another integer:");
    int a, c;
    float b;
    getline(std::cin, a, b, c);
    std::println("{} {} {}", a, b, c);
}

Preceding and intermediate whitespace is skipped, in case of an error the exception class defined above is thrown.

That’s all for this pair of articles on writing type-safe, extensible generic input functions. The use of fold expressions to unwrap template parameter packs demonstrates the power and flexibility of Modern C++, and from_chars() has measurable performance benefits over classic stream input.

Resources

Leave a comment