Another type of data validation in C++

A need that I met one day was to make sure some values are being properly controlled no matter who changes them.

struct Input {
    int a;
    int b;
};
    • a must be maximum 50 – if a greater value is assigned, 50 must be set
    • b must be minimum 50 – if a lower value is assigned, 50 must be set
    • if b is greater than 100, the flow cannot continue, the execution must be stopped

The fields in the struct can be changed by multiple layers of the application, so their values must be checked after each change. Possible solutions:

    • an API that assigns and verifies the values: each layer must use the API
    • an API that only verifies the values: after each layer updates the values, the API must be used by the caller
    • setters defined on the struct: SetA(int), SetB(int)

The API solutions require extra work; someone must use the API and not forget about it otherwise bugs could be introduced. The setters solution forces the usage of those methods, but I don’t want to rely on OOP here; instead, I want to go for a data-oriented approach and keep my struct as simple as possible.

I would like for a property of the struct to be configured in such a way that every time it’s being assigned a value, that value is verified against some requirements. In larger projects with layers that need to mutate some data passed around, it might be safer to go this way instead of relying on people to remember to explicitly do something.

How it looks

Someone told me they would like to see something like this:

struct Input {
    wrapped_value<int> a;
    wrapped_value<int> b;
};

wrapped_value is a wrapper that receives any value assigned to the property it wraps and makes sure it’s valid. The type of the property is passed as a template argument to the wrapper.

a and b should behave like their original types. Wrapping them, they are no longer integers, but wrapped_values.

Assign a value

The first thing is to make the wrapper accept the value that I want the wrapped property to have.

#include <cassert>

template <typename Value>
class wrapped_value {
   public:
    wrapped_value(Value&& value) : value_{value} {}
    Value value_;
};

struct Input {
    wrapped_value<int> a;
    wrapped_value<int> b;
};

int main()
{
    Input input{0, 0};
    input.a = 100;
    input.b = 30;
    assert(input.a.value_ == 100);
    assert(input.b.value_ == 30);
}

Manipulate the value

Now, each time a value is given, it should be limited to 50. Another template argument holds the limit.

#include <algorithm>
#include <cassert>

template <typename Value, int Limit>
class wrapped_value {
   public:
    wrapped_value(Value&& value) : value_{std::min(value, Limit)} {}
    Value value_;
};

struct Input {
    wrapped_value<int, 50> a;
    wrapped_value<int, 50> b;
};

int main()
{
    Input input{0, 0};
    input.a = 100;
    input.b = 30;

    assert(input.a.value_ == 50);
    assert(input.b.value_ == 30);
}

Behave like the original type

Next, I want the wrapped values to behave like their given type, I want to operate on them:

    • assign values in different ways: input.a += 1;
    • increment and decrement: --input.a;
    • use them in arithmetic operations to add/subtract/multiply/divide them: 2 + input.a;
    • logically use them: if (input.a && input.b) {...};
    • make comparisons: if (input.a > 1 || input.a <= input.b) {...};
    • stream to output: std::cout << input.a;
    • pass a wrapped value as an argument to a function expecting the wrapped value’s type

All the above cases are implemented by overloading some specific operators. Below there is an example with only some of the operators overloaded.

#include <algorithm>
#include <cassert>

template <typename Value, int Limit>
class wrapped_value {
   public:
    wrapped_value(Value&& value) : value_{std::min(value, Limit)} {}

    wrapped_value& operator+=(const Value& value)
    {
        value_ = std::min(value_ + value, Limit);
        return *this;
    }

    wrapped_value& operator--()
    {
        value_ = std::min(value_ - 1, Limit);
        return *this;
    }

    operator const Value&() const noexcept { return value_; }

   private:
    Value value_;
};

struct Input {
    wrapped_value<int, 50> a;
    wrapped_value<int, 50> b;
};

int inc(int a) { return a + 1; }

