Search code examples
c++templatesc++20metaprogramming

Is it possible to use constexpr if to check if the type is a container or a std::string


For example I have this class:

template <typename T, U>
class TLVParser
{
public:
    TLVParser(T value) : value_(std::move(value)) {}
    void parse(const std::span<uint8_t> &buffer, size_t &offset)
    {
        if constexpr (std::is_arithmetic_v<T>)
        {
            return parsePrimitive(buffer, offset);
        }
        else if constexpr
            constexpr(std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>)
            {
                return parseStringView(buffer, offset);
            }
        else if constexpr (std::is_same_v<T, std::span<U>> || std::is_same_v<T, std::vector<U>>)
        {
            return parseContainer(buffer, offset);
        }
        else
        {
            static_assert(always_false<T>::value, "Unsupported type for TLV parsing");
        }
    }

private:
    T value_;
};

As I understand, string_view, std::string, std::vector and std::span has some common properties that I could make the same parse function. So is there a way to use if constexpr to check if this type is a container like this ones without writing a || for each one, and just using a concept to garantee that this container should have a specific propertie like capable of range loop?

My second question is can I make a typename for the std::span<uint8_t> parameter so I can use a std::vector, std::array or even uint8_t* or char* ?


Solution

  • As a couple of people already pointed out in the comments, determining what constitutes a "container" is often the issue. One way is checking if it supports "std::begin()" and "std::end()", which also handles C-style arrays, opposed to just checking for classes that have members "begin()" and "end()" (which would ignore C-style arrays).

    Here's another technique however which I prefer for reasons I won't get into here (for C++17 and later though it can be slightly tweaked to handle earlier versions). It simply checks if it's a class that has an "iterator" or "const_iterator" alias (typedef), in addition to a C-style array. The use of the macro is ugly but it's just to create the type safe code that does the work of checking for "iterator" or "const_iterator" (and the macro can be used to create type safe "HasMemberType_?" classes to detect any other type alias as well if required, not just "iterator" or "const_iterator"). Concepts are also now available of course (in C++20) so you can also create a concept that just defers to "IsContainer_v" if you wish (see this below). Some of the "std" concept classes here can also likely be leveraged for those that want to implement something more modern (but I won't start tackling that here).

    Anyway, the following code wouldn't qualify for the standard itself but works very well in practice (and yes, it handles "std::string" if required since it has the necessary iterator aliases).

    Click here to run it.

    #include <type_traits>
    #include <vector>
    #include <iostream>
    
    #define DECLARE_HAS_MEMBER_TYPE(NAME) \
        template <typename, typename = void> \
        struct HasMemberType_##NAME : std::false_type \
        { \
        }; \
        template <typename T> \
        struct HasMemberType_##NAME<T, std::void_t<typename T::NAME>> : std::true_type \
        { \
        }; \
        template <typename T> \
        inline constexpr bool HasMemberType_##NAME##_v = HasMemberType_##NAME<T>::value;
    
    DECLARE_HAS_MEMBER_TYPE(iterator)
    DECLARE_HAS_MEMBER_TYPE(const_iterator)
    
    template <typename T>
    using IsContainer = std::bool_constant<HasMemberType_const_iterator_v<T> ||
                                           HasMemberType_iterator_v<T> ||
                                           std::is_array_v<T>>;
    
    template <typename T>
    inline constexpr bool IsContainer_v = IsContainer<T>::value;                                       
    
    int main()
    {
        std::cout << std::boolalpha << IsContainer_v<std::vector<int>> << "\n"; // true
        std::cout << std::boolalpha << IsContainer_v<int []> << "\n"; // true (C-style array)
        std::cout << std::boolalpha << IsContainer_v<int *> << "\n"; // false (pointer not considered a container)
        return 0;
    }