Skip to content

Instantly share code, notes, and snippets.

@ericniebler
Created December 10, 2016 04:27
Show Gist options
  • Save ericniebler/a63c8729f33d7a5e70104dc39af0209a to your computer and use it in GitHub Desktop.
Save ericniebler/a63c8729f33d7a5e70104dc39af0209a to your computer and use it in GitHub Desktop.
An almost perfect design for a hybrid constexpr/runtime string
// Copyright Eric Niebler 2016
#include <cstddef>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <utility>
#include <type_traits>
#define REQUIRES(X) \
typename std::enable_if<(X), int>::type = 0
using std::size_t;
constexpr char const* find_null(char const *z) {
return *z ? find_null(z+1) : z;
}
constexpr size_t constexpr_strlen(char const *z) {
return find_null(z) - z;
}
template <class T>
constexpr T constexpr_min(T a, T b) {
return b < a ? b : a;
}
template <class T>
constexpr T constexpr_max(T a, T b) {
return b < a ? a : b;
}
template <size_t N>
constexpr std::make_index_sequence<N>* indices() {
return nullptr;
}
enum class which_rep : char { small, big };
template <size_t N>
class cstring {
struct bigdata {
char* c_str_;
std::uint32_t size_;
char dummy_;
};
static constexpr size_t M =
constexpr_max(N + 1, sizeof(bigdata));
using smalldata = char[M];
union {
smalldata smalldata_;
bigdata bigdata_;
};
template <class T>
static constexpr char get_at_(T const &t, size_t i, size_t size) {
return i < size ? t[i] : i == M-1 ? (char)((unsigned char)(M-1 - size)) : '\0';
}
constexpr which_rep which_() const noexcept {
return static_cast<unsigned char>(smalldata_[M-1]) ==
static_cast<unsigned char>((char)-1)
? which_rep::big
: which_rep::small;
}
template <class T, size_t... Is>
constexpr cstring(T const &t, size_t size, std::index_sequence<Is...>*) noexcept
: smalldata_{get_at_(t, Is, size)...} {}
static constexpr char s_empty[1]{};
public:
constexpr cstring() noexcept : cstring{s_empty, 0, indices<M>()} {}
template <size_t X, REQUIRES(X <= M && M < 255)>
constexpr cstring(char const (&that)[X]) noexcept
: cstring{that, X-1, indices<M>()} {
}
template <size_t X, REQUIRES(X > M || M >= 255)>
cstring(char const (&that)[X]) noexcept(false)
{
smalldata_[M-1] = (char)-1;
bigdata_.size_ = X-1;
bigdata_.c_str_ = strdup(that);
}
// Gah! this makes the type non-literal; can't constexpr construct!
// ~cstring() noexcept {}
constexpr char const * c_str() const noexcept {
return which_() == which_rep::small ? smalldata_ : bigdata_.c_str_;
}
constexpr std::size_t size() const noexcept {
return which_() == which_rep::small
? static_cast<size_t>(M-1 - static_cast<unsigned char>(smalldata_[M-1]))
: bigdata_.size_;
}
constexpr char const& operator[](std::size_t i) const noexcept {
return which_() == which_rep::small
? smalldata_[i]
: bigdata_.c_str_[i];
}
constexpr bool is_small() const {
return which_() == which_rep::small;
}
};
int main() {
static constexpr cstring<23> s{"hello world hello world"};
constexpr char const& c = s[3];
static_assert(s.size() == 23u, "");
static_assert(sizeof(s) == 24u, "");
static_assert(s[s.size()] == '\0', "");
static_assert(s.is_small(), "");
std::printf("\"%s\"\n", s.c_str());
static constexpr cstring<0> s2;
constexpr char const& c2 = s2[0];
static_assert(s2.size() == 0u, "");
static_assert(sizeof(s2) == 16u, "");
static_assert(s2[s2.size()] == '\0', "");
static_assert(s2.is_small(), "");
std::printf("\"%s\"\n", s2.c_str());
cstring<5> b{"hello world!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"};
std::printf("\"%s\"\n", b.c_str());
if(!b.is_small()) {
std::printf("Is big! Size = %d\n", (int)b.size());
}
}
@ericniebler
Copy link
Author

The idea is to make a string that can be constexpr if the string fits in the internal buffer but to let it grow dynamically so long as it's not constexpr. The good news: it actually works! The bad news: I can't declare a destructor to clean up the dynamic memory in the non-constexpr case. If I do, then the type is no longer literal and it can never be constexpr. :-(

Is there a way to do what I want to do?

@JavierJF
Copy link

I have something, but I think, it isn't what your were looking for. The problem is that, at some point you need the argument X to take part in the instantiation process. But that is something you only can get when you apply the constructor, so it can't take part in the "inheritance decisions".

The most elegant thing I can think of, is a constexpr templated helper function, that decides between two different types "cstring". The different types could simply be this "cstring" with a second template parameter, being this an a "enum class" type. Something like this. Sorry if it isn't very polished, and I hope that could help.

PD: I have left the copyright mark in the top of the document, sorry for editing it, if you wish I can delete the file after you have see it.

@ericniebler
Copy link
Author

The most elegant thing I can think of, is a constexpr templated helper function

Yes, and if that helper function is a so-called deduction guide (coming in C++17), I can get pretty close to what I'm after, I think.

@JavierJF
Copy link

JavierJF commented Dec 14, 2016

Yes, and if that helper function is a so-called deduction guide (coming in C++17)

Oh! I didn't read about that yet! It's a really interesting feature, and I also agree with that pushes this really close to what you wanted. At least it will be nice and clean syntactically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment