1
\$\begingroup\$

I am creating a template function with variardic arguments, to handle a specific classes that have some interface, method, member or whatever is specialized in a specialization area. However I came to a solution to handle even types that are not supported, thus avoid exceptions, polymorphism, virtual functions, RTTI, etc. I'd like to hear a suggestions and also a peak to the implementation. The example below shows a simple parsing of known network protocols (pseudo logic), that can handle all relevant types, if not specialized classes are passed, they shall be omitted as it happens.

#include <iostream>
#include <cstring>

template<typename T>
struct is_validator
{
    static const bool value = false;
};


struct Null {

    Null(const std::string& res[[maybe_unused]]) {}

    bool valid() const {
        return  false;
    }
};


struct RtpRFC
{

    std::string m_res;

    RtpRFC(const std::string& res) : m_res{res}{}

    bool valid() const {
        if (!strcmp(m_res.c_str(), "rtp"))
            return true;
        return  false;
    }

};


struct RtspRFC
{
public:

    std::string m_res;

    RtspRFC(const std::string& res) : m_res{res}{}

    bool valid() const {
        if (!strcmp(m_res.c_str(),"rtsp"))
            return  true;
        return false;
    }

};


struct StunRFC
{
    std::string m_res;

    StunRFC(const std::string& res) : m_res{res}{}

    bool valid() const {
        if (!strcmp("stun", m_res.c_str()))
            return  true;
        return false;
    }

};


struct NonValid
{
};



template<>
struct is_validator<Null>
{
    static const bool value  = true;
};
template<>
struct is_validator<RtpRFC>
{
    static const bool value  = true;
};
template<>
struct is_validator<StunRFC>
{
    static const bool value  = true;
};
template<>
struct is_validator<RtspRFC>
{
    static const bool value  = true;
};

/*terminator*/
bool  VParse(...) {
    return false;
}

template <class T,
         //typename std::enable_if<is_validator<T>::value>::type,
         typename...ARgs>
bool VParse(T type, ARgs&&... FArgs)
{
    if constexpr (is_validator<T>::value) {
    if (type.valid()) {
         return true;
    }
        else {
            return VParse(std::forward<ARgs>(FArgs)...);
        }
    }
    return VParse(std::forward<ARgs>(FArgs)...);

}


int main(void)
{
    std::string ret{};
    const std::string someNetworkData[10] = {
        "stun", "rtp", "stun", "stun", "rtsp", "rtp", "http", "http2", "udp"
    };

    for(int i=0; i < 10; i++) {
        auto res = someNetworkData[i];
        bool valid = VParse(
                10,
                "test",
                NonValid{},
                RtpRFC{res} ,
                RtspRFC{res},
                StunRFC{res},
                Null{res}, //dummies
                Null{res},
                Null{res}
            );
        if  (valid) {
            ret += res;
            ret += "|";
        }
    }

    std::cout << ret;
    return 0;
}
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

Avoid C string functions if possible

There is no need to use strcmp(), especially not if you are working with std::strings to begin with. For example, you can just write:

bool valid() const {
    return m_res == "rtp";
}

Also make sure then to #include <string> instead of #include <cstring>.

Simplifying the code

Since your code needs C++17 anyway, you can simplify VParse() significantly by using fold expressions and a helper function, like so:

template <typename T>
bool is_valid(T&& type)
{
    if constexpr(is_validator<T>::value) {
        return type.valid();
    }
    return false;
}

template <typename... Args>
bool VParse(Args&&... args)
{
    return (is_valid(std::forward<Args>(args)) || ...);
}

This splits the code into one self-contained part that checks a single argument, and another part that just iterates over all arguments.

Should you allow types that are not validators?

I think it's risky to have your function check whether is_validator<T>::value is a valid expression, and ignoring it if not. It's quite easy to make a typo somewhere and turn a valid validator into something that's not, and your code will then explicitly allow that, instead of letting the compiler catch the error. So I would recommend using SFINAE (like the one you commented out), or concepts if you can use C++20, to limit the accepted types, unless you really have a situation where it would be better to allow arbitrary types and ignore those that you can't use.

Alternatives

I assume the above code is just a toy example, but consider what happens if the list of validators grows a lot more than you have in your example. VParse() will have to call each validator in turn before one returns true. This can be expensive. If it is just matching strings, then you could just use a std::unordered_set<std::string> of valid types and check if a given protocol name is in that set.