int main()
{
    Input input{0, 0};
    input.a = 100;
    input.b = 30;

    assert(input.a == 50);
    assert(input.b == 30);

    --input.a;
    input.b += 55;

    assert(input.a == 49);
    assert(input.b == 50);

    assert(inc(input.a) == 50);
}

Most of the operators do not need to be overloaded because of the user-defined conversion function. By overloading operator Value&(), every time I use the wrapped value in its original type context, the usage will be on the wrapped value’s type: wrapped_value<int> will be handled as an int. What’s left to overload are the operators I want to manipulate the value with.

Manipulate the value in multiple ways

This implementation is limited to a specific use case: limiting the given value. It should be customizable to support any need. For this situation, I changed the wrapped_value to receive a list of wrappers, which are functors that implement each case: limiting the lower bound, limiting the upper bound, throwing an exception if the value is not valid, and any other requirement. The requirement of a wrapper functor is to receive the value as an argument and to return it after implementing its logic.

template <typename Value, int Limit>
struct UpperBoundLimiter {
    constexpr Value operator()(const Value& value) const noexcept {
        return std::min(value, Limit);
    }
};

And a property of the struct is wrapped as:

wrapped_value<int, UpperBoundLimiter<int, 50>, SecondWrapper, ThirdWrapper> a;

The wrappers are stored internally into a tuple that is statically iterated when the stored value is changed (assign, increment etc), each of them being called for the new value in a pipeline (each wrapper computes a value that is passed to the next wrapper).

#include <algorithm>
#include <cassert>
#include <stdexcept>
#include <tuple>

template <typename Wrappers, std::size_t Size = std::tuple_size<Wrappers>::value, std::size_t Index = Size - 1>
class wrappers_iterator {
   public:
    template <typename Value>
    void operator()(Wrappers& wrappers, Value&& new_value, Value& value) const
    {
        value = std::get<Size - Index - 1>(wrappers)(std::forward<Value>(new_value));
        wrappers_iterator<Wrappers, Size, Index - 1>{}(wrappers, std::forward<Value>(value), value);
    }
};

template <typename Wrappers, std::size_t Size>
class wrappers_iterator<Wrappers, Size, 0> {
   public:
    template <typename Value>
    void operator()(Wrappers& wrappers, Value&& new_value, Value& value) const
    {
        value = std::get<Size - 1>(wrappers)(std::forward<Value>(new_value));
    }
};

template <typename Value, typename... Wrappers>
class wrapped_value {
    static_assert(sizeof...(Wrappers) > 0, "at least one wrapper required");

   public:
    wrapped_value(Value&& value) : value_{value} { iterate_(wrappers_, std::forward<Value>(value), value_); }

    wrapped_value& operator+=(const Value& value)
    {
        iterate_(wrappers_, value_ + value, value_);
        return *this;
    }

    wrapped_value& operator--()
    {
        iterate_(wrappers_, value_ - 1, value_);
        return *this;
    }

    operator const Value&() const noexcept { return value_; }

   private:
    Value value_;

    using WrappersType = std::tuple<Wrappers...>;
    static WrappersType wrappers_;
    static wrappers_iterator<WrappersType> iterate_;
};

template <typename Value, typename... Wrappers>
std::tuple<Wrappers...> wrapped_value<Value, Wrappers...>::wrappers_ = {};

template <typename Value, typename... Wrappers>
wrappers_iterator<std::tuple<Wrappers...>> wrapped_value<Value, Wrappers...>::iterate_ = {};

template <typename Value, int Limit>
struct LowerBoundLimiter {
    constexpr Value operator()(const Value& value) const noexcept { return std::max(value, Limit); }
};

template <typename Value, int Limit>
struct UpperBoundLimiter {
    constexpr Value operator()(const Value& value) const noexcept { return std::min(value, Limit); }
};

template <typename Value, int Limit>
struct UpperLimitValidator {
    Value operator()(const Value& value) const
    {
        if (value > Limit) {
            throw std::range_error{"upper limit error"};
        }
        return value;
    }
};

