[C++] A std::tuple To Concatenate Validation Criteria

Recently I had that issue that I needed to connect several validation criteria. In a prototype (or proof of concept) the naive way is this:

// some defined components which hold the data to our context
struct components {
    // some components/data defined here
};

//... 
// and the validation functions concatenated in this if statement
if (is_valid_criteria_1(components) &&
    is_valid_criteria_2(components) &&
    is_valid_criteria_3(components))
{
    // ... 
}

Worth to mention is that all is_valid.. functions were memberfunctions. Usually I prefer free functions over member functions and in this case the functions didn't modify any of the classes members.

However, it didn't feel right and the fact that with more validation criteria I needed to always adapt this if statement didn't feel right.

So, second thoughts, another naive approach is an interface with a virtual validate function and std::vector. But this isn't so nice either, dynamic polymorphism, an interface, etc., etc... And I know all the validation criteria at compile time, so this doesn't feel good too.

Finally: std::tuple + std::apply

And this brought me a satisfying solution. Let's take a look on this:

namespace cwt {
    // let's put the implementation details into its own namespace
    namespace details
    {
        // here can i define my validation criteria
        struct criteria_1 {
            bool operator()(/*pass optional arguments*/) const {
                return true;
            }
        };
        // second criteria
        struct criteria_2 {
            bool operator()() const {
                return true;
            }
        };
        // and the third criteria
        struct criteria_3
        {
            bool operator()() const {
                return true;
            }
        };
    
        // and now we just use an alias for a tuple with all our types here
        using validations = std::tuple<
            criteria_1, 
            criteria_2, 
            criteria_3
        >;
        
    } // namespace details

    // let's define the final validation class
    class validation 
    {
    public:
        //where we call this function to validate
        [[nodiscard]] bool is_valid() 
        {
            // by default I assume all criteria as true
            bool result = true;
            // a lambda which gets each validation result
            auto check_result = [&result](bool r) {
                if (r == false) {
                    // and sets the overall result to false if one criteria fails
                    result = false;
                }
            };

            // unfortunately this was the only way (which I found)
            // to capture each return value or implement multiple lines to 
            // the expanding parameter pack

            // but now let's use std::apply forward it's arguments and capture each result
            // with the just created lambda expression
            std::apply(
                [&check_result](auto&&... args) {
                    (check_result(args()),...);
                },
                m_validations
            );
            // and finally we return our overall result
            return result;
        }
    private:
        // and we have this as private member where we now 
        // have all implementation details away from the caller
        details::validations m_validations;
    };

}


int main() 
{
    cwt::validation v;
    if (v.is_valid()) {
        // do this
    } else {
        // do that
    }
    
    return 0;
}

Now, if I need to implement further validation criteria in this code, I create another type criteria_4 and add it to my tuple.

It can get a bit ugly when you have different signatures to your single criterias, which (fortunately) was not the case for me. I had a error callback as argument and a components struct where the data is:

struct components {
    // ... 
};

// ...

// let's add a template parameter callback here and possible data
struct criteria_1 {
    template<typename Callback>
    bool operator()(const components& c, Callback&& cb) const {
        // ... 
        // if something goes wrong I'll use my callback
        cb(/*...*/);
    }
};

// ... 

class validation 
{
public:
    // and same here, a templated function for the callback
    template<typename Callback>
    [[nodiscard]] bool is_valid(const components& c, Callback&& cb) 
    {
        // the lambda remains the same
        bool result = true;
        auto check_result = [&result](bool r) {
            if (r == false) {
                result = false;
            }
        };
        // and now I capture the components and the callback in this lambda
        // to provide it to each validation criteria
        std::apply(
            [&check_result, &c, &cb, &event_callback](auto&&... args) {
                (check_result(args(c, cb)),...);
            },
            m_validations
        );
        return result;
    }
private:
    details::validations m_validations;
};

// ...

int main() 
{
    // some components
    cwt::components some_data;
    cwt::validation v;
    // and now we can pass in a error callback
    if (v.is_valid(some_data, [](){
        std::cout << "my error callback\n";
    })) {
        // do this
    } else {
        // do that
    }

    return 0;
}

And the good thing here is, that with the templated error callback I decoupled the dependency to the receiver of the error callback. This allowed me to write unittests and replace the error callback very easy.

Conclusion

You can find this example here on compiler explorer.

I hope that helped, not more to mention here from me and see you on the next one.

Best Thomas

Edit 2024

Well... it turned out for me that this approach was more of a play around and actually way over-engineered. The better and simpler solution is just a couple of sequential if statements.

Previous
Previous

[C++] Breaking Dependencies With Templates

Next
Next

[C++] Rendering An OpenGL Framebuffer Into A Dear ImGui Window