Home > Software design >  How to avoid using #define macros in C in a case where I have to concatenate two const char* varia
How to avoid using #define macros in C in a case where I have to concatenate two const char* varia

Time:01-24

I would like to remove the reliance of #define macros in my code, and I'm unable to achieve the same functionality with constexpr.

To be pratical consider the following example:

#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

    };
};

I've learned the hard way that fmt::format() function is not a constexpr function and it's only a runtime function. I was expecting that I could it on the code to be more expressive, but I can't. So I've tried using std::string, but again I got the same exact results after changing the code to something like:

#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

    };
};

So what are my misunderstandings:

  1. That I could use fmt in a constexprcontext. Which is untrue.
  2. std::string with proper support from libstdc should be constexpr to evaluate string operations at compile time, but it does not seems to be the case.
  3. I misunderstood the utility of __cpp_lib_constexpr_string macro on the Standard Library.
  4. That C 20 would give more flexibility with text manipulation in a constexpr context.

I already done my homework and came across other questions os StackOverflow about similar issues, or how to use std::string in a constexpr context:

But none of them answered my question with clarity: How to concatenate two given strings at compile time to properly get rid of #define macros in the code?

This seems to be trivial, since both strings are known at compile time, and they are also constexpr. The final goal would be to have a third constexpr const char* with the content: CloysterHPC Installer without using any #define macros on the code.

How to achieve this? Is that even possible in the current state of the language? Should I continue using the macro? My current setup is GCC 12.1.1 with default libstdc on a RHEL 8.7 system:

gcc-toolset-12-12.0-5.el8.x86_64
gcc-toolset-12-libstdc  -devel-12.1.1-3.4.el8_7.x86_64

Thanks.

PS: Please note that sometimes I mentioned strings in the question knowing that they aren't std::string but actually const char*, it was just a language convention and not a type definition.

CodePudding user response:

That I could use fmt in a constexprcontext. Which is untrue.

Yes, I am not actually sure why that is the case. Maybe there are some issues that would make it too hard to implement at the moment (e.g. reliance on some non-constexpr function in the implementation).

But even if it was constexpr, it wouldn't help here because it returns std::string and the following points:

std::string with proper support from libstdc should be constexpr to evaluate string operations at compile time, but it does not seems to be the case.

Libstdc does support C 20 constexpr-friendly std::string and can manipulate them at compile-time. There is no implementation issue here. However, there is currently no mechanism in the language to let a dynamic allocation at compile-time live until runtime. std::string does however require dynamic allocation, because it can store strings of any length. So it can't be possible to pass a std::string from a compile-time context to a runtime context (i.e. define a std::string with the constexpr keyword). Any std::string used at compile-time must be destroyed before the compile-time context ends.

I misunderstood the utility of __cpp_lib_constexpr_string macro on the Standard Library.

It only indicates that std::string can be used at compile-time as I described above.

That C 20 would give more flexibility with text manipulation in a constexpr context.

Inside a constexpr context you are free to manipulate a std::string in whatever way you want since C 20. But you are not only trying to manipulate a std::string in a compile-time context, you are also trying to pass a std::string across the compile-time/runtime boundary.


But none of them answered my question with clarity: How to concatenate two given strings at compile time to properly get rid of #define macros in the code?

As I described above, the fundamental issue is that std::string requires dynamic allocation to provide strings of arbitrary length. So it can't be used for this purpose. Just a const char* also can't be used, because even if you evaluate a constant at compile-time, that is still an object with a lifetime and a raw pointer can't manage the lifetime of an object. (String literals are an exception because the language specifically gives them a lifetime simply by being written in the code.)

So to solve your problem, you need a type that can own and manage the lifetime of the string's contents and that doesn't need dynamic memory allocation, meaning that it must store fixed length strings, e.g.:

template<std::size_t N>
using fixed_string = std::array<char, N>;

Here I would interpret N to contain space for a null-terminator.

The string length here is part of the type, so we can't simply write a function taking inputs as function arguments that don't encode the length in their types. Or for more general string manipulation, the output string's length might depend on the contents of the string inputs.

So, we need to make sure that the inputs are passed in a way that their types encode the string. One way of doing this is by passing as template arguments.

Now you can write e.g. the following function:

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;
}

which can be called with fixed_string's as template arguments.

Now the remaining problem is passing string literals as template arguments, which isn't allowed directly, i.e. the template parameter may not be of type const char* referring to a string literal. We need a way to convert the string literal to a fixed_string, which however isn't too difficult, in fact that is what the library function std::to_array does (assuming our definition of fixed_string):

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

Now title.data() can be used as you did your original title (and if you want you can store a constexpr const char* to that, but you need to store the fixed_string in an actual constexpr variable to manage the lifetime of the data).

All of this can be improved by actually defining your own fixed_string class with string and range semantics instead of relying on std::array. For example, fixed_string could be given a constructor from string literals (as references to const char arrays). With a matching deduction guide, the auto template parameters could also be replaced with fixed_string, so that CTAD can allow accepting string literals directly as template arguments instead of passing through std::to_array, etc. The implementation above is a minimal one. A lot could be done cleaner.

Also, one can use std::integral_constant or a similar template and string literal operators to move from template arguments to function arguments.

As an additional aside, in the implementation of concat_constant_strings you could form std::strings from str1 and str2 first, then produce a new string and finally construct a fixed_string to return. This way you can lift any normal std::string operation:

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;
}

CodePudding user response:

The code you wrote to "concatenate strings" is an access violation waiting to happen:

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

All the temporary strings you're building there expire at the end of that line, and the pointer left behind will point to unallocated memory. Again, access violation waiting to happen.

Now as to your concatenation, operator on a string and either a string or a literal constant does actually work and it is indeed constexpr. If you look at the compiler output from concatenating two constexpr string objects, you can see that it is concatenating them correctly:

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

What you can't do is "leak" dynamic memory allocations (for example, by assigning the result to a variable). The way the C 20 constexpr stuff is done, dynamic allocations work inside functions (in this case, inside operator ), but they cannot leak to the outside, they have to be gathered and released inside the function. We'll need more support for what you're trying to do.

CodePudding user response:

You can use FMT_COMPILE to format a std::array at compile time.

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

  • Related