struct Input {
    wrapped_value<int, UpperBoundLimiter<int, 50>> a;
    wrapped_value<int, UpperLimitValidator<int, 100>, LowerBoundLimiter<int, 50>> b;
};

int inc(int a) { return a + 1; }

int main()
{
    Input input{0, 0};
    input.a = 100;
    input.b = 30;

    assert(input.a == 50);
    assert(input.b == 50);

    --input.a;
    try {
        input.b += 55;
        assert(false);
    }
    catch (const std::range_error&) {
    }

    assert(input.a == 49);
    assert(input.b == 50);

    assert(inc(input.a) == 50);
}

What’s next?

Other types

The first limitation I’ve seen is when I want a wrapper that uses double or other types that cannot be used as template parameters until C++20. I’ve implemented everything for C++11 with GCC 9.3. I cannot have the following:

template <typename Value, double Min, double Max>
struct LimitValidator {
    Value operator()(const Value& value) const
    {
        if (value < Min || value > Max) {
            throw std::range_error{"limit error"};
        }
        return value;
    }
};

A non-type template parameter cannot have type ‘double’ before C++20

This is something I did not focus on, but a workaround could be:

template <typename Value>
struct LimitValidator {
    double min = 10.0;
    double max = 20.0;

    Value operator()(const Value& value) const
    {
        if (value < min || value > max) {
            throw std::range_error{"limit error"};
        }
        return value;
    }
};

struct Input {
    wrapped_value<double, LimitValidator<double>> a;
};

Performance

The good thing is that (most of?) the implementation is compile-time work and it will probably get inlined with some optimization level (to be tested).

And there is no size overhead. If I have a property of 8 bytes, the wrapped property is the same size.

What I still have to consider is passing the value from one wrapper to another. I’ve used an rvalue to pass the value, but I did it more like a TODO, I did not actually pay a lot of attention to this aspect.

Implementation correctness

Aspects to analyze: Const correctness, noexcept specifier, copyable/movable.

A more advanced example

#include <algorithm>
#include <cassert>
#include <iostream>
#include <sstream>
#include <tuple>

namespace msd {

template <typename Wrappers, std::size_t Size = std::tuple_size<Wrappers>::value, std::size_t Index = Size - 1>
class wrappers_iterator {
   public:
    template <typename Value>
    Value operator()(Wrappers& wrappers, Value&& new_value) const
    {
        auto value = std::get<Size - Index - 1>(wrappers)(std::forward<Value>(new_value));
        return wrappers_iterator<Wrappers, Size, Index - 1>{}(wrappers, std::move(value));
    }
};

template <typename Wrappers, std::size_t Size>
class wrappers_iterator<Wrappers, Size, 0> {
   public:
    template <typename Value>
    Value operator()(Wrappers& wrappers, Value&& new_value) const
    {
        return std::get<Size - 1>(wrappers)(std::forward<Value>(new_value));
    }
};

template <typename Value, typename... Wrappers>
class wrapped_value {
    static_assert(sizeof...(Wrappers) > 0, "at least one wrapper required");

   public:
    wrapped_value() noexcept(std::is_nothrow_default_constructible<Value>::value) = default;

    template <class T>
    wrapped_value(T&& value) noexcept(std::is_nothrow_constructible<Value>::value&& noexcept(
        this->iterate_(this->wrappers_, std::forward<Value>(value))))
        : value_(iterate_(wrappers_, std::forward<Value>(value)))
    {
    }

    operator const Value&() const noexcept { return value_; }

    wrapped_value& operator+=(const Value& value) noexcept(noexcept(this->iterate_(this->wrappers_,
                                                                                   this->value_ + value, this->value_)))
    {
        iterate_(wrappers_, value_ + value, value_);
        return *this;
    }

