在C++中存储非空终止的C字符串常量

7
在有人说“这么做很糟糕”的之前,我需要说明:
  1. 我明白为什么需要使用一个以NUL结尾的字符串。
  2. 我知道可以像这样声明:
    char mystr[] = { 'm', 'y', ' ', 's', 't', 'r', 'i', 'n', 'g'};
    不过,C风格字符串表示法的方便性太大了。

我的理由是我正在为微控制器编程,需要将数据存储到程序内存中。这些数据包括字节、字、双字和浮点数等形式。我希望数据包含不连续的字符串而没有NUL。

我尝试过使用参数为<size_t N, char* A><size_t N, char (&A)[N]>的模板来遍历数组并将其内容存储到静态数组中,但我好像搞不定。我想标准实际上可能会禁止这样做,在一般情况下这是可以理解的,但在特定情况下(具体来说,就是这种情况)很不幸。

如果我能将字符串重新映射为诸如boost::mpl::vector_c<char, ...>之类的东西,那就更好了,因为我有其他代码可以正确地存储它,但从模板内部解引用数组以用作常量模板参数似乎也被禁止了。

您有什么想法吗?

编辑:

伪代码示例(这有点牵强附会,因为真正的代码要大得多,而且我可能不会像这样一个字节一个字节地读取,也不会使用一个字面值来迭代到字符串的末尾。那将在数据中的某个地方嵌入。):

// this stores bytes in an array
template<typename X, typename T, T ...numbers>
struct x
{
  static PROGMEM volatile const T data[];
};
template<typename X, typename T, T ...numbers>
PROGMEM volatile const T x<X, T, numbers...>::data[] = { numbers... };

void main()
{
  // this will not work, but the idea is you have byte 0 as 1, 
  // byte 1 as 2 byte 2 as 3 byte 3 as 's', byte 4 as 'o'...
  // byte 22 as 'g', byte 23 as 4, byte 24 as 5, byte 25 as 6.
  typedef x<int, char, 1,2,3,"some embedded string",4,5,6> xx;
  for(i=0; i<20; ++i)
    Serial.print(pgm_read_byte_near(&xx::data[0] + 3));
}

同时请注意,我没有使用C++11,而是使用了C++0x,可能还用到了一些扩展。


1
听起来是一个有趣的问题,但是问题不是很清楚。你能提供一些伪代码的例子吗? - Angew is no longer proud of SO
11
不要这样做,因为这样的行为非常糟糕。 - user155407
2
你的微控制器内存不足吗?你对尾随的 '\0' 字节有什么反对意见吗?它占用的内存与你的前导长度字节相同。 - brian beuning
1
@adrian,请详细说明“更紧密地控制数据布局”的含义。我认为这是缺失的部分。 - brian beuning
1
@DavidC.Rankin,对不起,你的意思是什么?C++不是C。以前它是一个子集,但现在两者在很多方面已经分道扬镳了。 - Adrian
显示剩余24条评论
2个回答

3

第三次尝试

魔法和诡计

如果您正在使用C++11(我知道,但在其不存在时,我认为代码生成是您最好的选择),那么用户定义的字面值似乎应该能够处理这个问题。例如,使用:

template <char... RAW>
inline constexpr std::array<char, sizeof...(RAW)> operator "" _fixed() {
    return std::array<char, sizeof...(RAW)>{RAW...};
}

希望这个能正常工作:
const std::array<char, 7> goodbye = goodbye_fixed;

...但是遗憾的是这样不行(字面量需要是数字,可能是为了解析原因)。使用"goodbye"_fixed也行不通,因为它需要一个operator "" _fixed(const char *s, int length)重载,而编译时数组又一次衰减为指针。

最终我们需要调用以下内容:

const auto goodbye = operator "" _FS <'g','o','o','d','b','y','e'>();

第二次尝试

自动生成丑陋的代码

我认为你是对的,你不能轻易地拦截字符串字面量机制。 老实说,通常的方法是使用构建工具在单独的文件中为您生成丑陋的代码(例如国际化库)。

例如,您可以输入

fixed_string hello = "hello";

或在专门的文件中使用类似的东西,构建系统会生成一个头文件。
const std::array<char, 5> hello;

