Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

<xmemory>: constexpr containers fail to compile if _DEBUG is defined #4427

Closed
tiagomacarios opened this issue Feb 27, 2024 · 6 comments
Closed
Labels
invalid This issue is incorrect or by design

Comments

@tiagomacarios
Copy link
Member

The following code fails to compile if _DEBUG is defined.

https://godbolt.org/z/8e6ejch4n

#include <vector>
constexpr std::vector<int> c_emptyVectorInt;
<source>(2): error C2131: expression did not evaluate to a constant
<source>(2): note: (sub-)object points to memory which was heap allocated during constant evaluation
@frederick-vs-ja
Copy link
Contributor

IMO this can be treated as a duplicate of #1035. This is probably "already" fixed in vNext.

@StephanTLavavej StephanTLavavej added the invalid This issue is incorrect or by design label Feb 27, 2024
@fsb4000
Copy link
Contributor

fsb4000 commented Feb 27, 2024

You can't have global constexpr variables. I think we have a closed issue about it but I found only a Discord disscussion
изображение

It works in the compiler explorer because without _DEBUG, an empty vector does no allocate. But it could allocate.

@StephanTLavavej
Copy link
Member

Yep, this is half a duplicate, half by design.

Half a duplicate - in v19, all STL containers dynamically allocate a "proxy object" in debug mode to make iterator debugging work. We can't change this without breaking ABI, and this prevents constexpr vector and constexpr string from being constructed at the top level - because that would attempt to make a constexpr dynamic allocation survive until runtime. This does not prevent normal use of constexpr vector and constexpr string during the evaluation of constexpr functions.

Half by design - vector and string's default and move constructors are supposed to be noexcept (I am glossing over irrelevant micro-details). We've applied that to our implementation, simply allowing any exceptions from the debug allocations to slam into noexcept and terminate (that's #1035 like @frederick-vs-ja said). (As an aside, in vNext, I have a scheme to avoid the debug allocations (for everyone except deque which I hadn't solved at the time) by simply taking locks more aggressively.) However, a conforming and "good" implementation, that has noexcept and doesn't actually allow allocation failures to slam into noexcept, could still allocate memory! There's nothing stopping vector and especially string from trying to allocate eagerly - they could be "nice" and catch failure. So attempting to construct a top-level constexpr vector is (as I understand the Standard) not technically conforming, even though no vector implementations actually try to eagerly allocate capacity.

For string this is easier to see - the Small String Optimization is not mandated, so attempting to make a constexpr string str{"meow"}; will be sensitive to the implementation's SSO buffer size.

Ultimately, unlike #1035 (which a problem, albeit a very specific one that doesn't strike most code), top-level constexpr vectors that are totally empty are totally useless. Top-level constexpr strings are more tempting but also non-portable due to the SSO variation as I mentioned. Since there's nothing beyond #1035 that the STL needs to do differently here, I'm going to resolve this as "not planned".

@StephanTLavavej StephanTLavavej closed this as not planned Won't fix, can't repro, duplicate, stale Feb 27, 2024
@tiagomacarios
Copy link
Member Author

I understand that the constexpr case does not make much sense (it was the minimal repro), but what about the constinit case? What if I want to make sure certain variables do not accrue an initialization cost?

https://godbolt.org/z/EaM6aebch

#include <vector>

constinit std::vector<int> c_emptyVectorInt;

int main()
{
    c_emptyVectorInt.push_back(1);
}

@CaseyCarter
Copy link
Member

I understand that the constexpr case does not make much sense (it was the minimal repro), but what about the constinit case? What if I want to make sure certain variables do not accrue an initialization cost?

Every namespace-scope Standard container has dynamic initialization, even if it only registers the destructor with at_exit. I'm not sure constinit is doing much here given we can easily see there's no initializer.

@tiagomacarios
Copy link
Member Author

constinit guarantees it over time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
invalid This issue is incorrect or by design
Projects
None yet
Development

No branches or pull requests

5 participants