在C++中,如果我必须将两个const char *变量连接起来,该如何避免使用#define宏?

11

我想在我的代码中消除对 #define 宏的依赖,但是我无法通过 constexpr 实现相同的功能。

为了实际应用,考虑以下示例:

#define PRODUCT_NAME "CloysterHPC"
constexpr const char* productName = PRODUCT_NAME;

class Newt : public View {
private:
    struct TUIText {

#if __cpp_lib_constexpr_string >= 201907L
        static constexpr const char* title =
            fmt::format("{} Installer", productName).data();
#else
        static constexpr const char* title = PRODUCT_NAME " Installer";
#endif

    };
};

我通过艰难的方式学到了fmt::format()函数不是一个constexpr函数,它只是一个运行时函数。我原本期望能够在代码中使用它来更加表达性强,但事实证明我不能这样做。所以我尝试使用std::string,但是在将代码改为类似下面这样之后,我得到了完全相同的结果:

#define PRODUCT_NAME "CloysterHPC"
constexpr const char* productName = PRODUCT_NAME;

class Newt : public View {
private:
    struct TUIText {

#if __cpp_lib_constexpr_string >= 201907L
        static constexpr const char* title = std::string{
            std::string{productName} + std::string{" Installer"}}.data();
#else
        static constexpr const char* title = PRODUCT_NAME " Installer";
#endif

    };
};

那么我的误解是什么:

  1. 我以为可以在 constexpr 上下文中使用 fmt,但这是不正确的。
  2. std::string 在有适当支持的情况下应该是 constexpr 的,以便在编译时评估字符串操作,但它似乎并非如此。
  3. 我误解了标准库中 __cpp_lib_constexpr_string 宏的效用。
  4. C++20 将在 constexpr 上下文中提供更多文本操作的灵活性。

我已经做了功课,发现了 Stack Overflow 上关于类似问题或如何在 constexpr 上下文中使用 std::string 的其他问题:

但是它们都没有清晰地回答我的问题:我如何在编译时连接两个给定的字符串,以正确地摆脱代码中的`#define`宏?

这似乎很简单,因为两个字符串都在编译时已知,并且它们也是`constexpr`。最终目标是获得第三个`constexpr const char*`,其内容为:CloysterHPC Installer,而不在代码中使用任何`#define`宏。

我该如何实现��个?在当前语言状态下是否可能?我应该继续使用宏吗?我的当前设置是GCC 12.1.1,RHEL 8.7系统上默认使用libstdc++
gcc-toolset-12-12.0-5.el8.x86_64
gcc-toolset-12-libstdc++-devel-12.1.1-3.4.el8_7.x86_64

注意:有时我在问题中提到字符串时,知道它们不是std::string,而实际上是const char*。这只是一种语言约定,而不是类型定义。


2
有必要将其作为常量嵌入吗?在成本方面,连接字符串绝对是微不足道的。 - tadman
嗨@tadman,我不明白为什么要这样做。我同意Concat很简单,但如果一个#define宏可以在编译时完成类似的工作,为什么我要在运行时进行Concat呢?这里的理由是为了遵守核心指南,但将复杂性移动到运行时可能会看起来像是降级?如果这是解决方案,我宁愿保留宏。 - Vinícius Ferrão
1
我的意思是你可以尽情做你想做的事情,但是当一个函数只被调用了几次,并且返回一个新的std::string时,其开销很可能是微不足道的。除非你在一个仅有几千字节内存的系统上,否则你很可能会过度优化一个无关紧要的问题。 - tadman
5
也许这是个人偏好,但我认为宏会让事情变得一团糟,应该尽可能避免使用,即使在理论上影响了性能。我怀疑你甚至不会注意到这两种方法之间的差异,即使你仔细测量,拼接一个字符串只需要几个纳秒。记住宏是无类型的。一个const是理想的,但下一个最好的选择是一个可以调用并返回所需内容的函数。除非你每秒打印一百万次产品名称,否则这不会成为问题。 - tadman
@tadman:它可以在嵌入式系统中节省宝贵的RAM(总可用量可能低至1,500字节),尽管可能需要额外措施来实际将常量字符串放入闪存。一个例子就是我用来写这个评论的宏键盘。 - Peter Mortensen
显示剩余3条评论
3个回答

