11

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 practical, 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 in 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 of Stack Overflow_ about similar issues, or how to use std::string in a constexpr context:

But none of them answered my question with clarity: How can I 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 can I 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

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
  • 2
    Is it really necessary to have that baked in as a constant? Concatenating strings is absolutely trivial in terms of cost. – tadman Jan 24 '23 at 02:04
  • Hi @tadman, I can't see why I should. Concat is trivial, I agree, but why I would concat on runtime if a #define macro would do the similar at compile time? The reasoning here is to comply with core guidelines, but moving the complexity to runtime may seems like a downgrade? If that's the solution I would rather keep the macro. – Vinícius Ferrão Jan 24 '23 at 02:11
  • 1
    I mean you can do whatever you've got to do, but the overhead of a function that returns a new `std::string` when it's only called a handful of times is likely inconsequential. Unless you're on a system with mere kilobytes of memory, you're likely over-optimizing a non-problem. – tadman Jan 24 '23 at 02:36
  • Gotcha @tadman. That case specifically, even in runtime, would be extremely cheap. I may be wrong but I think those small strings would benefit from SSO. Both would be good. I still prefer keeping the macro, seems to be more clear to me, until the language catch up and enable us to what I wanted in first place. The macro works for me, but I was playing around with the issue. Thanks. – Vinícius Ferrão Jan 24 '23 at 02:44
  • 5
    Perhaps it's personal preference, but I think macros make a giant mess of things and should be avoided whenever possible, even if it theoretically impacts performance. I doubt you'd notice the difference between these two approaches even if you measured carefully, it takes a handful of nanoseconds to bang together a string. Remember macros are untyped. A `const` is ideal, but the next best thing is a function you can call which returns exactly what you want on-demand. Unless you're printing the product name a million times a second it won't be a problem. – tadman Jan 24 '23 at 02:52
  • "constexpr" and "evaluated at compile time" are two rather different things. – n. m. could be an AI Jan 24 '23 at 05:47
  • @tadman: It can save precious RAM (the total available may run as low as, say, 1,500 bytes) on embedded systems (though extra measures may be required to actually put the constant strings in [flash memory](https://en.wikipedia.org/wiki/Flash_memory)). One example would be the macro keyboard I using to write this comment. – Peter Mortensen Jan 24 '23 at 16:05
  • @PeterMortensen I did acknowledge that might be the case, but in that case you'll need to be extremely careful about what C++ features you use. Everything is a trade-off to optimize memory use, even at the expense of performance. I'd also argue you shouldn't write normal code as if you only have 1500 bytes of memory available. – tadman Jan 24 '23 at 16:07

3 Answers3

10

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

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
6

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;
}
user17732522
  • 53,019
  • 2
  • 56
  • 105
4

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.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Blindy
  • 65,249
  • 10
  • 91
  • 131
  • @ViníciusFerrão Don't take it too personally. The C++ compiler treats us far more harshly than this. – tadman Jan 24 '23 at 02:37
  • 1
    Yeah I did not @tadman... And I've said that was "something like that" on my question, I just wrote it here without too much thinking. That's is what it is. SO in it's best. – Vinícius Ferrão Jan 24 '23 at 02:41
  • 1
    Simply digesting the spaghetti allocation scheme involving an `` in an attempt to arrive at `constexpr string c` is a great example of harsh treatment and bewildering array of things the the compiler to do trying to compile something that it doesn't support. – David C. Rankin Jan 24 '23 at 02:57