    wrapped_value& operator++() noexcept(noexcept(this->iterate_(this->wrappers_, this->value_ + 1)))
    {
        value_ = iterate_(wrappers_, value_ + 1);
        return *this;
    }

    wrapped_value& operator--() noexcept(noexcept(this->iterate_(this->wrappers_, this->value_ - 1)))
    {
        value_ = iterate_(wrappers_, value_ - 1);
        return *this;
    }

    wrapped_value& operator-=(const Value& value) noexcept(noexcept(this->iterate_(this->wrappers_,
                                                                                   this->value_ - value)))
    {
        value_ = iterate_(wrappers_, value_ - value);
        return *this;
    }

    bool operator!() const noexcept(noexcept(!this->value_)) { return !value_; }

    friend std::ostream& operator<<(std::ostream& os, const wrapped_value& v) noexcept(noexcept(os << v.value_))
    {
        os << v.value_;
        return os;
    }

   private:
    Value value_;

    using WrappersType = std::tuple<Wrappers...>;
    static WrappersType wrappers_;
    static wrappers_iterator<WrappersType> iterate_;
};

}  // namespace msd

template <typename Value, typename... Wrappers>
std::tuple<Wrappers...> msd::wrapped_value<Value, Wrappers...>::wrappers_ = {};

template <typename Value, typename... Wrappers>
msd::wrappers_iterator<std::tuple<Wrappers...>> msd::wrapped_value<Value, Wrappers...>::iterate_ = {};

// Wrappers
template <typename Value, int Limit>
struct LowerBoundLimiter {
    constexpr Value operator()(const Value& value) const noexcept { return std::max(value, Limit); }
};

template <typename Value, int Limit>
struct UpperBoundLimiter {
    constexpr Value operator()(const Value& value) const noexcept { return std::min(value, Limit); }
};

template <typename Value>
struct Notifier {
    Value operator()(const Value& value) const noexcept
    {
        std::cout << "Value set to: " << value << "\n";
        return value;
    }
};

template <typename Value, int Limit>
struct UpperLimitValidator {
    Value operator()(const Value& value) const
    {
        if (value > Limit) {
            throw std::range_error{"upper limit error"};
        }
        return value;
    }
};

// Struct with wrapped values
struct Input {
    msd::wrapped_value<int, LowerBoundLimiter<int, 1>, UpperBoundLimiter<int, 10>, Notifier<int>> a;
    msd::wrapped_value<int, UpperLimitValidator<int, 1000>, UpperBoundLimiter<int, 10>> b;
};

int inc(int a) { return a + 1; }

int main()
{
    Input in{};

    static_assert(sizeof(in.a) == sizeof(int), "size of wrapped_value greater than type's");

    double a = 1000.0;
    in.a = a;
    assert(in.a == 10);

    in.a = 1;
    in.b = 2;
    assert(in.a == 1);
    assert(in.b == 2);

    std::stringstream str;
    str << in.a;
    assert(str.str() == "1");

    int x = in.a + 1;
    assert(x == 2);

    auto y = 2 + in.a;
    assert(y == 3);

    auto x1 = in.a - 1;
    assert(x1 == 0);

    int y1 = 2 - in.a;
    assert(y1 == 1);

    in.a = 100;
    assert(in.a == 10);
    assert(10 == in.a);
    assert(in.a == 10.0);

    assert(in.a == in.a);
    assert(!(in.a == in.b));
    assert(in.a != in.b);
    assert(in.a <= 10);
    assert(in.b < in.a);

    ++in.a;
    assert(in.a == 10);

    --in.a;
    assert(in.a == 9);

    ++in.a;
    assert(in.a == 10);

    in.a = in.a - in.b;
    assert(in.a == 8);

    in.a -= 5;
    assert(in.a == 3);

    assert(not in.a == false);

    in.b = 1000;
    assert(in.b == 10);

    try {
        in.b = 1001;
        assert(false);
    }
    catch (const std::range_error&) {
    }

    assert(inc(in.a) == 4);
}

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.