C++20 是否可以让 constexpr 函数返回具有静态 constexpr 数组且值通过宏传递的类型的元组?

Car*_*ood 5 c++ constexpr c++20 constexpr-function

经过两三天的尝试,我不得不放弃并编写了一个“最小”测试用例,希望能够证明该问题。

我需要的是一种将字符串文字(作为不带引号的宏参数传递)转换为可在 constexpr 环境中访问的字符串(用前缀连接)的方法(请参阅https://wandbox.org/permlink/Cr6j6fXemsQRycHI真实代码( tm));这意味着,它们(宏参数)应该被字符串化,然后转换为类型(例如 template <... 'h', 'e', 'l', 'l', 'o', ...>)或转换为static constexpr array<char, N>传递的唯一类型的 a (例如 template <... A<1> ...>,其中A<1>::strastatic constexpr array<char, 6>包含内容'h', 'e', 'l', 'l', 'o', '\0'.

我强烈喜欢后者,只有当后者不可能时才选择前者。

为了在简短的测试用例中演示确切的问题/要求,我提出了以下内容:

一些标题...

#include <array>
#include <tuple>
#include <cassert>
#include <string>
#include <iostream>
Run Code Online (Sandbox Code Playgroud)

然后为了演示最终结果应该如何表现:

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)
Run Code Online (Sandbox Code Playgroud)

最后,其余的代码有望强制执行我需要执行 上述操作的所有操作:

//=====================================================================
// 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');
}
Run Code Online (Sandbox Code Playgroud)

第二个代码块需要一些明显的修复:

  1. "abc"它包含和 的硬编码字符串,"def"这些字符串应该来自传递到MEMBER第三个块顶部的宏参数。
  2. 传递给的参数f()甚至没有被使用。

这里的想法是,每个参数都会f()产生返回元组的元素类型 - 为了简单起见,我对此进行了修复:只有两个参数。

元组的每个元素都保证是唯一的类型,但这种唯一性取决于它包含一个针对每个参数递增的整数模板参数;f()在真实代码中,通过每个唯一(用户)类仅调用一次(C上面)来进一步保证唯一性;但由于在这个测试用例中f()它只被调用一次,所以我把它省略了。std::tuple<A<0>, A<1>>因此,理论上的目标是从字面上返回。

由于每个元组元素都是唯一的类型,因此理论上它们可以包含 astatic constexpr array以及传递给MEMBER宏作为其内容的参数。但是,我不知道如何实现这一目标。

我真正需要的是在第三个块中“编码”:如果这适用于任何标识符字符串(即没有空格,如果这很重要)作为宏参数(在替换最后的测试字符串之后),那么块二的接口应该教我如何做到这一点。

完整的测试用例可以在这里在线找到: https: //wandbox.org/permlink/vyPK9qktAzcdP3wt

编辑

感谢pnda的解决方案,我们现在有了答案;我只是做了一些微小的更改,以便第三个代码块可以保持原样。使用以下作为第二个代码块,它可以编译并运行!

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))>{}
Run Code Online (Sandbox Code Playgroud)

pnd*_*nda 1

可以在模板参数内存储字符串文字。大约一年前,我写了一个类,它正是这样做的。这允许元组创建是 constexpr,同时还提供了一种非常好的传递字符串的方式。该类还可以用于使用字符串来区分两种类型,而无需修改模板类的其余部分,如果您使用无类型句柄,这将非常有用。

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);
    }
};
Run Code Online (Sandbox Code Playgroud)

使用此类,我们可以将元组创建传递给此类,并附加一个字符串文字。遗憾的是,这确实稍微修改了您C::x从使用MEMBER函数参数的定义到使用MEMBER模板参数的定义。也完全可以在模板参数中使用普通字符串文字。

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

对于函数 f,我们现在希望将参数作为模板实参。因此,我们现在将使用该类TemplateStringLiteral来接收字符串文字,同时使其成为模板参数包以允许任意数量的参数。

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

现在我们已经有了一个f可以std::tuple从通过模板参数传入的一定数量的字符串文字创建 a 的函数,我们只需要定义该元组所保存的类 A 即可。我们无法直接将字符串文字传递给,std::tuple因为它没有类似于TemplateStringLiteral. 定义 A 类非常简单,因为我们只需要一个str保存字符串文字的字段。

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

因此,使用 TemplateStringLiteral 我们得到了一个大约 16 行 C++ 的实现,并且在我看来非常容易理解。