还有一个使用了丑陋初始化的cpp位于上方代码块下面。


第一次尝试

遗漏了“看起来像字符串文字”的要求

我尝试过模板……

像这样吗?

#include <array>
const std::array<char, 5> hello = { 'h', 'e', 'l', 'l', 'o' };

#include <cstdio>
int main()
{
    return std::printf("%.*s\n", hello.size(), &hello.front());
}

如果你没有C++11,可以使用Boost.Array,或者自己编写代码。 需要注意的是,这只是一个类型包装器,包装了const char[5],因此应该可以放在数据段中(我已经确认它可以放在.rodata中,使用本地gcc进行测试)。


这不仅仅是打字(虽然我猜那也是其中一部分),而是风格。它很丑,影响了我想要做的事情,即生成一个字节数组,向用户传达可读的消息。 - Adrian
需求可能是win/mac/linux。嗯,也许可以使用C++预处理器作为前端来创建一个简单的解析器,并以此方式生成代码。但我很难应对这个问题。 - Adrian
1
也许有人知道更好的方法。我对用户定义字面值很乐观 :( - Useless
更新:用户定义的字面值在gcc和clang上可以工作,但仅作为扩展。请参见我上面的答案。 - Adrian
我需要花一些时间消化这一切,但是做得很好! - Useless
显示剩余6条评论

2
我曾经忘记了这个问题,并不确定能否找回当时的原始代码,但我已经想出如何存储一个没有终止NUL字符的字符串。
在c++17中,我能够使用一组不包含尾零的字符填充一个constexpr std :: array。
#include <array>
#include <cstdio>

constexpr size_t str_len(char const * x)
{
    char const * begin = x;
    while (*x) {
        ++x;
    }
    return x - begin;
}

constexpr auto var = "hello there";

template <size_t I, size_t Max>
constexpr auto fn()
{
    // Although I did this recursively, this could have also been done iteratively.
    if constexpr (I < Max) {
        auto x = fn<I + 1, Max>();
        x[I] = var[I];
        return x;
    }
    else {
        return std::array<char, Max>{};
    }
}

int main()
{
    auto x = fn<0, str_len(var)>();
    printf("'%*.*s'\n", x.size(), x.size(), x.data());
    return 0;
}

这将产生以下汇编代码:

.LC0:
  .string "'%*.*s'\n"
main:
  sub rsp, 24
  mov edx, 11
  mov esi, 11
  movabs rax, 7526676540175443304 ; <<< hello there
  mov QWORD PTR [rsp+5], rax
  mov eax, 29285
  lea rcx, [rsp+5]
  mov edi, OFFSET FLAT:.LC0
  mov WORD PTR [rsp+13], ax
  xor eax, eax
  mov BYTE PTR [rsp+15], 101
  call printf
  xor eax, eax
  add rsp, 24
  ret

是的,7526676540175443304 是 "hello there" 字符串,没有任何终止的 NUL 字符。 请参见 演示。 将第一行代码放入 main() 的全局空间将导致该字符串位于全局 .text 段中。
.LC0:
  .string "'%*.*s'\n"
main:
  sub rsp, 8
  mov ecx, OFFSET FLAT:x
  mov edx, 11
  xor eax, eax
  mov esi, 11
  mov edi, OFFSET FLAT:.LC0
  call printf
  xor eax, eax
  add rsp, 8
  ret
x:           ; <<< hello there
  .byte 104
  .byte 101
  .byte 108
  .byte 108
  .byte 111
  .byte 32
  .byte 116
  .byte 104
  .byte 101
  .byte 114
  .byte 101

最初的回答:

我也可以将其转换为类型:

演示

template <char x, typename...Ts>
struct X
{
};

constexpr int str_len(char const * x)
{
    char const * begin = x;
    while (*x) {
        ++x;
    }
    return x - begin;
}

constexpr auto var = "hello there";

template <int I>
constexpr auto fn()
{
    if constexpr (I - 1 != 0)
        return X<var[str_len(var) - I], decltype(fn<I - 1>())>{};
    else
        return X<var[str_len(var) - I], void>{};
}

int main()
{
    decltype(nullptr)(fn<str_len(var)>());
    return 0;
}

这给我输出:

最初的回答:

<source>:28:5: error: cannot convert 'X<'h', X<'e', X<'l', X<'l', X<'o', X<' ', X<'t', X<'h', X<'e', X<'r', X<'e', void> > > > > > > > > > >' to 'decltype(nullptr)' (aka 'nullptr_t') without a conversion operator
    decltype(nullptr)(fn<str_len(var)>());
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

"最初的回答"翻译成英文是 "Original Answer"。
以下为需要翻译的内容:

Demo

现在,我可以进一步修改它,以满足上面提出的要求。要求是将字符串存储为非NULL终止,但也要在c++0x中实现这一点,而这不是c++0x,因此我不会将其标记为答案。但我想把它放出来。

编辑

似乎gnu和clang也有一个扩展,允许将字符串放入模板类型中:
template <char...Cs>
struct chars {};

template <typename T, T...Xs>
chars<Xs...> operator""_xxx() {
    return {};
}

int main()
{
    decltype(nullptr)("hello there"_xxx);
    return 0;
}

"最初的回答"应该是:

这将输出:


<source>:5:14: warning: string literal operator templates are a GNU extension [-Wgnu-string-literal-operator-template]
chars<Xs...> operator""_xxx() {
             ^
<source>:11:5: error: cannot convert 'chars<'h', 'e', 'l', 'l', 'o', ' ', 't', 'h', 'e', 'r', 'e'>' to 'decltype(nullptr)' (aka 'nullptr_t') without a conversion operator
    decltype(nullptr)("hello there"_xxx);
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

演示

请注意,将字符串放入模板参数的唯一原因是将字符串作为constexpr传递,这可能有一些有趣的原因,例如允许基于传递的字符串改变所述constexpr函数的返回类型。这有一些有趣的可能性。

另外需要注意的是:不可能直接将字符串传递给constexpr函数并使其改变返回类型,因为作为参数时它不再是constexpr,这有点烦人。唯一的方法是将其声明为外部constexpr变量,并从函数内引用该外部constexpr变量,就像我第二个示例中所示。

编辑2

事实证明,虽然您不能直接将某些内容作为constexpr值传递,但可以传递一个lambda,它将作为constexpr函数工作。

#include <array>
#include <cstdio>

constexpr size_t str_len(char const * x)
{
    char const * begin = x;
    while (*x) {
        ++x;
    }
    return x - begin;
}

template <size_t I = 0, typename FN>
constexpr auto fn2(FN str) {
    constexpr auto Max = str_len(str());
    if constexpr (I < Max) {
        auto x = fn2<I + 1>(str);
        x[I] = str()[I];
        return x;
    }
    else {
        return std::array<char, Max>{};
    }
}

auto x = fn2<>([]{ return "hello there"; });

int main()
{
    printf("'%*.*s'\n", x.size(), x.size(), x.data());
    return 0;
}

这将产生与我第一个示例相同的汇编输出。 演示 坦率地说,我很惊讶它实际上起作用了。

编辑3

鉴于我已经找到了如何传递constexpr字符串,现在我可以创建一个非递归类型:
#include <utility>

constexpr std::size_t str_len(char const * x)
{
    char const * begin = x;
    while (*x) {
        ++x;
    }
    return x - begin;
}

template <char...> struct c{};

template <typename FN, std::size_t...Is>
constexpr auto string_to_type_impl(FN str, std::index_sequence<Is...>)
{
    return c<str()[Is]...>{};
}

template <typename FN>
constexpr auto string_to_type(FN str)
{
    constexpr auto Max = str_len(str());
    return string_to_type_impl(str, std::make_index_sequence<Max>{});
}

int main()
{
    std::nullptr_t(string_to_type([]{ return "hello there"; }));
    return 0;
}

使用以下输出结果:

最初的回答:

<source>:29:5: error: cannot convert 'c<'h', 'e', 'l', 'l', 'o', ' ', 't', 'h', 'e', 'r', 'e'>' to 'std::nullptr_t' (aka 'nullptr_t') without a conversion operator
    std::nullptr_t(string_to_type([]{ return "hello there"; }));
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

当然,对于这些使用c++11的工作,constexpr函数必须转换为递归三元版本。

演示


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接