\$\endgroup\$
5
  • \$\begingroup\$ Thanks for the response. No, the full code really resolves pcap headers and does some complex logic. However I expect to have a zero runtime overhead. I tried out SFINAE but it didn't helped much, maybe my usage was not correct. Concepts are good advice, maybe will try out, in any case I will need to add more and more to VParse. So shall I be concerned if I add like 30+ protocols, besides the slow compilation time? Do you suspect that I will crash the compiler? \$\endgroup\$ Commented Apr 25, 2022 at 16:42
  • \$\begingroup\$ Compilation time is not the issue you should be worried about. 30 arguments to a variadic template function should also not be an issue. However, the compiler will generate code to check each of those arguments in order (except those that are elided because there is no valid is_validator<T>::value for their type). It's not magically going to have zero runtime overhead. \$\endgroup\$
    – G. Sliepen
    Commented Apr 25, 2022 at 17:08
  • \$\begingroup\$ Good to know. The other way is to have some base class with pure or virtual valid() function, but I have to pass an array of base classes and I will go to the virtual tables, which I can't decide if it's better. Maybe I will just view it in godbolt. But indeed there has to be a check for a valid parser, because every network packet can be either stun either rtp either something else, so I am still planning how to approach as simple as possible. \$\endgroup\$ Commented Apr 25, 2022 at 17:13
  • \$\begingroup\$ Using base classes will be even slower, yes. My point is that instead of brute force checking against every validator, there might be a way to decide which protocol you are dealing with in a faster way. For example, if every protocol had a magic 32-bit number at the very start, then you could just do a lookup in a std::unordered_map. Another option would be to remember the type of the previous packet, and first check the next packet against that type's validator, if it's likely that you receive multiple packets from the same stream in a row. \$\endgroup\$
    – G. Sliepen
    Commented Apr 25, 2022 at 18:45
  • \$\begingroup\$ Yes, thanks for the template improvement. This is just a prototype, the resolution is algorithmic problem, so I will find some versatile way depending on the frequency of the patterns. Thanks again for the suggestions. \$\endgroup\$ Commented Apr 25, 2022 at 18:48
2
\$\begingroup\$

GSliepen posted some good stuff, so I'll try to omit duplicates in my own answer.

Formatting. The compiler doesn't care, but the human reader cares. You wrote:

template <class T,
         //typename std::enable_if<is_validator<T>::value>::type,
         typename...ARgs>
bool VParse(T type, ARgs&&... FArgs)
{
    if constexpr (is_validator<T>::value) {
    if (type.valid()) {
         return true;
    }
        else {
            return VParse(std::forward<ARgs>(FArgs)...);
        }
    }
    return VParse(std::forward<ARgs>(FArgs)...);

}

Notice the misindented if; the random blank line after return; the arbitrary mixture of class and typename keywords; the miscapitalized ARgs; the mixture of cases in variable names (type, FArgs). Also the number of lines wasted by { braces; but that's more of a religious issue, I admit. Anyway, I would have written:

template <class T, typename... Args>
bool VParse(T type, Args&&... args) {
    if constexpr (is_validator<T>::value) {
        return type.valid() || VParse(std::forward<Args>(args)...);
    }
    return VParse(std::forward<Args>(args)...);
}

GSliepen's simplification using a helper is_valid and a fold-expression is indeed the best answer.


template<>
struct is_validator<Null> {
    static const bool value  = true;
};
template<>
struct is_validator<RtpRFC> {
    static const bool value  = true;
};

(1) Use constexpr for compile-time constants, not plain const. (const is useful for pass-by-const-reference and stuff, where you're saying "I promise not to modify this, but somebody else might be allowed to, I don't care.")

(2) C++11 has true_type and false_type for this. Write:

template<class> struct is_validator : std::false_type {};
template<> struct is_validator<Null> : std::true_type {};
template<> struct is_validator<RtpRFC> : std::true_type {};

(3) It seems to me that your criterion for "Is it a validator?" is really no more or less than "Does it have a .valid() method?" You can implement a trait that checks for that exact requirement, using a C++11 partial specialization:

template<class, class = void> struct is_validator : std::false_type {};

template<class T>
struct is_validator<T, decltype(
    std::declval<const T&>().is_valid(), void()
)> : std::true_type {};

or using a C++20 requires-expression:

template<class T>
concept validator = requires (const T& t) {
    t.is_valid();
};

// optionally, keep your familiar type trait, like this
template<class> struct is_validator : std::false_type {};
template<validator T> struct is_validator<T> : std::true_type {};

The efficiency issue that stands out to me isn't that your recursive (or fold-expression) implementation loops linearly over all possible validator types. The issue for me is that you spent all that time to construct all N validators in the first place, when only one of them will ever be used!

    bool valid = VParse(
            10,
            "test",
            NonValid{},
            RtpRFC{res} ,
            RtspRFC{res},
            StunRFC{res},
            Null{res}, //dummies
            Null{res},
            Null{res}
        );

Maybe this is just a toy example. But notice how many different std::string copies you're making of res. I think it would make more sense to pass in just the names of the types, like

template<class... Validators>
bool canParseAsOneOf(const std::string& res) {
    return (
        Validators(res).valid() || ...
    );
}

bool valid = canParseAsOneOf<RtpRFC, RtspRFC, StunRFC>(res);

Now you don't spend time constructing a StunRFC object until you've already determined that RtpRFC and RtspRFC don't work.

If you do this, of course, you're expanding the list of affordances required by your validator types. A validator now must also be constructible from a const string&. So we should update our "validator" concept:

template<class T>
struct is_validator<T, decltype(
    T(std::declval<const std::string&>()),
    std::declval<const T&>().is_valid(),
    void()
)> : std::true_type {};

or in C++20

template<class T>
concept validator = requires (const T& t, const std::string& s) {
    T(res);
    t.is_valid();
};
\$\endgroup\$
1
  • \$\begingroup\$ Very good suggestions, will refactor to avoid copies. Also thank you for the prompt review. \$\endgroup\$ Commented Apr 25, 2022 at 21:04

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.