10
您可以使用FMT_COMPILE在编译时格式化一个std::array
constexpr auto make_title() {
  constexpr std::size_t size = fmt::formatted_size(
    FMT_COMPILE("{} Installer"), productName);
  std::array<char, size + 1> title{};
  fmt::format_to(title.data(), FMT_COMPILE("{} Installer"), productName);
  return title;
}

struct Newt {
  struct TUIText {
    static constexpr auto title = make_title();
  };
};

演示


6
我不能在constexpr语境中使用fmt,这是不正确的。
是的,我实际上并不确定为什么会出现这种情况。可能存在一些问题,使得目前实施起来太困难(例如依赖于实现中的某些非constexpr函数)。
但即使它是constexpr的,它也无法帮助这里,因为它返回std :: string,并且有以下几点:
具有libstdc ++的适当支持的std :: string应该是constexpr以在编译时评估字符串操作,但似乎并非如此。

Libstdc++支持C++20的constexpr-友好std::string,并且可以在编译时操作它们。这里没有实现问题。但是,目前的语言中没有机制能让编译时的动态分配保持到运行时。由于std::string可以存储任意长度的字符串,因此它需要动态分配,所以不可能将std::string从编译时上下文传递到运行时上下文(即使用constexpr关键字定义一个std::string)。任何在编译时使用的std::string必须在编译时上下文结束之前被销毁。

我误解了标准库中__cpp_lib_constexpr_string宏的实用性。

它只表示可以像我上面描述的那样在编译时使用std::string

C++20会在constexpr上下文中提供更多的文本操作灵活性。

constexpr 上下文中,自 C++20 起,您可以自由地以任何方式操作 std::string。但是,您不仅要在编译时上下文中操作 std::string,还要尝试通过编译时/运行时边界传递 std::string
但是他们都没有���晰地回答我的问题:如何在编译时连接两个给定的字符串,以正确地摆脱代码中的#define宏? 正如我上面所描述的,根本问题在于std::string需要动态分配才能提供任意长度的字符串。因此,它不能用于此目的。即使您在编译时评估常量,也不能仅使用const char*,因为那仍然是一个具有生命周期的对象,而原始指针无法管理对象的生命周期。(字符串字面值是一个例外,因为语言通过写入代码来明确地赋予它们生命周期。)
因此,要解决您的问题,您需要一种类型,可以拥有和管理字符串内容的生命周期,并且不需要动态内存分配,这意味着它必须存储固定长度的字符串,例如:
template<std::size_t N>
using fixed_string = std::array<char, N>;

在这里,我会将N解释为包含空终止符的空间。

字符串长度在这里是类型的一部分,因此我们不能简单地编写一个函数,将输入作为函数参数传递,而不在它们的类型中编码长度。或者对于更一般的字符串操作,输出字符串的长度可能取决于字符串输入的内容。

因此,我们需要确保以编码字符串的方式传递输入。一种方法是通过传递模板参数来实现。

现在,您可以编写如下函数:

template<auto str1, auto str2>
constexpr auto concat_constant_strings() {
    constexpr auto size = (std::ranges::size(str1)-1) + (std::ranges:::size(str2)-1) + 1;
    fixed_string<size> result;
    std::ranges::copy(str1, std::ranges::begin(result));
    std::ranges::copy(str2, std::ranges::begin(result)+std::ranges::size(str1)-1);
    return result;
}

可以使用fixed_string作为模板参数进行调用。

