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:
- That I could use
fmt
in aconstexpr
context. Which is untrue. std::string
with proper support fromlibstdc
should beconstexpr
to evaluate string operations at compile time, but it does not seems to be the case.- I misunderstood the utility of
__cpp_lib_constexpr_string
macro on the Standard Library. - 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:
- Difference between `constexpr` and `#define`
- constexpr std::string in C 20, how does it work?
- Is it possible to use std::string in a constexpr?
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::string
s 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();
};
};