2
\$\begingroup\$

NOTICE: I refactored the code and made a new question like was advised.

I'm making an input validator function that is inspired by @JDługosz from the following post so that other new C++ programmers can just plug it into their code for easy input validation.

I have made it with a little deviation regarding the predicates and everything works perfectly! The problem is I see that I repeat myself in a couple of places in the code and I can't come up with a solution that doesn't cause needless complexity. The repetitions are:

  1. The custom exceptions are mostly glorified std::runtime_error.
  2. The Arithmetic and String predicates repeat the same static_asserts. However, what bothers me more is that MinMaxSymbols could inherit from MinSymbol and MaxSymbol, but from what I read isocpp STRONGLY recommends you don't use multiple inheritance for code reuse.
  3. Apart from that T input(const std::string& prompt) and std::string input(const std::string& prompt) repeat almost the same code but I don't see an elegant way to avoid that.

Also, I can't remove the templates from the string predicates MinMaxSymbols, MinSymbols, MaxSymbols because the input function that accepts the predicate is a template template.

Lastly, any feedback on the code in general is welcome!

#include <type_traits>
#include <stdexcept>
#include <iostream>
#include <string>
#include <limits>

#include <cstddef>

/* Custom exceptions--------------------------------------------------------- */
class StreamFailure final : public std::runtime_error
{
public:
    StreamFailure(const std::ios::iostate state)
        : std::runtime_error{ "Stream failed (non-recoverable state)" },
          m_state{ state }
    {}

    std::ios::iostate state() const noexcept
    {
        return m_state;
    }

private:
    std::ios::iostate m_state;
};

class ExtractionFailure final : public std::runtime_error
{
public:
    ExtractionFailure()
        : std::runtime_error{ "Failed to extract value from stream" }
    {}
};

class PredicateFailure final : public std::runtime_error
{
public:
    PredicateFailure()
        : std::runtime_error{ "Input predicate not met" }
    {}
};

class BadPredicateParams final : public std::runtime_error
{
public:
    BadPredicateParams()
        : std::runtime_error{ "Bad predicate parameters" }
    {}
};

/* Arithmetic predicates----------------------------------------------------- */
template<typename T>
class Between final
{
public:
    static_assert(std::is_arithmetic<T>::value,
        "Only arithmetic types are supported");

    constexpr Between(const T min, const T max)
        : m_min{ min }, m_max{ max }
    {
        if(min >= max)
        {
            throw BadPredicateParams{};
        }
    }

    constexpr bool operator()(const T value) const noexcept
    {
        return value >= m_min && value <= m_max;
    }

private:
    T m_min;
    T m_max;
};

template<typename T>
class GreaterThen final
{
public:
    static_assert(std::is_arithmetic<T>::value,
        "Only arithmetic types are supported");

    constexpr GreaterThen(const T min) noexcept
        : m_min{ min }
    {}

    constexpr bool operator()(const T value) const noexcept
    {
        return value > m_min;
    }

private:
    T m_min;
};

template<typename T>
class SmallerThen final
{
public:
    static_assert(std::is_arithmetic<T>::value,
        "Only arithmetic types are supported");

    constexpr SmallerThen(const T max) noexcept
        : m_max{ max }
    {}

    constexpr bool operator()(const T value) const noexcept
    {
        return value < m_max;
    }

private:
    T m_max;
};

/* String predicates--------------------------------------------------------- */
template<typename T>
class MinMaxSymbols final
{
public:
    static_assert(std::is_same<std::string, T>::value,
        "Only suported for std::string");

    constexpr MinMaxSymbols(const std::size_t min, const std::size_t max)
        : m_min{ min }, m_max{ max }
    {
        if(min >= max)
        {
            throw BadPredicateParams{};
        }
    }

    constexpr bool operator()(const std::string& str) const noexcept  
    {
        return str.size() >= m_min && str.size() <= m_max;
    }

private:
    std::size_t m_min;
    std::size_t m_max;
};

template<typename T>
class MaxSymbols final
{
public:
    static_assert(std::is_same<std::string, T>::value,
        "Only suported for std::string");

    constexpr MaxSymbols(const std::size_t max) noexcept
        : m_max{ max }
    {}

    constexpr bool operator()(const std::string& str) const noexcept
    {
        return str.size() <= m_max;
    }

private:
    std::size_t m_max;
};

template<typename T>
class MinSymbols final
{
public:
    static_assert(std::is_same<std::string, T>::value,
        "Only suported for std::string");

    constexpr MinSymbols(const std::size_t min) noexcept
        : m_min{ min }
    {}

    constexpr bool operator()(const std::string& str) const noexcept
    {
        return str.size() >= m_min;
    }

private:
    std::size_t m_min;
};

/* Custom input-------------------------------------------------------------- */
template<typename T>
T input(const std::string& prompt)
{
    if(!prompt.empty())
    {
        std::cout << prompt;
    }

    T temp{};
    std::cin >> temp;
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    constexpr auto max{ std::numeric_limits<std::streamsize>::max() };
    if(std::cin.fail())
    {
        std::cin.clear();
        std::cin.ignore(max, '\n');
        throw ExtractionFailure{};
    }
    std::cin.ignore(max, '\n');

    return temp;
}

template<>
std::string input(const std::string& prompt)
{
    if(!prompt.empty())
    {
        std::cout << prompt;
    }

    std::string temp{};
    std::getline(std::cin, temp);
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    constexpr auto max{ std::numeric_limits<std::streamsize>::max() };
    if(std::cin.fail())
    {
        std::cin.clear();
        std::cin.ignore(max, '\n');
        throw ExtractionFailure{};
    }

    return temp;
}

template<typename T, template<typename> class Predicate>
T input(const std::string& prompt, Predicate<T> pred)
{
    T temp{ input<T>(prompt) };

    if(!pred(temp))
    {
        throw PredicateFailure{};
    }

    return temp;
}

