如何避免在C++中使用#define宏来连接两个const char* 变量?

zf2sa74q  于 2023-01-28  发布在  其他
关注(0)|答案(3)|浏览(126)

我希望在代码中消除对#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,这是不正确的。

  1. std::stringlibstdc++的适当支持下应该是constexpr,以便在编译时计算字符串操作,但情况似乎并非如此。
    1.我误解了标准库上__cpp_lib_constexpr_string宏的实用程序。
  2. C++20将在constexpr上下文中提供更大的文本操作灵活性。
    我已经做了功课,遇到了其他问题os StackOverflow关于类似的问题,或者如何在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

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

aiazj4mn

aiazj4mn1#

您可以使用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();
  };
};

Demo

vsmadaxz

vsmadaxz2#

我可以在constexprcontext中使用fmt。这是不正确的。
是的,我实际上不确定为什么会这样。也许有一些问题会使它目前很难实现(Eidogg.在实现中依赖一些非constexpr函数)。
但即使它是constexpr,在这里也没有用,因为它返回std::string和以下几点:
在libstdc 的适当支持下,std::string应该是constexpr,以便在编译时计算字符串操作,但实际情况似乎并非如此。
Libstdc 确实支持C20 constexpr友好的std::string,并且可以在编译时操作它们。这里没有实现问题。但是,目前语言中没有机制让编译时的动态分配持续到运行时。但是std::string确实需要动态分配,因为它可以存储任意长度的字符串,所以不可能将std::string从编译时上下文传递到运行时上下文(例如,使用constexpr关键字定义std::string)。编译时使用的任何std::string必须在编译时上下文结束之前销毁。
我误解了标准库中__cpp_lib_constexpr_string宏的实用程序。
它只表明std::string可以在编译时使用,正如我上面所描述的。
C
20将在constexpr上下文中提供更大的文本操作灵活性。
从C++20开始,在constexpr上下文中,你可以自由地以任何你想要的方式操作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.data()可以像原来的title一样使用(如果需要,可以存储一个constexpr const char*,但是需要将fixed_string存储在一个实际的constexpr变量中,以管理数据的生命周期)。
所有这些都可以通过使用字符串和范围语义定义自己的fixed_string类来改进,而不是依赖于std::array。(作为对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;
}
wgx48brx

wgx48brx3#

您编写的“连接字符串”代码是一个即将发生的访问冲突:

static constexpr const char* title = std::string{
    std::string{productName} + std::string{" Installer"}}.data();

所有在那里构建的临时字符串都会在该行的末尾过期,而留下的指针将指向未分配的内存,再次出现访问冲突。
现在来看看你的连接,operator+stringstring或者一个常量上确实有效,它确实是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+内部),但它们不能泄漏到外部,它们必须在函数内部收集和释放。我们需要更多的支持来尝试做什么。

相关问题