3

I'm writing an open source sqlite interface library (mSqliteCpp for instance) that uses c++ classes and types to correctly manage sqlite databases.

So far the library is heavily using TMP to build SQL strings, but I found a possible issue that may somehow affect the efficiency of the library.

I'm using classes to manage Fields definitions, classes definitions, queries and statements. Basically to define a table or a SELECT statement, one defines the fields using proper FieldDef<T> objects, then pass them to a statement builder that returns the correctly formatted SQL statement.

For example:

auto fldId = sqlite::makeFieldDef("id", sqlite::FieldType::Integer());
auto fldName = sqlite::makeFieldDef("name", sqlite::FieldType::Text());
auto fldValue = sqlite::makeFieldDef("value", sqlite::FieldType::Real());

sqlite::statements::Select select("Table", fldId, fldName, fldValue);

ASSERT_EQ(select.string(), "SELECT id,name,value FROM Table;");

Note that each field is strongly typed by the FieldType type passed to the makeFieldDef function, but different fields with similar type can be exchanged. For this reason the SQL statement in the ASSERT call is built at runtime.

I would like to have them built at compile time, at least under certain conditions. For example, developer could use a different class or maybe the constexpr keyword. But at this time I've no idea if this could be achieved.

What are the possible patterns? Can strings be statically built through TMP? I'm using C++11/14.

Thank you.

