Why does constexpr prevent auto type deduction in this statement?
Asked Answered
P

2

8

In the following if I use constexpr the compiler apparently the compiler says "expression must have a constant value", this happens on MSVC and GCC:

int main() {
    constexpr auto nnn = {
        "this", "sentence", "is", "not", "a", "sentence",
            "this", "sentence", "is", "a", "hoax"
    };
}

Without the constexpr the compiler can deduce that nnn variable is an std::initializer_list<const char*>. And the std::initialiser_list class has a constexpr constructor, why can't this be constexpr?

Puberulent answered 19/9, 2024 at 7:56 Comment(14)
This question is similar to: Why do auto and template type deduction differ for braced initializers?. If you believe it’s different, please edit the question, make it clear how it’s different and/or how the answers on that question are not helpful for your problem.Swim
May be to complete the dup, the braced initialization leads to deduce a std::initializer_list which cannot be used in a constant expression. Sorry, I don't have right now the precise statements from the standard (but the compiler are quite explicit about that :) ). I think it might be the lack of a constexpr copy/move constructor (nn deduced as std::initializer_list and copy/move constructed from the right hand side)Swim
Clang error message is more informative: "note: pointer to subobject of temporary is not a constant expression" DemoVioletvioleta
btw I removed the dupe flag, cause it does not address the constexpr part: #17583167Swim
The clang message seems to show that my comment is not quite accurate, thus I'm waiting for a more correct answer...Swim
As working alternative: constexpr std::array nnn = {..}; DemoVioletvioleta
"std::initialiser_list class has a constexpr constructor" only the default one, else it uses a "magical" conversion from braced-init-list to std::initializer_listVioletvioleta
The given program compiles in all three compilers so this is not a minimal reproducible example. Always test your programs with the latest compiler versions and different compilers with standard conformant flags.Entryway
@user12002570: not when it is used in local scope DemoVioletvioleta
@Entryway I think OP intends nnn to be a local variable.Sharp
@WeijunZhou Yes, this is why we ask for a minimal reproducible example.Entryway
@Violetvioleta Yes, this is why we ask for a minimal reproducible example.Entryway
Related/dupe old msvc bug: constexpr int* ptr =&i compiles in msvc but not with clang and gccEntryway
Related to why-is-an-initializer-list-of-enum-values-not-considered-a-constant-expressionVioletvioleta
M
12

Type deduction is not failing. The compiler is deducing auto to std::initializer_list<const char*>. You get the same result if you wrote std::initializer_list<const char*> instead of auto.

The initialization fails to be a constant expression (something that can only be checked after all types have been deduced).

It fails to be a constant expression, because std::initializer_list works as if an array with the same storage duration was defined in the same scope to hold the elements and as if the std::initializer_list object held pointers to the beginning and end of that array.

Because that array would have automatic storage duration in your example, the initialization of the std::initializer_list object can't be a constant expression, because the result of a constant expression can only contain pointers to objects with static storage duration. (The value of pointers to an object with automatic storage duration would change each time the initialization is performed, so can't be constant expression results.)

You can force the unnamed array to have static storage duration (as well as the std::initializer_list) by using static:

int main() {
    constexpr static auto nnn = {
        "this", "sentence", "is", "not", "a", "sentence",
            "this", "sentence", "is", "a", "hoax"
    };
}
Midgett answered 19/9, 2024 at 8:55 Comment(12)
Do you have standard quotes for the "as if" part?Sharp
@WeijunZhou eel.is/c++draft/dcl.init#list-5 and eel.is/c++draft/dcl.init#list-6. It is not exactly the same as described in my answer, since I wanted to avoid talking about temporary objects, but the effect is the same.Midgett
@WeijunZhou And arguably "and the std​::​initializer_list<E> object is constructed to refer to that array" should somehow be covered in [expr.const] which it currently isn't.Midgett
All the initializer_list would have to do is contain an array of const char pointers, each of which is pointing to static memory valid for the life of the program. The initialiser_list itself would have automatic storage duration, true, but that would be the case for basically any class with a constexpr constructor, and those can be assigned to constexpr without making it static. It doesn't make sense.Puberulent
@Puberulent It could, but that is not specified. It is specified that the array has the same lifetime as the initializer_list object. For example, (assuming you were permitted to) if you called main recursively, the standard guarantees you that nnn.data() in each recursive call will be a different pointer value. Your suggestion also wouldn't work generally in the non-constexpr case, because the initializers are supposed to be able to depend on runtime values, which may be different in each call.Midgett
@Puberulent Every self-referential class or class referencing another automatic storage duration variable also has the same problem when using constexpr without static. constexpr and constexpr static do not have the same meaning and which one you need depends on the use case, although if you are only interested in the value, not pointers/references to the variable, then constexpr static is usually better. Even without constexpr you have the same issue anyway: Do you want instances of the variable to have the same or different addresses when the function is called recursively?Midgett
@Puberulent The initializer_list object itself can't contain the array btw. initializer_list has shallow copy semantics. Copying the initializer_list should only copy pointers to the backing array, not the whole array. (Of course we can argue that initializer_list is not a good design in the first place, but that's beside the point.)Midgett
Isn't constexpr std::array nnn = { "this", "sentence", "is", "not", "a", "sentence", "this", "sentence", "is", "a", "hoax" }; closer to the intended semantic?Swim
@Swim Are you asking me or OP? Yes, std::array is probably closer to the semantics that one expects in most situations. But OP didn't say why they want a std::initializer_list. (And that auto initialized with braces defaults to std::initializer_list is again a design decision that one can argue about, but it is too late to change.)Midgett
@Midgett both of you actually. It was a mere remark, not a critic of the answer that I found perfectly clear.Swim
@Midgett "auto initialized with braces defaults to std::initializer_list", doesn't any initialised braces just simply mean initializer_list, outside of a constructor call with the elements being arguments?Puberulent
@Puberulent No, braces do not automatically imply std::initializer_list in any case, except when using auto in a variable definition. Otherwise braces only relate to std::initializer_list in so far as that they cause overload resolution to prefer functions that take std::initializer_list parameters in different contexts.Midgett
C
-1