int main()
{
    int         i{};
    float       f{};
    std::string s{};

    while(true)
    {
        try
        {
            i = input<int>("Enter a int between 1-4 inclusive: ", Between<int>{ 1, 4 });
            std::cout << i << '\n';
            i = input<int>("Enter a int greater then 4: ", GreaterThen<int>{ 4 });
            std::cout << i << '\n';
            i = input<int>("Enter a int smaller then 4: ", SmallerThen<int>{ 4 });
            std::cout << i << '\n';
            i = input<int>("Enter any number that fits a int: ");
            std::cout << i << '\n';

            f = input<float>("Enter a float between 1.5 - 2.5 inclusive: ", Between<float>{ 1.5, 2.5 });
            std::cout << f << '\n';
            f = input<float>("Enter any number that fits a float: ");
            std::cout << f << '\n';

            s = input<std::string>("Enter a name (3-6 symbols): ", MinMaxSymbols<std::string>{ 3, 6 });
            std::cout << s << '\n';
            s = input<std::string>("Enter a name (3 symbols max): ", MaxSymbols<std::string>{ 3 });
            std::cout << s << '\n';
            s = input<std::string>("Enter a name (3 symbols min): ", MinSymbols<std::string>{ 3 });
            std::cout << s << '\n';
            s = input<std::string>("Enter any word: ");
            std::cout << s << '\n';
        }
        catch(const StreamFailure& e)
        {
            std::cerr << e.what() << '\n';
            std::cerr << "badbit: " << (e.state() & std::ios::badbit) << '\n';
            std::cerr << "eofbit: " << (e.state() & std::ios::eofbit) << '\n';
            std::cerr << "failbit: " << (e.state() & std::ios::failbit) << '\n';
        }
        catch(const ExtractionFailure& e)
        {
            std::cerr << e.what() << '\n';
        }
        catch(const PredicateFailure& e)
        {
            std::cerr << e.what() << '\n';
        }
        catch(const std::exception& e)
        {
            std::cerr << e.what() << '\n';
            break;
        }
    }

    return 0;
}
\$\endgroup\$
5
  • 1
    \$\begingroup\$ Incorporating advice from an answer into the question violates the question-and-answer nature of this site. You could post improved code as a new question, as an answer, or as a link to an external site - as described in I improved my code based on the reviews. What next?. I have rolled back the edit, so the answers make sense again. \$\endgroup\$ Commented Oct 8, 2023 at 9:24
  • \$\begingroup\$ but @Martin York said I'm violating the sites rules by posting it as an answer here... now I'm confused. and if I open another question with similar code and the same question I know how it works around here, it will be marked as duplicate and closed anyway. \$\endgroup\$ Commented Oct 9, 2023 at 14:12
  • 1
    \$\begingroup\$ If you posted your improved code as an answer, then you should phrase it as a review (explain what you changed and why). But if you want it to be further reviewed, then a new question is the correct thing to do. It's only a duplicate if you post the exact same code, so you needn't worry about it being closed for that reason. It's a good idea when posting the improved code to include a link back to this first version, and to summarise the improvements you've already made (and, more importantly, any suggestions you chose not to implement - so you don't get the same answers again!). \$\endgroup\$ Commented Oct 9, 2023 at 14:20
  • \$\begingroup\$ Oh, also choose one of the answers to mark as "accepted" on this question - choosing an accepted answer helps indicate that this version is "done". \$\endgroup\$ Commented Oct 9, 2023 at 14:21
  • \$\begingroup\$ @TobySpeight Thanks! il make a new question and mark an answer here \$\endgroup\$ Commented Oct 10, 2023 at 13:32

3 Answers 3

4
\$\begingroup\$

Overall:

Good idea.
But your checks seem to be designed for machine input (not user input where a user could be told about the error and can then retry). But on the other hand you are prompting for specific details which is for humans and would only slow the machine reader down. If I assume your primary use case is for "manual human input" then retry logic being built into your class is much better than throwing an exception (which is also why streams don't throw exceptions by default).

I liked the idea for validation of the input but would like the ability to chain validators so that I don't have to create a specific validator for a scenario (but could rather chain existing validators).

I don't like that you are limiting things to specific category types, I would use the language's ability to use "Duck" typeing to define how the validators work for those specific types.

Code Review:

This class seems valid as it has more state than a simple runtime error

    StreamFailure(const std::ios::iostate state)
        : std::runtime_error{ "Stream failed (non-recoverable state)" },
          m_state{ state }

But the following classes may be superfluous. But it depends. Do you see users explicitly catching these errors. If the answer is yes then go ahead and keep them. But if they are simply there for the static strings they contain I am not sure they are worth the effort.

class ExtractionFailure final : public std::runtime_error
class PredicateFailure final : public std::runtime_error
class BadPredicateParams final : public std::runtime_error

Personally. I create exception classes specifically because I think that it is worth catching the exceptions. It is worth catching the exception when there is potential corrective actions available for the user of my interface. Thus in the exception I document what it means and what I think the catcher "can" do to compensate as part of the exception.

If the exception is just a generic warning I use one of the standard exceptions and dump some information to a log file.


A lot of C++ programmer are used to use iterator like ranges. So end is one past the end. In the class Between you are not using it this way. Here end is within the range. Mathematically you are probably correct, but you have to consider your audience.

class Between final
{
public:
    ...     
    constexpr bool operator()(const T value) const noexcept
    {
        return value >= m_min && value <= m_max;
    }
    ...    
};

Why are you limiting to arithmetic types?

    static_assert(std::is_arithmetic<T>::value,
        "Only arithmetic types are supported");

A lot of code in C++ uses duck typing. If the type defines the appropriate operators then why should you not be able to use it with these classes.


Same comment here:

class MinMaxSymbols final
    static_assert(std::is_same<std::string, T>::value,
        "Only suported for std::string");

If the class T supports the size() method then why not allow the use of this class to validate it.

If you only want to support string. Why are you limiting to char type strings. Why is their no support for wchar_t strings?


Design issue on input() functions.

  1. You are forcing an extra move operation (which on some types may be a copy).
  2. You only support std::cin?
    I suppose if this is only for direct manual user input then that is fine. But a lot of time the program will be developed using user input to test and validate but when it goes into production you will then be streaming input from a file or socket (or the output of one command will be streamed to the input of this command).

I would make Input a wrapper class then overload the operator>>.

So you usage would look like this:

int i = input<int>("Testing: ");

I would change it to look like:

int i;
// Allows any input stream.
// Streams directly into the variable.
// If you are not using "human input" then turn of re-checking
std::cin >> Input(i, "Testing: ", humanInput);

--

   // You are forcing a zero construction of `temp`
    T temp{};

    // But now you overwrite the default construction.
    // Seems like the initial forced zeroing is wasted.
    std::cin >> temp;


    // The stream class already has a way to automatically throw
    // when things go wrong.
    // std::cin.exceptions(std::ios::eofbit | std::ios::badbit);
    // see: https://cplusplus.com/reference/ios/ios/exceptions/
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    constexpr auto max{ std::numeric_limits<std::streamsize>::max() };
    if(std::cin.fail())
    {
        std::cin.clear();
        std::cin.ignore(max, '\n');
        throw ExtractionFailure{};
    }



    // So the input worked.
    // Now you are ignoring the remainder of the line.
    // Not sure this is a good idea. I would say if there
    // is anything else on the line you have bad input
    // and throw an error.

    // example: Reading and int
    // User Types: 12T
    // Your code works and silently ignores the T character.
    // That to me looks like bad data.

    // example: Reading a string (for user name).
    // User Input "Loki Astari"
    // Your code works and silenlently drops the second part of the name
    std::cin.ignore(max, '\n');

    return temp;
}

If this is truly checking interactive user input. Then should you not generate an error message and ask the user to try again. The only time worth bailing is if this is NOT interactive user input (it can't be fixed so stop processing now).

At the moment, you are forcing the user of your code to wrap this in a try catch block.

int result;
while (true) {
    try {
        result = input<int>("Testing: ");
        break;
    }
    catch(ExtractionFailure const& e) {
        std::cout << "Bad Input: Try again\n";
    }
}

I would do soemmthing like this:

template<typename T>
class Input
{
    T&          dst;
    std::string prompt;
    bool        human;
    constexpr std::sszie_t max{ std::numeric_limits<std::streamsize>::max() };
    public:
        Input(T& d, std::string prompt, bool human)
            : dst(d)
            , std::move(prompt)
            , human(human)
        {}
        friend std::istream& operator>>(std::istream& s, Input& dest) {
            dest.read(s);
            return s;
        }
        void read(std::istream& s)
        {
            while (true)
            {
                if(!prompt.empty() && human)
                {
                    std::cout << prompt << std::endl;
                }
            
                if (s >> temp) {
                    // It worked

                    // If this is human "Line" based input then anything
                    // else on the line may be an error. If this is
                    // machine readable input then it may not be line based.
                    break;
                }
                if(s.eof() || s.bad())
                {
                    // Non-correctable error
                    throw StreamFailure{ std::cin.rdstate() };
                }
            
                // Correctable Error.
                // So reset the stream if this is a human.
                if (!human) {
                    throw ExtractionFailure{};
                }

                // Human input line based.
                // So clear error ignore to the end of the line
                s.clear();
                s.ignore(max, '\n');
                std::cout << "Bad input: Try again\n";
            }
        }
};

This alternative allows a single predicate.

template<typename T, template<typename> class Predicate>
T input(const std::string& prompt, Predicate<T> pred)
{
    T temp{ input<T>(prompt) };

    if(!pred(temp))
    {
        throw PredicateFailure{};
    }

    return temp;
}

Have you though about chaining tests so that you can apply multiple?

MyBrain  brain;
std::cin >> Input(brain, "Add brain characteristics: ", human) >> ValidateNerve(true) >> ValidateBundled(true) >> CheckIQLevel(85, 101 + (human ? 0 : 99));

\$\endgroup\$
4
  • \$\begingroup\$ Thanks for the in depth review! Why I don't use std::cin.exceptions - I'd rather have more specific exceptions. In terms of design I agree it needs rethinking but I'd like it to stay a function so that it resembles pythons input function. Some changes I'm considering: (1) Remove predicates completely (2) Always return std::string Allowing me to write code like this: int i = std::stoi(input("Enter a number: ")); which is similar to pythons: i = int(input("Enter a number: ")) Leaving the validation of the number to the user. any thoughts ? \$\endgroup\$ Commented Oct 6, 2023 at 11:27
  • 1
    \$\begingroup\$ This is a bad rationalization: stay a function so that it resembles pythons input function. Every language has a unique way of doing things, trying to apply the style of one language to another will always make your code look and feel bad. Learn the way that people who use the language a lot of use the language (they use it a particular way for a reason). \$\endgroup\$ Commented Oct 6, 2023 at 19:21
  • \$\begingroup\$ That being said: As long as your code is easy to read (maintenance is one of the largest costs) then lets see how it works. I am not going to comment on your answer below (Make it a proper question). \$\endgroup\$ Commented Oct 6, 2023 at 19:28
  • \$\begingroup\$ I edited the original question to have the code because I think if I make a new question with slightly different code then it will be closed for being a duplicate \$\endgroup\$ Commented Oct 8, 2023 at 6:54
2
\$\begingroup\$

I like the idea; in fact, this is something I’ve considered doing myself from time to time.

Before I get into the review, I have to ask: why is this tagged C++14? It seems peculiar to tie yourself to a standard that is almost a decade old. C++23 is likely only a couple weeks away. These days, I would expect, at a minimum, C++17 support… if not C++20.

Anyhow, on to the review:

The very first thing I notice is that you have no namespace. All code should be namespaced; you should never put code in the global namespace. It’s good practice to have your own personal namespace—say namespace globalturist—and then nested namespaces for all your projects, libraries, etc..

class StreamFailure final : public std::runtime_error

There is already a standard exception for IOstreams failures, and streams can automatically throw it if you choose.

std::ios_base::failure doesn’t truck around a copy of the stream state flags… but then, it doesn’t need to. If anyone wants to see what the state of std::cin is, they can just ask it directly.

I would ditch both StreamFailure and ExtractionFailure, and just use the standard exception.

I would go even further, and not throw exceptions on stream failures, but more on that later.

class PredicateFailure final : public std::runtime_error

I’m not sure what the benefit is to marking this exception as final. Indeed, I don’t see the point in marking every class final; that just seems seems pointlessly gratuitous, and offers no real benefits. In particular here, though, there are a lot of very good reasons why someone might want to create a more derived exception. Just consider your Between predicate. I might want more information, so I can give better error messages, so:

class value_out_of_range : public PredicateFailure
{
public:
    value_out_of_range()
        : value_out_of_range{"value is out of range"}
    {}

    explicit value_out_of_range(std::string const& msg)
        : PredicateFailure{msg}
    {}
};

class value_too_large : public value_out_of_range
{
public:
    value_too_large()
        : value_too_large{"value too large"}
    {}

    explicit value_too_large(std::string const& msg)
        : value_out_of_range{msg}
    {}
};

class value_too_small : public value_out_of_range
{
public:
    value_too_small()
        : value_too_small{"value too small"}
    {}

    explicit value_too_small(std::string const& msg)
        : value_out_of_range{msg}
    {}
};

// Using C++17 NTTPs; can do the same in C++14, but more verbose
template <auto Min, decltype(Min) Max>
class between
{
public:
    static_assert(Min < Max);

    constexpr auto operator()(T const& v) const
    {
        if (v < Min)
            throw value_too_small{};
        if (Max < v)
            throw value_too_large{};

        return true;
    }
};

try
{
    auto i = input<int>("input dice value: ", between<1, 6>{});
}
catch (value_too_small const&)
{
    std::cerr << "value was too small";
}
catch (value_too_large const&)
{
    std::cerr << "value was too large";
}

Generally, making a more specific and precise error than just “predicate failure” seems useful. I don’t see the logic in forbidding it.

class BadPredicateParams final : public std::runtime_error

If someone passes bad arguments to a predicate, that would seem to be a logic error, not a run-time error. A logic error is one that can be detected and fixed before actually attempting the operation. For example, “file open error” is a run-time error, because there is no way to know the file won’t open until you actually try to open it. “Invalid argument” is a logic error, because you can check the argument before you pass it to the function, to see if it’s valid.

And, incidentally, not only should this class probably inherit from std::logic_error, there is already std::invalid_argument. You could either replace this class with std::invalid_argument, or derive from it.

On to the Between predicate….

    static_assert(std::is_arithmetic<T>::value,
        "Only arithmetic types are supported");

But why?

Throughout your code, there is a pattern of over-constraining. You make all classes final, you make all function parameters const, and you constrain all your predicates unnecessarily. I would advise that you chill a bit; it is not necessary to be so restrictive. You should not constrain stuff unless there is a good reason to. Otherwise, leave things open, so other code can adapt, extend, or otherwise build on what you’ve provided.

In this case, not only is there no sensible reason to restrict this predicate class to arithmetic types, this constraint actually manages to both over-constrain and under-constrain at the same time. You prevent, for example, using strong alias types, extended integer types, non-numeric types that have a concept of “between”, and so on… but at the same time, Between<bool> is legal.

Generally, trying to constrain types like this is a sucker’s game in C++17 and before. Until concepts came along in C++20, seriously constraining stuff was a massive headache, when it was possible at all. What you actually want to do here, syntactically, is constrain to types that support comparisons. Traditionally, in C++, that means the less-than operator. So you want to constrain to types that support less-than comparisons that return bool. You could write that constraint in C++17 and before… but it would not be trivial. I wouldn’t bother. Either move to C++20 and use a concept, or forget about it.

And that’s just the syntactic constraints. There are also semantic constraints, and they can get extremely complex. Even here, things are much more complicated than you think. You want to restrict the Between predicate to types that are ordered… that is, types that support operator<. Except, there are different kinds of ordering! Do you want to restrict to totally ordered types? Probably not. You probably only want weak ordering. Strict weak ordering? Maybe, maybe not. As you can see, there are many flavours of “ordered”.

My advice:

  1. don’t constrain stuff unless you are sure you know:
    • what the constraints actually are, and
    • why they are the right constraints; and
  2. don’t bother to constrain anything without C++20 concepts, which not only provide much better syntactic constraint, but also provide semantics.
    constexpr Between(const T min, const T max)
        : m_min{ min }, m_max{ max }

Making the parameters const serves no purpose, and, in fact, inhibits the most basic optimization you should use when taking “sink” parameters:

    constexpr Between(T min, T max)
        : m_min{std::move(min)}
        , m_max{std::move(max)}

It won’t make a difference for scalar types, of course, but there is no reason to restrict this predicate to scalar types. It would make a huge difference for more complex types, of course.

    constexpr Between(const T min, const T max)
        : m_min{ min }, m_max{ max }
    {
        if(min >= max)
        {
            throw BadPredicateParams{};
        }
    }

In C++, the fundamental comparison operations are equals (==) and less-than (<). You’ll notice that all algorithms and such are defined in terms of those operations (for example std::sort(), std::map, and so on). In theory, all other operations will be defined in terms of those. So, in practice, in generic code it makes sense to restrict comparisons to those operations. In this case:

    constexpr Between(T min, T max)
        : m_min{std::move(min)}
        , m_max{std::move(max)}
    {
        if (not (m_min < m_max))
            throw BadPredicateParams{};
    }

The same idea applies to your greater-than predicate. Instead of:

        return value > m_min;

you should do:

        return m_min < value;

One other important thing I should mention: GreaterThen, SmallerThen, MinSymbols, MaxSymbols all have constructors that take a single value, often a simple integral type. None of them are marked explicit, which allows implicit conversions from those simple integral types. This is a bad thing. Single argument constructors should almost always be makred explicit.

Okay, now I note that you have GreaterThen, SmallerThen, and Between, and MinSymbols, MaxSymbols, and MinMaxSymbols. The list of predicates you are going to want is going to get enormous if you keep with this pattern.

I would suggest rethinking things. For example, you could define a basic predicate type that wraps any predicate, and support boolean operations to combine predicates. That way you could write:

// instead of:
auto name = input<std::string>(
    "Enter a name (3-6 symbols): ",
    MinMaxSymbols<std::string>{3, 6}
);
// you could do:
auto name = input<std::string>(
    "Enter a name (3-6 symbols): ",
    MinSymbols<std::string>{3} and MaxSymbols<std::string>{6}
);

// number must be greater than or equal 0.0, and
// less than (but not equal to!) 1.0.
auto number = input<double>("",
    equal_to{0.0} or (greater_than{0.0} and less_than{1.0}))

That would greatly shrink the number of predicates you actually need.

Another option—and, I think, the best option—is simply to take arbitrary predicates:

auto number = input<double>("", [](double v) { return v >= 0.0 and v < 1.0; });

This effectively obsoletes all of your existing predicates. For example:

// this would still work:
auto v = input<int>("", GreaterThan<int>{5});
// but so would this:
auto v = input<int>("", std::bind(std::greater<>{}, _1, 5));
// or in C++23:
auto v = input<int>("", std::bind_back(std::greater<>{}, 5));
// or:
auto v = input<int>("", [](auto x) { return x > 5; });

// and if you really wanted a named, reusable type:
auto greater_than = [](auto n) {
    return [n](auto x) { return x > n; };
};
auto v = input<int>("", greater_than(5));

Up to this point, I’ve basically said “throw this out” to everything. You don’t need most of your custom exceptions—there are standard exceptions that do the same thing—and you don’t need your predicates—you should just accept arbitrary predicates. But that stuff was all really ancillary stuff. Now we get to the actual meat of the library.

template<typename T>
T input(const std::string& prompt)

I’m torn about this interface. It’s simple, but it’s almost too simple.

The thing with exceptions is that they should be… exceptional. Which, of course, begs the question, what is exceptional? That’s always a tricky question, but particularly so when dealing with user input. How “exceptional” is it that a user would enter bad input? I would say… not very. By my thinking, you should only enter the exception handling system when things have gone unexpectedly and terribly wrong… and a user entering bad input is neither particularly rare nor catastrophic. Indeed, in a normal program run, it might be an honest and even common mistake.

On the other hand, there may be situations where it would be inconceivable and disastrous for a user to enter bad input.

But to handle the general case, I would consider using something like C++23’s std::expected. (You can make your own in C++14; it’s not complex.) That allows flexibility:

// assume:
template <typename T>
auto input(std::string const&) -> std::expected<T, std::exception_ptr>;

// you could do:
if (auto value = input<int>("give number: "); value)
    use(*value);
else
    std::rethrow_exception(value.error());

// or with the monadic interface:
input<int>("give number: ")
    .and_then(use)
    .or_else([](auto e) { std::rethrow_exception(e); })
;

// or if you think bad input will be rare, and want a clean happy path,
// you can use your existing pattern like so:
try
{
    auto value = *input<int>("give number: ");
    use(value);
}
catch (std::bad_expected_access<std::exception_ptr> const& x)
{
    try
    {
        std::rethrow_exception(x.error());
    }
    catch (StreamFailure const& x)
    {
        std::cerr << x.what() << '\n';
        std::cerr << "badbit: " << (x.state() & std::ios::badbit) << '\n';
        std::cerr << "eofbit: " << (x.state() & std::ios::eofbit) << '\n';
        std::cerr << "failbit: " << (x.state() & std::ios::failbit) << '\n';
    }
    catch(ExtractionFailure const& x)
    {
        std::cerr << x.what() << '\n';
    }
    catch (PredicateFailure const& x)
    {
        std::cerr << x.what() << '\n';
    }
    catch (std::exception const& x)
    {
        std::cerr << x.what() << '\n';
    }
}

You could also have both options, by using a tag, so input<T>("") returns expected<T, exception_ptr>, and input<T>(throw_on_error, "") returns T, but throws on error.

The other thing that bothers me about the interface is that it is hard-coded to use std::cin/std::cout and nothing else. That’s inflexible, but more importantly, it means your code is functionally untestable. (Not literally untestable, because it is possible to mock std::cin and std::cout… it’s just hard.) If your code is untestable, it will not be tested. Untested code is garbage code, not in the sense that it is “bad”, but in the sense that it is utterly useless in any real world project that expects rigorously tested code… which is any serious project… so it’s effectively garbage.

I would suggest taking a step back and rethinking the public API. It should be easy to use, flexible, and also testable. So, maybe:

template <typename T, typename Predicate>
auto input(std::istream& in, std::ostream& out, std::string_view prompt, Predicate&& pred)
    -> std::expected<T, std::exception_ptr>;

template <typename T, typename Predicate>
auto input(std::string_view prompt, Predicate&& pred)
{
    return input<T>(std::cin, std::cout, prompt, std::forward<Predicate>(pred));
}

template <typename T>
auto input(std::string_view prompt)
{
    return input<T>(std::cin, std::cout, prompt, [](auto&&) { return true; });
}

For even more flexibility, you could even do:

template <typename T, typename Char, typename Traits, typename Predicate>
auto input(
        std::basic_istream<Char, Traits>& in,
        std::basic_ostream<Char, Traits>& out,
        std::basic_string_view<Char, Traits> prompt,
        Predicate&& pred
    )
    -> std::expected<T, std::exception_ptr>;

template <typename T, typename Predicate>
auto input(std::string_view prompt, Predicate&& pred)
{
    return input<T>(std::cin, std::cout, prompt, std::forward<Predicate>(pred));
}

template <typename T, typename Predicate>
auto input(std::wstring_view prompt, Predicate&& pred)
{
    return input<T>(std::wcin, std::wcout, prompt, std::forward<Predicate>(pred));
}

template <typename T>
auto input(std::string_view prompt)
{
    return input<T>(std::cin, std::cout, prompt, [](auto&&) { return true; });
}

template <typename T>
auto input(std::wstring_view prompt)
{
    return input<T>(std::wcin, std::wcout, prompt, [](auto&&) { return true; });
}

And, of course, you could add overloads that take a tag, and return T instead of std::expected<T, std::exception_ptr>, but throw on error.

You’ll note I used C++17’s std::string_view rather than std::string const& parameters, because that is the proper way to take string values you just want to inspect. If you’re stuck with C++14, you’ll either have to roll your own, use a library, or just bite the bullet and accept you will be paying the cost of string constructions unnecessarily.

Now let’s actually dive into the function:

    T temp{};
    std::cin >> temp;

It’s important to note that you are relying on T being default constructible (and cheaply so). That won’t always be true, so to support other types you should probably add some overloads that take an initial T value somehow, and use that to input into. Avoid copying it, too, because that would just be assuming it is copyable. This can get really tricky!

    T temp{};
    std::cin >> temp;
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    constexpr auto max{ std::numeric_limits<std::streamsize>::max() };
    if(std::cin.fail())
    {
        std::cin.clear();
        std::cin.ignore(max, '\n');
        throw ExtractionFailure{};
    }
    std::cin.ignore(max, '\n');

Okay, you’re doing IOstreams error handling all wrong. For starters, EOF is not an error. Let me illustrate with this simple program:

# buildfile ####################################################

using cxx

exe{prog} : cxx{*}
// prog.cxx ////////////////////////////////////////////////////

#include <iostream>

auto main() -> int
{
    using type = int;

    auto v = type{};
    std::cin >> v;

    std::cout.setf(std::ios_base::boolalpha);

    if (std::cin)
        std::cout << "SUCCESS! ";
    else
        std::cout << "FAILURE! ";

    std::cout << "v = " << v << '\n';

    std::cout << "okay: " << bool(std::cin) << '\n';
    std::cout << "good: " << std::cin.good() << '\n';
    std::cout << "eof:  " << std::cin.eof() << '\n';
    std::cout << "fail: " << std::cin.fail() << '\n';
    std::cout << "bad:  " << std::cin.bad() << '\n';
}
$ b
c++ cxx{prog} -> obje{prog}
ld exe{prog}           
$ printf '123\n' | ./prog
SUCCESS! v = 123
okay: true
good: true
eof:  false
fail: false
bad:  false
$ printf '123' | ./prog
SUCCESS! v = 123
okay: true
good: false
eof:  true
fail: false
bad:  false
$ 

You can see that we got successful reads both when EOF was set and when it was not. Here are two more examples to show how things work:

$ printf '123x456' | ./prog
SUCCESS! v = 123
okay: true
good: true
eof:  false
fail: false
bad:  false
$ printf 'x123x456' | ./prog
FAILURE! v = 0
okay: false
good: false
eof:  false
fail: true
bad:  false
$ 

So EOF is not an error, but fail always is. fail will always be true on an input failure, regardless of the cause. It will never be true if anything was successfully read, regardless of the state of the stream. In fact, operator bool for streams is literally just not fail().

So the correct way to check for valid input is either not cin.fail(), or just bool(cin). In other words:

    T temp{};
    if (not (std::cin >> temp))
    {
        // there was some kind of error

        // If std::cin.bad() is true, then cin is broken. There is
        // nothing you can do. It's over.

        // If std::cin.eof() is true, then there is no more input.
        // Again, there is nothing you can do. It's over.

        // If neither of the above is true, then there was just a
        // parsing error. There may be more input, and it may even
        // be valid.
        //
        // If you are doing line-oriented input, as you seem to be,
        // this would be where you'd ignore the rest of the line,
        // but otherwise leave cin in a valid state. You'd return an
        // error, because this input did fail, but you would allow
        // further inputs.
    }

    // This is dangerous!!!
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    return temp;

You’ll note I marked the ignore line as dangerous. That’s because, at that point, you have successfully read your input and are ready to return it… but if you then do the ignore and that fails… it may trigger an exception that wipes out all of your success so far. There is nothing you can do about that, though; if the user has enabled exceptions, this would be on their head. Still, it’s important to be aware that it might happen.

I would also point out that after a successful read, maybe you don’t want to just ignore the rest of the line. If I ask for a number, and the user does “123⏎” then that’s fine: I got “123” as input. But if the user does “123xyz⏎”… what does that mean? Should I accept “123” as input and ignore the rest? Seems dangerous, especially if the next input would have accepted “xyz”… maybe the user was just typing too fast, and didn’t realize they’d made an error, which would create incorrect input for the rest of the program… which might be very dangerous. Imagine:

enter the code number: 123
should i delete the entire hard drive: n
begin operation? y
done; quit now? y

That’s “123⏎n⏎y⏎y⏎”. But the user doesn’t hit the enter key hard enough after the first line, so it’s “123n⏎y⏎y⏎”, and “123n” gets read as “123”, the “n” gets ignored, and the next input… which is “y”… goes to the next line. You end up one “y” short… for the question about quitting, after the hard drive has already been erased. Ouch.

On top of that, reading input from std::cin directly is generally a dodgy prospect. You don’t know what the user has done with std::cin. Have they enabled exceptions? Have they changed the locale (which would affect how numbers are read)?

I would advise a different pattern. You are reading line-based input. I would suggest leaning into that, and actually reading whole lines from std::cin, and parsing them after the fact. For example:

template <typename T>
auto input(std::string const& prompt)
{
    // What happens if this output fails? Maybe you should check
    // for that, and throw?
    if (not prompt.empty())
        std::cout << prompt;

    auto line = std::string{};
    if (not std::getline(std::cin, line))
        // If this failed, then either cin was bad, or there was
        // no more input to read. So:
        throw StreamFailure{std::cin.rdstate()};

    // If T is std::string, then we’re done! Just return `line`!

    // Now we create an istringstream with line as the source data:
    auto iss = std::istringstream{std::move(line)};

    // Optional, copy the formatting data and locale from std::cin:
    iss.copyfmt(std::cin);
    iss.exceptions(std::cin::goodbit);  // we don't want exceptions
                                        //
                                        // ... or do we? you could
                                        // enable exceptions, and they
                                        // they will be automatically
                                        // thrown... but it will be
                                        // `std::ios_basefailure`,
                                        // not your custom exception.

    auto temp = T{};
    if (not (iss >> temp))
        // No need to do any cleanup on cin!
        throw ExtractionFailure{};

    // Okay, now we have successfully read a T value... but was
    // there any trailing garbage? Easy to test!
    if (iss.peek() != std::istringstream::traits_type::eof())
        // There is trailing garbage.
        throw ???;

    return temp;
}

You can see how much easier that is, because we only touch std::cin once. If there are any errors with the stream itself, we detect them on that one getline() read. But once that succeeds, the only errors will be parsing errors, and if that happens… well, std::cin is already in the correct state, so we don’t need to mess with it again.

And there are a lot of other benefits from doing it this way. For example, we don’t need to construct T at all if std::cin is bad.

Alright, moving on:

template<>
std::string input(const std::string& prompt)

Don’t specialize function templates.

What you want to do is delegate to a class, and specialize that, if necessary:

template <typename T>
struct input_impl
{
    auto operator()(std::string const&) -> T
    {
        // ...
    }
};

template <>
struct input_impl<std::string>
{
    auto operator()(std::string const&) -> std::string
    {
        // ...
    }
};

template <typename T>
auto input(std::string const& prompt)
{
    return input_impl<T>{}(prompt);
}

Or, even better, use the modern “neibloid” pattern:

template <typename T>
struct input_impl
{
    auto operator()(std::string_view prompt) const
        -> std::expected<T, std::exception_ptr>
    {
        return input<std::string>(prompt)
            .and_then([](auto line)
            {
                try
                {
                    auto iss = std::istringstream{std::move(line)};

                    iss.copyfmt(std::cin);
                    iss.exceptions(std::cin::goodbit);

                    auto temp = T{};
                    if (not (iss >> temp))
                        throw std::ios_base::failure{"failed to parse value"};

                    if (iss.peek() != std::istringstream::traits_type::eof())
                        throw std::ios_base::failure{"trailing garbage after parsed value"};

                    return temp;
                }
                catch (...)
                {
                    return std::unexpected{std::current_exception()};
                }
            });
    }
};

template <>
struct input_impl<std::string>
{
    auto operator()(std::string_view prompt) const
        -> std::expected<std::string, std::exception_ptr>
    {
        try
        {
            if (not prompt.empty())
                std::cout << prompt;

            auto line = std::string{};
            if (not std::getline(std::cin, line))
                throw std::ios_base::failure{"could not read from cin"};

            return line;
        }
        catch (...)
        {
            return std::unexpected{std::current_exception()};
        }
    }
};

template <typename T>
inline constexpr auto input = input_impl<T>{};

Moving on:

template<typename T, template<typename> class Predicate>

Okay, you’ve already run into problems with Predicate being a template template parameter:

Also, I cant remove the templates from the string predicates MinMaxSymbols, MinSymbols, MaxSymbols because the input function that accepts the predicate is a template template.

So… why? Why is the predicate a template template parameter? What does that help?

The thinking seems to be that for any type T<U> that has a function call operator, that function call operator will take a U. In other words:

template <typenaem U>
struct T
{
    auto operator()(U) const -> bool;
};

But that’s not really true. There’s no reason a template that takes a single argument would have an operator call at all (example: std::atomic<T>). And there are a lot of cases where a template that takes a single parameter has a call operator that takes a different type (example: std::uniform_int_distribution<T>).

So there is really no sensible reason to constrain the predicate only to types that are:

  1. templates
  2. with only one template parameter
  3. which is the same type as the call operator parameter.

And even with all that over-constraining, the predicate is still under-constrained, because you don’t check that it returns a type convertible to bool.

Again, don’t bother constraining until C++20. It’s not worth it. In C++20, constraining the predicate is simple:

template <typename T, std::predicate<T> Predicate>
auto input(std::string_view prompt, Predicate pred) -> T;

That’s pretty much all you need.

Replicating this behaviour before C++20 is very hard, though. It’s just not worth it.

If you just make Predicate an ordinary template parameter, you no longer need the superfluous std::string template arguments for all those predicates, and you allow arbitrary predicates. There is no sensible reason to constrain the predicate to be a template, let alone a template that specifically only takes a single parameter, that happens to be the same type that you’re testing.

I’m not going to bother reviewing the main() function, because it’s just for testing/demonstration purposes. I will point out that if you made the interface properly testable, you could write much better tests that don’t require manual checking. For example:

// Not using any particular testing platform. Also, this test
// should actually be split into multiple tests - one for the
// value, one to make sure the prompt works. This is just a
// very basic demo.
TEST
{
    auto oss = std::ostringstream{};
    auto iss = std::istringstream{"1234"};

    auto value = input<int>(iss, oss, "gimme int: ");

    CHECK(value == 1234);

    CHECK(oss.str() == "gimme int: ");
}

Before I wrap up the review, I’ll address some of your concerns:

The custom exceptions are mostly glorified std::runtime_error.

That’s not really a bad thing. It’s just unfortunate that deriving exceptions requires so much boilerplate.

Personally, I like to use Boost.Exception. It massively simplifies making exceptions, and gives a lot of extra power when using them. Most of my projects have error handling structured like this:

namespace indi {

struct exception
    : virtual std::exception
    , virtual boost::exception
{};

namespace inputlib {

struct predicate_failure : virtual exception {};
    struct input_value_out_of_range : virtual predicate_failure {};
        struct input_value_too_large : virtual input_value_out_of_range {};
        struct input_value_too_small : virtual input_value_out_of_range {};
    struct input_value_length_error : virtual predicate_failure {};
        struct input_value_too_long : virtual input_value_length_error {};
        struct input_value_too_short : virtual input_value_length_error {};

} // namespace inputlib

} // namespace indi

auto func()
{
    // in cases where i want to handle specific error types:
    try
    {
        // ...
    }
    catch (indi::inputlib::input_value_too_long const&)
    {
        std::cerr << "input too long!";
    }
    catch (indi::inputlib::input_value_too_short const&)
    {
        std::cerr << "input too short!";
    }
}

// in main() to print diagnostics for any unhandled exceptions
// (technically i usually use a lippincott function, but meh)
auto main() -> int
{
    try
    {
        // ...
    }
    catch (...)
    {
        std::cerr
            << "[ERROR]:\n"
            << boost::current_exception_diagnostic_information();
    }
}

I would mention that std::runtime_error is not always the right error to use. For logic errors, you should use a type that derives from std::logic_error, not std::runtime_error. And there are several pre-defined exception types that are more suitable for specific errors.

The Arithmetic and String predicates repeat the same static_asserts. However, what bothers me more is that MinMaxSymbols could inherit from MinSymbol and MaxSymbol, but from what I read [isocpp][2] STRONGLY recommends you dont use multiple inheritance for code reuse

I mean, it’s not really a bad thing that the static asserts are repeated. If you really wanted, you could make a base class with all the required static asserts, and derive from that. But I wouldn’t bother.

Yes, making MinMaxSymbols multiply-derive from MinSymbols and MaxSymbols would be a terrible idea, for a number of reasons. First of all, it would require turning a simple call into a virtual call, for no good reason.

More importantly, though, it would be nonsensical, because a MinMaxSymbols predicate is not a MinSymbols predicate or a MaxSymbols predicate, let alone both. Suppose I had a MinSymbols{3}, and you were relying on its behaviour, which is that it will pass "abc" and "abcdefg", but fail "ab". Then I silently replace it with MinMaxSymbols{3,6}… which should work if a MinMaxSymbols{3,6} IS-A MinSymbols{3}. Except now, the behaviour you were relying on is no longer true. "abc" still passes, but now "abcdefg" does not. So, clearly, MinMaxSymbols{3,6} is not a MinSymbols{3}, and should not derive from it.

Inheritance is the wrong model here. What you want is composition:

struct MinSymbols
{
    // ... what you already have ...
};

struct MaxSymbols
{
    // ... what you already have ...
};

class MinMaxSymbols
{
    MinSymbols _min_check;
    MaxSymbols _max_check;

public:
    constexpr MinMaxSymbols(std::size_t min, std::size_t max)
        : _min_check{min}
        , _max_check{max}
    {
        if (max < min)
            throw BadPredicateParams{};
    }

    constexpr auto operator()(std::string const& s) const noexcept
    {
        return _min_check(s) and _max_check(s);
    }
};

I would even use composition for the GreaterThen and SmallerThen, using the standard function objects. For example:

template <typename T, typename Compare>
class comparison_operation
{
public:
    constexpr explicit comparison_operation(T limit) noexcept
        : _limit{std::move(limit)}
    {}

    constexpr auto operator()(T const& value) const noexcept
    {
        return _compare(value, _limit);
    }

private:
    [[no_unique_address]] Compare _compare;
    T _limit;
};

template <typename T>
using greater_than = comparison_operation<T, std::greater<T>>;
template <typename T>
using greater_than_or_equal = comparison_operation<T, std::greater_equal<T>>;
template <typename T>
using less_than = comparison_operation<T, std::less<T>>;
template <typename T>
using less_than_or_equal = comparison_operation<T, std::less_equal<T>>;

Apart from that T input(const std::string& prompt) and std::string input(const std::string& prompt) repeat almost the same code but I dont see an elegant way to avoid that.

Sometimes, the best thing to do is take a step back, go do something else, and come back to look at a problem with a fresh eye.

I made a lot of suggestions for refactoring, but let’s ignore them, and look at your code as it is.

So, your string input function is this:

template<>
std::string input(const std::string& prompt)
{
    if(!prompt.empty())
    {
        std::cout << prompt;
    }

    std::string temp{};
    std::getline(std::cin, temp);
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    constexpr auto max{ std::numeric_limits<std::streamsize>::max() };
    if(std::cin.fail())
    {
        std::cin.clear();
        std::cin.ignore(max, '\n');
        throw ExtractionFailure{};
    }

    return temp;
}

Now, there’s nothing interesting about the first part (outputting the prompt). The next part… well, I mean, it’s only three statements, two of which are identical to the general input function, but one differs; when only 2 very simple lines of code are duplicated (and separated by a line that differs), it’s probably not worth worrying about duplication.

But then there’s the third part, which handles the case where parsing the string fails.

Did you catch that?

Let me repeat it: the third part handles the case where parsing the string fails.

See it now?

If not, just consider these questions: What, exactly, are you parsing when just directly inputting a string? And… how can that fail?

Here’s another way of looking at it. Suppose your input is “the first line⏎the second line⏎the third line⏎”. Suppose reading the first line fails—don’t ask me how, just assume for the thought experiment. So, the getline() call would read “the first line⏎”, leaving “the second line⏎the third line⏎” in the input. But there was a failure, so you enter that if block, where the fail bit is cleared, and then… you… ignore… everything… up to the next newline. So… “the second line⏎” just gets ignored… and the input is now “the third line⏎”. You just lost a whole line of input.

😬

So, here’s where I just give the punch line:

  1. string input “parsing” can never fail, because string input is not parsed, it’s just read; and
  2. even if it could fail, you have already consumed the whole line.

Which means… there’s actually a lot less duplicated logic here than you thought.

Here is what your string input function should look like:

template<>
std::string input(const std::string& prompt)
{
    if(!prompt.empty())
    {
        std::cout << prompt;
    }

    std::string temp{};
    std::getline(std::cin, temp);
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    // Parsing cannot fail, so this whole block is superfluous.
    //
    // But even if it weren't, you should not do the ignore().
    /*
    constexpr auto max{ std::numeric_limits<std::streamsize>::max() };
    if(std::cin.fail())
    {
        std::cin.clear();
        // std::cin.ignore(max, '\n');  // <-- DON'T DO THIS!!!
        throw ExtractionFailure{};
    }
    */

    return temp;
}

Or, in other words:

template<typename T>
T input(const std::string& prompt)
{
    if(!prompt.empty())
    {
        std::cout << prompt;
    }

    T temp{};
    std::cin >> temp;
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    constexpr auto max{ std::numeric_limits<std::streamsize>::max() };
    if(std::cin.fail())
    {
        std::cin.clear();
        std::cin.ignore(max, '\n');
        throw ExtractionFailure{};
    }
    std::cin.ignore(max, '\n');

    return temp;
}

template<>
std::string input(const std::string& prompt)
{
    if(!prompt.empty())
    {
        std::cout << prompt;
    }

    std::string temp{};
    std::getline(std::cin, temp);
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    return temp;
}

A lot less duplication than you thought, eh?

But, just for fun, let’s pretend that the two functions are identical, except for that one line. Could we avoid the duplication? Trivially! You’re just specializing in the wrong place:

template <typename T>
auto do_input(T& t)
{
    std::cin >> t;
}

auto do_input(std::string& s)
{
    std::getline(std::cin, s);
}

template <typename T>
auto input(std::string const& prompt)
{
    if(!prompt.empty())
    {
        std::cout << prompt;
    }

    T temp{};
    do_input(temp);
    if(std::cin.eof() || std::cin.bad())
    {
        throw StreamFailure{ std::cin.rdstate() };
    }

    constexpr auto max{ std::numeric_limits<std::streamsize>::max() };
    if(std::cin.fail())
    {
        std::cin.clear();
        std::cin.ignore(max, '\n');
        throw ExtractionFailure{};
    }
    std::cin.ignore(max, '\n');

    return temp;
}

See? Sometimes stepping back and coming back to code with a fresh eye can be magical!

Summary

The code is largely quite good. I didn’t detect any real bugs, except for the issues that arise from mishandling IOStreams exception handling. Aside from that, the code will work, just fine, in all cases, so far as I can tell.

It is also quite good stylistically and idiomatically; clear, logical, well-structured, except the one issue of specializing function templates.

Overall, quite good, bordering on excellent.

There are 3 main issues that come up repeatedly in the code.

  1. There is a pattern of over-constraining.

    Slapping final on every class is… absurd. I’m sure there is some crappy style guide out there that recommends this—there are a lot of crappy style guides out there (looking at you, Google style guide)—but it is generally pointless and actively crippling in some cases. final should only go on leaf classes in a hierarchy (which should usually be the first non-abstract class).

    Slapping const on every function parameter seems less silly… but is just as absurd, though understanding why is trickier. Basically, there are multiple types of function parameters; if you are only reading their values, you should take them as const& (which makes const superfluous)… if you are taking their values—and giving them to data members, for example—then you should take them by value, and move them. The only time you should use const is for trivial types, which you will always take by value.

    Insisting on types that conform to a very, very specific interface, when that interface is not actually required, is frustrating… as you discovered yourself when you had to make certain types templates for no good reason. If all you want is something with a call operator that takes a T and returns a bool… then that’s all you need. There is no point in demanding anything else. You don’t need it to be a template. You don’t need it to take T as its one and only template parameter.

    Trying to constrain types before C++20 is a sucker’s game. It can be done, theoretically, in a limited way… but it takes so much effort, and requires so much complicated code using arcane tricks like SFINAE, and even then only offers limited payoff, that it’s really not worth it.

  2. There is generally poor usage of the standard library.

    There are a lot of cases where existing standard types serve the exact same purpose as your custom exceptions, like std::invalid_argument and BadPredicateParams and std::ios_base::failure and StreamFailure. That doesn’t necessarily mean you shouldn’t make your own custom exception types… but it does mean you should consider using those more specific library types as the base.

  3. There are some misconceptions about error handling in the IOStreams library, which, to be fair, is quite complex.

    The main type of failure you need to be concerned when reading input is “failure”. If you didn’t get a “failure” error, then you successfully read and parsed valid input. Doesn’t matter what else is going on in the universe; you got what you asked for.

    Only after you detected a failure should you bother to discern whether it was a parse error without hitting the end of the stream (in which case, only failbit is set), a parse error because of hitting the end of the stream (in which case eofbit will be set), or a parse error due to a stream error (in which case badbit will be set). But always, always, always, the parse error comes first, so failbit will always be set on any kind of failure; if failbit is not set, then success, no matter what the other bits are.

    In practice, that means all you need to do is if (in >> t) or if (std::getline(in, s)), and if that succeeds, you’re done. Both of those operations will internally construct the sentry and check for badbit and eofbit automatically. If that test fails, then it might have already thrown std::ios_base::failure or some other exception. If it didn’t throw, then, and only then, should you bother to check for badbit or eofbit yourself… and only if you want to throw an exception with extra information. But I wouldn’t bother; I’d just throw std::ios_base::failure. If the user cares about why it failed (parsing, EOF, or stream failure), they can always check the stream themselves. No point in doing that extra work if they don’t care, and if they do care, they need to do the work themselves anyway… so just leave it to them.

I also suggest a refactor to avoid template function specialization. At the very least, make a single function template and delegate to a class template (which can then be specialized as you please). But you might want to consider the neibloid pattern.

And I would suggest considering an interface that makes exceptions optional… just like IOStreams itself does. I can’t imagine most code wanting to input stuff won’t be expecting the possibility of bad input, so it’s hardly an exception situation. Returning a type similar to std::expected allows the user to decide whether they want to deal with malformed input exceptionally or not.

And finally, I would strongly suggest moving to a more modern standard. C++14 is ancient. All modern compilers default to C++17 these days. There is no good reason for new code to hamstring itself by using such an old standard.

\$\endgroup\$
3
  • \$\begingroup\$ Hello! thanks for the feedback but a couple of notes: (1) I come from the embedded world where c++11/14 is as modern as you will get (2) this is my first time in years using the stl because by default I have no exceptions and no rtti and especially no iostream so I am bad at it (3) I based my stream error checks on this and this and they seem plenty convincing to me (4) I updated my current approach in the question if you are interested \$\endgroup\$ Commented Oct 9, 2023 at 14:09
  • \$\begingroup\$ 1) I also do embedded programming, and use C++17 and even C++20. C++17’s expanded constexpr has been a godsend in freestanding and embedded contexts. 2) That’s fine, IOStreams is horrible and hard to use in any case. 3) The first link is just demonstrating ignore(), not proper IOStreams error handling. The second is better (though there are some red flags… never use exit() in C++!), though a) it just ignores trailing garbage rather than detecting it as an error; and b) it ignores all the other complexity of IOStreams states and locales. 4) I’ll check it out! \$\endgroup\$
    – indi
    Commented Oct 11, 2023 at 20:22
  • \$\begingroup\$ sadly you are the minority, I agree c++17 and c++20 is better for embedded but where I'm from its rare in the extreme... sadly people refuse to move forward \$\endgroup\$ Commented Oct 12, 2023 at 10:15
1
\$\begingroup\$

I do not see a good reason for erroring when min == max.

Consider allowing the == case

// if(min >= max) {
if(min > max) {
  throw BadPredicateParams{};
}
\$\endgroup\$
1
  • \$\begingroup\$ Possibly debatable. If min == max, what's the point in asking for input? It seems that returning the single allowed value immediately without interaction would be better than throwing an exception: if (min == max) { return min; } \$\endgroup\$ Commented Oct 12, 2023 at 7:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.