Home > Mobile >  Is it possible with C 20 to have a constexpr function return a tuple of types that have static cons
Is it possible with C 20 to have a constexpr function return a tuple of types that have static cons

Time:08-08

After two or three days of trying, I had to give up and wrote a "minimal" test case I hope demonstrates the problem.

What I need is a method to convert string-literals, that are passed as macro arguments without quotes, into strings (catenated with a prefix) that are accessible in a constexpr environment (see https://wandbox.org/permlink/Cr6j6fXemsQRycHI for the Real Code(tm)); that means, they (the macro arguments) should be stringified and then converted into either a type (e.g. template<... 'h', 'e', 'l', 'l', 'o', ...>) or into a static constexpr array<char, N> of a unique type that is passed instead (e.g template<... A<1> ...>, where A<1>::str is a static constexpr array<char, 6> with the contents 'h', 'e', 'l', 'l', 'o', '\0'.

I strongly prefer the latter, and the former only if the latter isn't possible.

To demonstrate the exact problem/requirement in a short test case I came up with the following:

Some headers...

#include <array>
#include <tuple>
#include <cassert>
#include <string>
#include <iostream>

Then for the sake of demonstrating how the end-result should behave:

template<int I>
struct A;

template<>
struct A<0>
{
  static constexpr auto str = std::to_array("abc"); // The string-literal "abc" may NOT appear here.
                                                    // The code should work for any macro argument
                                                    // (though for this test case you may assume all
                                                    // string literals are three chars).
};

template<>
struct A<1>
{
  static constexpr auto str = std::to_array("def"); // Same.
};

constexpr auto f(char const* s0, char const* s1)
{
  return std::tuple<
    A<0>, // The 0, because this is the first argument, makes the type (A<0>) unique, and therefore
          // a static constexpr can be part of that unique type that contains the string s0.
    A<1>  // Same for 1 and A<1>.
  >{};
}

#define STR(arg) #arg
#define STRINGIFY(arg) STR(arg)

#define MEMBER(arg) STRINGIFY(arg)

And finally the rest of the code that hopefully enforces everything I need the above to do:

//=====================================================================
// NOTHING BELOW THIS LINE MAY BE CHANGED.

struct C
{
  static constexpr auto x = f(
    MEMBER(abc),
    MEMBER(def)
  );
};

int main()
{
  // The type returned by f() is a tuple.
  using xt = decltype(C::x);
  
  // Each element of that tuple must be a type...
  using e0 = std::tuple_element_t<0, xt>;
  using e1 = std::tuple_element_t<1, xt>;

  // ... that defines a static constexpr array<> 'str'.
  constexpr std::array a0 = e0::str;
  constexpr std::array a1 = e1::str;
  
  std::string s0{a0.begin(), a0.end()};    // Note that the array str includes a terminating zero. 
  std::string s1{a1.begin(), a1.end()};

  std::cout << "s0 = \"" << s0 << "\"\n";
  std::cout << "s1 = \"" << s1 << "\"\n";

  // ... that has the value that was passed as macro argument.
  assert(s0.compare("abc") && s0[3] == '\0');
  assert(s1.compare("def") && s1[3] == '\0');
}

The second code block needs some obvious fixes:

  1. it contains hard-coded strings for "abc" and "def" which are supposed to come from the macro arguments passed to MEMBER at the top of the third block.
  2. The arguments passed to f() are not even used.

The idea here is that each argument of f() results in an element type of the returned tuple - for the sake of simplicity I made this fixed: just two arguments.

Each element of the tuple is guaranteed to be a unique type, but that uniqueness depends on the fact that it contains an integer template parameter that is incremented for each argument; in the real code the uniqueness is guaranteed further by only invoking f() once per unique (user) class (C above); but since this in this test case f() is only invoked once anyway, I left that out. Literally returning std::tuple<A<0>, A<1>> is therefore in theory the goal.

Since each tuple element is a unique type, in theory they can contain a static constexpr array with the argument that was passed to the MEMBER macro as their content. However, I don't see how this is possible to achieve.

What I really need is "encoded" in the third block: if that works for any identifier string (i.e. no spaces, if that matters) as macro arguments (after also replacing the test strings at the end), then the interface of block two should teach me how to do this.

The complete test case can be found online here: https://wandbox.org/permlink/vyPK9qktAzcdP3wt

EDIT

Thanks to pnda's solution, we now have an answer; I just made some tiny changes so that the third code block can stay as-is. With the following as second code block it compiles and works!

struct TemplateStringLiteral {
  std::array<char, N> chars;
  consteval TemplateStringLiteral(std::array<char, N> literal) : chars(literal) { }
};

template<TemplateStringLiteral literal>
struct B {
  static constexpr auto str = literal.chars;
};

template<TemplateStringLiteral s>
struct Wrap
{
};

template <TemplateStringLiteral s0, TemplateStringLiteral s1>
consteval auto f(Wrap<s0>, Wrap<s1>)
{
  return std::tuple<
    B<s0>,
    B<s1>
  >{};
}

#define STR(arg) #arg
#define STRINGIFY(arg) STR(arg)
#define MEMBER(arg) Wrap<std::to_array(STRINGIFY(arg))>{}

CodePudding user response:

It is possible to store a string literal inside of a template argument. I wrote a class that does exactly that a year or so ago. This allows the tuple creation to be constexpr while also offering a quite nice way of passing the strings. This class can also be used to differentiate between two types using a string without modifying the rest of the templated class, which is very useful if you're using typeless handles.

template <std::size_t N>
struct TemplateStringLiteral {
    char chars[N];

    consteval TemplateStringLiteral(const char (&literal)[N]) {
        // Does anyone know of a replacement of std::copy_n? It's from
        // <algorithm>.
        std::copy_n(literal, N, chars);
    }
};

Using this class, it is possible for us to pass the tuple creation this class with a string literal attached to it. This does, sadly, slightly modify your C::x from using the MEMBER define for the function arguments to using the MEMBER define for the template arguments. It is also perfectly possible to just use normal string literals in the template arguments.

struct C
{
    // Essentially just f<"abc", "def">();
    static constexpr auto x = f<MEMBER(abc), MEMBER(def)>();
};

For the function f we now want to take the parameters as template arguments. So we'll now use the TemplateStringLiteral class to take in the string literals, while also making it a template parameter pack to allow for any amount of parameters.

template <TemplateStringLiteral... literal>
consteval auto f() {
    // I'll talk about A in a second. It just holds our string
    return std::tuple<A<literal>...> {};
}

Now that we've got a function f that can create a std::tuple from some amount of string literals passed in through template parameters, we'll just need to define the class A that the tuple holds. We can't pass the string literals directly to std::tuple as it does not have something akin to TemplateStringLiteral. Defining class A is very simple, as we only need a str field holding our string literal.

template<TemplateStringLiteral literal>
struct A {
    static constexpr auto str = std::to_array(literal.chars);
};

So, using TemplateStringLiteral we've got an implementation that's about 16 lines of C and is pretty easily understandable imo.

  • Related