The constexpr constructor mentioned by OP is the default constructor that creats an empty list. The actual usable constructor doesn't have any standard interface to begin with. One primary requirement of constant expressions is that every invoked function is visible, inlined and sepcified as constexpr. As a result, std::initializer_list just cannot be constexpr. It looked like a necessary abomination when C++11 was about to be completed. I wished it were deprecated by now. I can not imagine any use case without alternatives. Usage as function/constructor argument can be handled with std::span<const T> it just requires a bit more typing:

constexpr auto foo(std::span<std::uint64_t> arr){
   return std::ranges::fold_left(arr,0ull,std::plus<>{});
};

auto constexpr N = foo(std::array{1ull, 2, 3});

Other usages are just duplicates of std::array:

constexpr std::array nnn = {
        "this"sv, "sentence", "is", "not", "a", "sentence",
            "this", "sentence", "is", "a", "hoax"
    };//fixed-size array of std::string_views

As of C++23, all std containers have range constructors, So it's just possible to pass in a std::array:

std::vector<int> vec(std::from_range, std::array{1,2,3,4});

IMO, std::initializer_list has no more practical use cases. Everything already has a better substitution.

Cornet answered 19/9, 2024 at 9:49 Comment(10)
"std::initializer_list just cannot be constexpr" It is allowed by compilers by adding static as local scope, or just at global scope...Violetvioleta
@Violetvioleta If it's marked with constexpr why can't it be constexpr? That's so confusing. I know that constexpr doesn't guarantee constexpr, but consteval does, however to append the keyword constexpr to a type that "can't" be constexpr is really confusing.Puberulent
@Zebrafish: Unsure why I'm been mentioned (@). My comment was to spot issue in the answer.Violetvioleta
The none-default constructor used to create an actual list is not constexpr. Because it doesn't have an inline definition. It's platform specific constructor with out of line binary definition.Cornet
If anyone wants a citation on standard wording of exact reason why initializer_list can not be constexpr, tag the question as language_lawyer. But the gist of it is first paragraph in my post, unless something changes in future std revisions.Cornet
@Violetvioleta being static or not is irrelevant. All constexpr values have static duration. The problem is absence of none-default constexpr constructor; because there's no possible way to inline such a ctor. std::initializer_list relies on core definitions, it's like std::type_info.Cornet
So you mean that the other answer is incorrect, and the 3 compilers are also incorrect to accept code when nnn is declared as static?Violetvioleta
@Violetvioleta implementers follow their own route, when std is silent about a matter. making the snippet work is not wrong behavior; It is not just specified by std. constexpr object always has static duration, but in some scenarios compilers treat it differently based on existence of explicit static specifier; that's a compiler bug. Either both snippets should be accepted, or both rejected. Now we can push the commitee to overcomplicate this abomination and create more questions, or stop using it.Cornet
"One primary requirement of constant expressions" Requirements mostly list what it should NOT call. but "magical" braced-list to std::initializer_list is not listed as such. it is not a non-constexpr function (as not a function at all).Violetvioleta
@Violetvioleta That's the problem. Nobody says what it is. And if it is not something composable with code, it must not be put in library. Make initializer_list a keyword and give it all magical power you wish. Don't pretend it is part of the library, if it needs such deep definitions.Cornet

© 2022 - 2025 — McMap. All rights reserved.