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 (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 guarantee that this container should have a specific properties 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*
?
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;
}