HappyCactus
  • 1,935
  • 16
  • 24
  • 3
    If the string is a `std::string`, its constructor is potentially using dynamic memory which rules out it being `constexpr`. On the other hand, the cost of building a string would be minor compared to parsing and executing the SQL statement. – Bo Persson Dec 29 '17 at 12:52
  • Maybe [this](https://stackoverflow.com/a/36273815/4324224) is what you are looking for... – W.F. Dec 29 '17 at 13:53
  • If you just want the table's fieldnames in string literals, I have code which uses simple namespaces to achieve that with some elegance and type safety. If you are looking for metaprogramming in particular, I can't help you. – Kenny Ostrom Dec 29 '17 at 15:37
  • @BoPersson Thanks, you're indeed right. But it might be interesting to see any viable solution for other similar issue too. – HappyCactus Dec 29 '17 at 15:39
  • @KennyOstrom Thanks, what I'm looking for is a solution that will make most of the computation with strings at compile time. I'd like to have it implemented with modern C++ constructs, I'm not stuck with TMP but I can't figure out other ways to do it. If you can share your code, I'd be glad anyway to have a look if possible. Thanks! – HappyCactus Dec 29 '17 at 15:44
  • Mine is a simple use for readability and to define the names once. (It's not as strongly typed as the link you gave.) Rereading the question, those don't appear to be your concerns. Although, I would encourage you to measure, if you think generating the sql statements is slowing you down. – Kenny Ostrom Dec 29 '17 at 16:30
  • @KennyOstrom Thank you very much for your precious hint. I agree with you, I'd need to measure the overhead and I think it's at the moment far beyond the scope of the library. At the moment I'm just courious about how this could be implemented. Thank you again. – HappyCactus Dec 29 '17 at 16:43

2 Answers2

5

what I'm looking for is a solution that will make most of the computation with strings at compile time [...] At the moment I'm just courious about how this could be implemented.

Not an answer to your sqlite question but... if your "computation with strings at compile time" are simple as concatenation... just to play with constexpr and template metaprogramming...

std::string isn't constexpr but std::array can be.

So you can define a fake string as an alias for an array of chars.

template <std::size_t N>
using fakeStr = std::array<char, N>;

You can convert a string literal to a fake string as follows (with an helper function and playing with type traits)

template <std::size_t N, std::size_t ... Is>
constexpr fakeStr<sizeof...(Is)> mFSh (char const (& str)[N],
                                       std::index_sequence<Is...> const &)
 { return { { str[Is]... } }; }

template <std::size_t N>
constexpr auto makeFakeStr (char const (& str)[N])
 { return mFSh(str, std::make_index_sequence<N-1>{}); }

The fake string concatenation is simple as follows

template <std::size_t N1, std::size_t ... I1s,
          std::size_t N2, std::size_t ... I2s>
constexpr fakeStr<N1+N2> cFSh (fakeStr<N1> const & s1,
                               std::index_sequence<I1s...> const &,
                               fakeStr<N2> const & s2,
                               std::index_sequence<I2s...> const &)
 { return { { s1[I1s]..., s2[I2s]... } }; }


template <std::size_t N1, std::size_t N2>
constexpr auto concatFakeStr (fakeStr<N1> const & s1, fakeStr<N2> const & s2)
 { return cFSh(s1, std::make_index_sequence<N1>{},
               s2, std::make_index_sequence<N2>{}); }

and a constexpr comparison can be done as follows (with C++17 the helper function can be simpler)

template <std::size_t N1, std::size_t N2, std::size_t ... Is>
constexpr bool cmpFSh (fakeStr<N1> const & s1,
                       fakeStr<N2> const & s2,
                       std::index_sequence<Is...> const &)
 { 
   using unused = bool[];

   bool ret { true };

   (void) unused { true, ret &= s1[Is] == s2[Is] ... };

   return ret;
 }

template <std::size_t N1, std::size_t N2>
constexpr bool compareFakeStr (fakeStr<N1> const & s1, fakeStr<N2> const & s2)
 { return (N1 == N2) && cmpFSh(s1, s2, std::make_index_sequence<N1>{}); }

If you want a std::string from a fakeStr, the following code (not constexpr, obviously) can help

template <std::size_t N, std::size_t ... Is>
std::string fFSh (fakeStr<N> const & s, std::index_sequence<Is...> const &)
 { return { s[Is]..., '\0' }; }

template <std::size_t N>
auto fromFakeString (fakeStr<N> const & s)
 { return fFSh(s, std::make_index_sequence<N>{}); }

The following is a full (C++14) working example

#include <array>
#include <string>
#include <iostream>
#include <type_traits>

template <std::size_t N>
using fakeStr = std::array<char, N>;

template <std::size_t N, std::size_t ... Is>
constexpr fakeStr<sizeof...(Is)> mFSh (char const (& str)[N],
                                       std::index_sequence<Is...> const &)
 { return { { str[Is]... } }; }

template <std::size_t N>
constexpr auto makeFakeStr (char const (& str)[N])
 { return mFSh(str, std::make_index_sequence<N-1>{}); }


template <std::size_t N1, std::size_t ... I1s,
          std::size_t N2, std::size_t ... I2s>
constexpr fakeStr<N1+N2> cFSh (fakeStr<N1> const & s1,
                               std::index_sequence<I1s...> const &,
                               fakeStr<N2> const & s2,
                               std::index_sequence<I2s...> const &)
 { return { { s1[I1s]..., s2[I2s]... } }; }


template <std::size_t N1, std::size_t N2>
constexpr auto concatFakeStr (fakeStr<N1> const & s1, fakeStr<N2> const & s2)
 { return cFSh(s1, std::make_index_sequence<N1>{},
               s2, std::make_index_sequence<N2>{}); }

template <std::size_t N1, std::size_t N2, std::size_t ... Is>
constexpr bool cmpFSh (fakeStr<N1> const & s1,
                       fakeStr<N2> const & s2,
                       std::index_sequence<Is...> const &)
 { 
   using unused = bool[];

   bool ret { true };

   (void) unused { true, ret &= s1[Is] == s2[Is] ... };

   return ret;
 }

template <std::size_t N1, std::size_t N2>
constexpr bool compareFakeStr (fakeStr<N1> const & s1, fakeStr<N2> const & s2)
 { return (N1 == N2) && cmpFSh(s1, s2, std::make_index_sequence<N1>{}); }


template <std::size_t N, std::size_t ... Is>
std::string fFSh (fakeStr<N> const & s, std::index_sequence<Is...> const &)
 { return { s[Is]..., '\0' }; }

template <std::size_t N>
auto fromFakeString (fakeStr<N> const & s)
 { return fFSh(s, std::make_index_sequence<N>{}); }


int main()
 {
   constexpr auto f1  = makeFakeStr("abcd");
   constexpr auto f2  = makeFakeStr("xyz");
   constexpr auto f12 = concatFakeStr(f1, f2);
   constexpr auto f3  = makeFakeStr("abcdxyz");

   static_assert( true == compareFakeStr(f12, f3), "!" );
   static_assert( false == compareFakeStr(f12, f1), "!" );

   auto s12 = fromFakeString(f12);

   std::cout << s12 << std::endl;
 }

I repeat: just to play.

max66
  • 65,235
  • 10
  • 71
  • 111
  • Starting with C++20, [std::string](https://en.cppreference.com/w/cpp/string/basic_string) is part of the *`constexpr` All The Things* family. – IInspectable Mar 14 '20 at 08:55
2

@max66's solution is probably better, but there is a proof of concept of a different approach: to store strings as char parameter packs:

template <char ...C> struct cexpr_str
{
    static constexpr char value[] {C..., '\0'};
};

Here is a complete example, demonstrating how to create such strings from string literals, concatenate, and compare them:

#include <cstddef>
#include <iostream>
#include <type_traits>
#include <utility>

template <char ...C> struct cexpr_str
{
    static constexpr char value[] {C..., '\0'};
};

namespace impl
{
    using std::size_t;

    template <typename ...P> struct cexpr_cat
    {
        using type = cexpr_str<>;
    };
    template <typename A, typename B, typename ...P> struct cexpr_cat<A, B, P...>
    {
        using type = typename cexpr_cat<typename cexpr_cat<A, B>::type, P...>::type;
    };
    template <char ...A, char ...B> struct cexpr_cat<cexpr_str<A...>, cexpr_str<B...>>
    {
        using type = cexpr_str<A..., B...>;
    };
    template <typename T> struct cexpr_cat<T>
    {
        using type = T;
    };

    template <typename, typename> struct cexpr_str_subseq {};
    template <size_t ...S, char ...C> struct cexpr_str_subseq<std::index_sequence<S...>, cexpr_str<C...>>
    {
        using type = cexpr_str<cexpr_str<C...>::value[S]...>;
    };

    template <size_t N> constexpr char cexpr_str_at(const char (&str)[N], size_t pos)
    {
        if (pos < 0 || pos >= N)
            return '\0';
        else
            return str[pos];
    }
}
template <typename ...P> using cexpr_cat = typename impl::cexpr_cat<P...>::type;

#define MAKE_CEXPR_STR(x) ::impl::cexpr_str_subseq<::std::make_index_sequence<sizeof x - 1>,\
                          ::cexpr_str<CEXPR_STR_16(0,x),CEXPR_STR_16(16,x),CEXPR_STR_16(32,x),CEXPR_STR_16(48,x)>>::type
#define CEXPR_STR_16(s,x) CEXPR_STR_4(s,x),CEXPR_STR_4(s+4,x),CEXPR_STR_4(s+8,x),CEXPR_STR_4(s+12,x)
#define CEXPR_STR_4(s,x)  ::impl::cexpr_str_at(x,s),::impl::cexpr_str_at(x,s+1),::impl::cexpr_str_at(x,s+2),::impl::cexpr_str_at(x,s+3)

int main()
{
    // You can use this macro to create a string.
    // Note that it limits the length to 64, but can be easily rewritten to allow larger values.
    using A = MAKE_CEXPR_STR("Hello,");

    // If you don't want to use that macro, you can do this.
    using B = cexpr_str<'w','o','r','l','d'>;

    // You can concat any number of comma-separated strings, not just two.
    using C = cexpr_cat<A,B>;

    // This prints: `Hello,`+`world`=`Hello,world`.
    std::cout << "`" << A::value << "`+`" << B::value << "`=`" << C::value << "`\n";

    // You can use std::is_same[_v] to compare those strings:
    std::cout << std::is_same_v<B,A> << '\n'; // Prints 0.
    std::cout << std::is_same_v<B,MAKE_CEXPR_STR("world")> << '\n'; // Prints 1.
}
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
  • 2
    string comparison through `std::is_same`; nice. – max66 Dec 29 '17 at 21:49
  • I prefer your method since it offers compile-time string comparison. The obvious downside is of course having to use macro for string construction. Isn't there a way to build your char pack struct using the expansion approach in @max66 's solution? – xiay Apr 06 '19 at 01:57
  • hmmm, I could not get your solution to compile in clang 7.0.1 with `-std=c++17` – xiay Apr 06 '19 at 08:10
  • @logicor It's broken in Clang 8.0.0 too, looks like a Clang bug to me. Explicitly specify array size for `value` in `struct cexpr_str` as `[sizeof...(C)+1]` to make it work. – HolyBlackCat Apr 06 '19 at 08:27
  • *"Isn't there a way to build your char pack struct using the expansion approach"* ¯\\_(ツ)_/¯ If you figure it out, I'll edit it into the answer. – HolyBlackCat Apr 06 '19 at 08:29