现在剩下的问题是将字符串字面值作为模板参数传递,这是不允许的,即模板参数不能是指向字符串字面值的const char*类型。我们需要一种方法将字符串字面值转换为fixed_string,但实际上这并不太困难,事实上这就是库函数std::to_array所做的(假设我们对fixed_string进行了定义):

static constexpr auto title = concat_constant_strings<std::to_array(PRODUCT_NAME), std::to_array(" Installer")>();

现在可以像使用原始的title一样使用title.data()(如果需要,您可以将其存储为constexpr const char*,但是需要将fixed_string存储在实际的constexpr变量中以管理数据的生命周期)。

所有这些都可以通过实际定义自己的fixed_string类来改进,该类具有字符串和范围语义,而不是依赖于std::array。例如,可以为fixed_string提供从字符串字面值(作为对const char数组的引用)的构造函数。通过匹配的推断指南,auto模板参数也可以替换为fixed_string,以便CTAD可以直接接受字符串字面值作为模板参数,而无需通过std::to_array等方式传递。上面的实现是最小化的,可以更加优雅地实现。

此外,可以使用std::integral_constant或类似的模板和字符串字面值操作符,从模板参数移动到函数参数。

另外,关于实现concat_constant_strings,您可以先从str1str2创建std::string,然后生成一个新的字符串,最后构造一个fixed_string返回。这样,您就可以使用任何普通的std::string操作:
template<auto str1, auto str2>
constexpr auto some_constant_string_operation() {
    constexpr auto lambda = []{
        std::string string1(std::ranges::begin(str1), std::prev(std::ranges::end(str1)));
        std::string string2(std::ranges::begin(str2), std::ranges::prev(std::ranges::end(str2)));
        std::string result;
        /* any std::string manipulation */;
        return result;
    };
    constexpr auto size = std::ranges::size(lambda())+1;
    fixed_string<size> result;
    std::ranges::copy(lambda(), std::ranges::begin(result));
    return result;
}

4
你写的“字符串拼接”代码存在访问冲突的风险。
static constexpr const char* title = std::string{
    std::string{productName} + std::string{" Installer"}}.data();

你正在构建的所有临时字符串都在该行结束时过期,留下的指针将指向未分配的内存。再次,等待发生访问冲突。

现在关于你的连接,string 上的 operator+ 和一个 string 或文字常量确实可以工作,并且它确实是 constexpr。如果你查看从连接两个 constexpr string 对象的编译器输出,你会发现它们被正确地连接了:

constexpr string a = "meep";
constexpr string b = "moop";
constexpr string c = a + b;

// look at the end of this:
// error: 'std::__cxx11::basic_string<char>{std::__cxx11::basic_string<char>::_Alloc_hider{((char*)(& c.std::__cxx11::basic_string<char>::<anonymous>.std::__cxx11::basic_string<char>::<unnamed union>::_M_local_buf))}, 8, std::__cxx11::basic_string<char>::<unnamed union>{char [16]{'m', 'e', 'e', 'p', 'm', 'o', 'o', 'p', 0}}}' is not a constant expression

你不能够泄露动态内存分配(例如通过将结果赋值给变量)。C++20的constexpr支持方式是,在函数内部(在本例中是operator+内部),可以使用动态分配,但不能泄露到外部,必须在函数内部进行收集和释放。需要更多的支持来实现你所需求的目标。


@ViníciusFerrão 不要太在意。C++编译器对我们的处理比这还要严厉。 - tadman
1
是的,我没有@tadman...而且我在我的问题中说过那是“类似的东西”,我只是没有太多思考地在这里写下来。这就是它的本质。SO最好的状态。 - Vinícius Ferrão
1
简单地消化涉及到一个 <未命名联合> 的意大利面分配方案,试图得出 constexpr string c 是一个很好的例子,说明编译器在尝试编译它不支持的东西时所做的严厉处理和令人困惑的事情数组。 - David C. Rankin

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