Obviously we can concatenate two string literals in a constexpr
function, but what about concatenation of a string literal with a string returned by another constexpr
function as in the code below?
template <class T>
constexpr const char * get_arithmetic_size()
{
switch (sizeof(T))
{
case 1: return "1";
case 2: return "2";
case 4: return "4";
case 8: return "8";
case 16: return "16";
default: static_assert(dependent_false_v<T>);
}
}
template <class T>
constexpr std::enable_if_t<std::is_arithmetic_v<T>, const char *> make_type_name()
{
const char * prefix = std::is_signed_v<T> ? "int" : "uint";
return prefix; // how to concatenate prefix with get_arithmetic_size<T>() ?
}
static_assert(strings_equal(make_type_name<int>, make_type_name<int32_t>);
The code makes compiler-independent string identifier of an arithmetic type.
EDIT1:
A little bit more complicated example is:
template<typename Test, template<typename...> class Ref>
struct is_specialization : std::false_type {};
template<template<typename...> class Ref, typename... Args>
struct is_specialization<Ref<Args...>, Ref> : std::true_type {};
template <class T>
constexpr std::enable_if_t<is_specialization<T, std::vector>::value || is_specialization<T, std::list>::value, const char *> make_type_name()
{
return "sequence"; // + make_type_name<typename T::value_type>;
}
static_assert(strings_equal(make_type_name<std::vector<int>>(), make_type_name<std::list<int>>()));
str() method to get std::string from it. std::string::c_str() function returns pointer to const char buffer (i.e. const char * ) of string contained within it, that is null-terminated. You can then use it as any other const char * variable. Better way would be to const char *C = (a + b).
If you don't have the choice, using const char* gives a guarantee to the user that you won't change his data especially if it was a string literal where modifying one is undefined behavior. Show activity on this post. By using const you're promising your user that you won't change the string being passed in.
Here is a quick compile time string class:
template<std::size_t N>
struct ct_str
{
char state[N+1] = {0};
constexpr ct_str( char const(&arr)[N+1] )
{
for (std::size_t i = 0; i < N; ++i)
state[i] = arr[i];
}
constexpr char operator[](std::size_t i) const { return state[i]; }
constexpr char& operator[](std::size_t i) { return state[i]; }
constexpr explicit operator char const*() const { return state; }
constexpr char const* data() const { return state; }
constexpr std::size_t size() const { return N; }
constexpr char const* begin() const { return state; }
constexpr char const* end() const { return begin()+size(); }
constexpr ct_str() = default;
constexpr ct_str( ct_str const& ) = default;
constexpr ct_str& operator=( ct_str const& ) = default;
template<std::size_t M>
friend constexpr ct_str<N+M> operator+( ct_str lhs, ct_str<M> rhs )
{
ct_str<N+M> retval;
for (std::size_t i = 0; i < N; ++i)
retval[i] = lhs[i];
for (std::size_t i = 0; i < M; ++i)
retval[N+i] = rhs[i];
return retval;
}
friend constexpr bool operator==( ct_str lhs, ct_str rhs )
{
for (std::size_t i = 0; i < N; ++i)
if (lhs[i] != rhs[i]) return false;
return true;
}
friend constexpr bool operator!=( ct_str lhs, ct_str rhs )
{
for (std::size_t i = 0; i < N; ++i)
if (lhs[i] != rhs[i]) return true;
return false;
}
template<std::size_t M, std::enable_if_t< M!=N, bool > = true>
friend constexpr bool operator!=( ct_str lhs, ct_str<M> rhs ) { return true; }
template<std::size_t M, std::enable_if_t< M!=N, bool > = true>
friend bool operator==( ct_str, ct_str<M> ) { return false; }
};
template<std::size_t N>
ct_str( char const(&)[N] )->ct_str<N-1>;
you can use it like this:
template <class T>
constexpr auto get_arithmetic_size()
{
if constexpr (sizeof(T)==1)
return ct_str{"1"};
if constexpr (sizeof(T)==2)
return ct_str{"2"};
if constexpr (sizeof(T)==4)
return ct_str{"4"};
if constexpr (sizeof(T)==8)
return ct_str{"8"};
if constexpr (sizeof(T)==16)
return ct_str{"16"};
}
template <class T, std::enable_if_t<std::is_arithmetic<T>{}, bool> = true>
constexpr auto make_type_name()
{
if constexpr (std::is_signed<T>{})
return ct_str{"int"} + get_arithmetic_size<T>();
else
return ct_str{"uint"} + get_arithmetic_size<T>();
}
which leads to statements like:
static_assert(make_type_name<int>() == make_type_name<int32_t>());
passing.
Live example.
Now one annoying thing is that the length of the buffer is in the type system. You could add a length
field, and make N
be "buffer size", and modify ct_str
to only copy up to length
and leave the trailing bytes as 0
. Then override common_type
to return the max N
of both sides.
That would permit you do pass ct_str{"uint"}
and ct_str{"int"}
in the same type of value and make the implementation code a bit less annoying.
template<std::size_t N>
struct ct_str
{
char state[N+1] = {0};
template<std::size_t M, std::enable_if_t< (M<=N+1), bool > = true>
constexpr ct_str( char const(&arr)[M] ):
ct_str( arr, std::make_index_sequence<M>{} )
{}
template<std::size_t M, std::enable_if_t< (M<N), bool > = true >
constexpr ct_str( ct_str<M> const& o ):
ct_str( o, std::make_index_sequence<M>{} )
{}
private:
template<std::size_t M, std::size_t...Is>
constexpr ct_str( char const(&arr)[M], std::index_sequence<Is...> ):
state{ arr[Is]... }
{}
template<std::size_t M, std::size_t...Is>
constexpr ct_str( ct_str<M> const& o, std::index_sequence<Is...> ):
state{ o[Is]... }
{}
public:
constexpr char operator[](std::size_t i) const { return state[i]; }
constexpr char& operator[](std::size_t i) { return state[i]; }
constexpr explicit operator char const*() const { return state; }
constexpr char const* data() const { return state; }
constexpr std::size_t size() const {
std::size_t retval = 0;
while(state[retval]) {
++retval;
}
return retval;
}
constexpr char const* begin() const { return state; }
constexpr char const* end() const { return begin()+size(); }
constexpr ct_str() = default;
constexpr ct_str( ct_str const& ) = default;
constexpr ct_str& operator=( ct_str const& ) = default;
template<std::size_t M>
friend constexpr ct_str<N+M> operator+( ct_str lhs, ct_str<M> rhs )
{
ct_str<N+M> retval;
for (std::size_t i = 0; i < lhs.size(); ++i)
retval[i] = lhs[i];
for (std::size_t i = 0; i < rhs.size(); ++i)
retval[lhs.size()+i] = rhs[i];
return retval;
}
template<std::size_t M>
friend constexpr bool operator==( ct_str lhs, ct_str<M> rhs )
{
if (lhs.size() != rhs.size()) return false;
for (std::size_t i = 0; i < lhs.size(); ++i)
if (lhs[i] != rhs[i]) return false;
return true;
}
template<std::size_t M>
friend constexpr bool operator!=( ct_str lhs, ct_str<M> rhs )
{
if (lhs.size() != rhs.size()) return true;
for (std::size_t i = 0; i < lhs.size(); ++i)
if (lhs[i] != rhs[i]) return true;
return false;
}
};
template<std::size_t N>
ct_str( char const(&)[N] )->ct_str<N-1>;
The function implementations now become:
template <class T>
constexpr ct_str<2> get_arithmetic_size()
{
switch (sizeof(T)) {
case 1: return "1";
case 2: return "2";
case 4: return "4";
case 8: return "8";
case 16: return "16";
}
}
template <class T, std::enable_if_t<std::is_arithmetic<T>{}, bool> = true>
constexpr auto make_type_name()
{
constexpr auto base = std::is_signed<T>{}?ct_str{"int"}:ct_str{"uint"};
return base + get_arithmetic_size<T>();
}
which is a lot more natural to write.
Live example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With