7

In reading How are char arrays / strings stored in binary files (C/C++)?, I was thinking about the various ways in which the raw string involved, "Nancy", would appear intact in the resulting binary. That post's case was:

int main()
{
    char temp[6] = "Nancy";
    printf("%s", temp);

    return 0;
}

and obviously, in the general case (where the compiler can't confirm if temp is unmutated), it must actually initialize a stack local array to allow for mutations in the future; the array itself must have space allocated (on the stack, or maybe using registers for truly weird architectures), and it must be populated on each call to the function (let's pretend this isn't main which is called only once in C++ and typically only once in C), to avoid reentrancy issues and the like. Whether it hardcodes the initialization into the assembly, or does a memcpy from the program's constant data section is irrelevant; there is definitely something that must be initialized per-call.

By contrast, if char temp[6] = "Nancy"; was replaced with any of:

  1. const char *temp = "Nancy";
  2. char *temp = "Nancy"; (C only; in C++ the literals are const char[], though in practice they're not mutable in C either)
  3. static const char temp[6] = "Nancy";
  4. static char temp[6] = "Nancy";

then the program need not allocate any array-length-based resources per call (just a pointer variable in cases #1 & #2), and in all but case #4, it can put the data in read-only memory baked into the binary's data constants (#4 would put it in the section for read-write memory, but it could still be baked into the binary and loaded copy-on-write).

My question: Does the standard provided leeway for const char temp[6] = "Nancy"; to behave equivalently to static const char temp[6] = "Nancy";? Both are immutable, and modifying them is against the rules. The only differences I'm aware of would be:

  1. Without static, you'd expect the array's address to be colocated with other locals, not in some other part of program memory (could have affects on cache performance)
  2. Without static, you're technically saying the variable is created and destroyed on each call

I don't see anything obviously broken in terms of observable behavior by the standard:

  • You can't watch the array exist and cease to exist except in terms of undefined behavior, e.g. returning a pointer to temp, where there are no guarantees
  • You can't legally compute ptrdiff_t for unrelated variables (only within a given array, plus the one-past-the-end virtual element of said array)

so I'd think the compiler could safely "treat as static" for this case by as-if rules; there's no way to observe the difference, so it can do whatever it feels best.

Is there anything I'm missing where either the C or C++ standard would require some sort of per-call initialization of the const but non-static function scoped array? If the C and C++ standards disagree, I'd like to know that too.

Edit: As Barmar points out in the constants, there are standards-legal ways to detect this behavior in a particular compiler, e.g.:

int myfunc() {
    const char temp[6] = "Nancy";
    const char temp2[6] = "Nancy";
    return temp == temp2;  // true if compiler implicitly made them static or combined them, false if not
}

or:

int otherfunc(const char *s) {
    const char temp[6] = "Nancy";
    return s == temp;
}

int myfunc() {
    const char temp[6] = "Nancy";
    return otherfunc(temp); // true if compiler implicitly made them shared statics, false if not
}
ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • 3
    If the function is recursive, you could store `temp` in each invocation in global pointer variables and then test if they're equal or not. So the question becomes whether those addresses are required to be different. – Barmar Apr 20 '22 at 15:18
  • @Barmar I was thinking along the same lines. However, compilers are allowed to use string-pooling for identical literals, so can they do the same in the case of `const` stuffs across recursive calls? – Adrian Mole Apr 20 '22 at 15:22
  • @Barmar: Ooh, nice way to get a standard legal test. That's a start, and that test might be the basis for figuring out whether the standard requires a particular behavior or not... – ShadowRanger Apr 20 '22 at 15:24
  • @AdrianMole: Well, the case we're discussing *isn't* literals. It's a `const` array initialized from such a literal (which in practice doesn't require the literal to exist; if the array is a local, it could just insert raw `mov` instructions to populate it). So the question is whether string-pooling can even apply to such an array in the first place, given it's not a literal, and it's not `static const` (so it's *logically* stack local memory that you're not allowed to modify, but aside from Barmar's suggestion, there's no way to legally differentiate if it's *really* program constants). – ShadowRanger Apr 20 '22 at 15:25
  • 1
    Agreed. We're in shadowy territory. (See what I did there?) But, AFAIK, the Standards (C and C++) don't say anything about memory management, stacks, heaps or registers, and simply refer to an *Abstract Machine*. But the C++ Standard does have an "as if" rule. – Adrian Mole Apr 20 '22 at 15:28
  • Possibly a simpler test is this: `const char temp1[] = "Nancy"; const char temp2[] = "Nancy"; printf("%d\n", temp1 == temp2);`. Can it print `1`? – Barmar Apr 20 '22 at 15:31
  • @AdrianMole: Yeah, I'm not expecting the language to phrase it in terms of stack vs. global storage vs. whatever. But it might have something to say about whether non-`static` `const` arrays are required to have a unique address (e.g. can Barmar's `temp` and `temp2` actually be stored at the same memory address, whether or not they're declared in the same function?). I *think* the as-if rule should allow this optimization, unless there's weirdness that says "if you're not `static`, you must have a unique address (allocated when the function is entered?), `const` or not", but I'm not sure. – ShadowRanger Apr 20 '22 at 15:35
  • @Barmar: I added a couple examples to the end of my question based on your suggestions. In the process, I *almost* decided it might be forbidden in C (because if `otherfunc`'s parameter was made `restrict`, the addresses would match when they shouldn't), but on checking, `restrict` means bupkes if you don't mutate the data (it's about guarantees about overlapping writes, and doesn't actually forbid two such identical pointers to *exist*, as long as you don't modify what they point to, and with `const` involved, we can't do that). – ShadowRanger Apr 20 '22 at 16:02
  • The as-if rule makes this tricky. A compiler may choose a different strategy if you do things to observe which strategy it picks – Caleth May 06 '22 at 09:59
  • Per C11 `temp` has no linkage and "each declaration of an identifier with no linkage denotes a unique entity" (6.2.2/2). The "unique entity" implies (I guess) "unique address". If an optimizer proved that the uniqueness property is not used, then (I guess) it can make `temp` to have static storage duration. – pmor May 09 '22 at 13:37
  • As for C++: have you seen Jason Turner's [C++ Weekly](https://www.youtube.com/watch?v=4pKtPWcl1Go) on this? Generally static mixed with constexpr enforces compile-time initialization by the power of the language standard. AFAIR, compiler is allowed to optimize non-static code in a similar manner utilizing as-if, but then it's simply more likely to fail on that. – alagner May 12 '22 at 15:25
  • A compiler is free to implement `return temp == temp2;` [the same](https://godbolt.org/z/caPh34dz5) as it would `return false;` – Caleth May 12 '22 at 15:33

3 Answers3

1

Per C11, 6.2.2/6 temp has no linkage, because it is:

a block scope identifier for an object declared without the storage-class specifier extern

and per C11, 6.2.2/2:

each declaration of an identifier with no linkage denotes a unique entity

The "unique entity" implies (I guess) "unique address". Hence, the compiler is required to provide the uniqueness property.

However (speculating), if an optimizer proved that the uniqueness property is not used AND estimated that reading from memory is faster than writing & reading registers (generated code for = "Nancy"), then (I guess) it can make temp to have static storage duration. Note that usually writing & reading registers is much faster than reading from memory.

Extra: temp has block scope, not function scope.


Below the initial answer (which is "out of scope").

C11, 6.8 Statements and blocks, Semantics, 3 (emphasis added):

The initializers of objects that have automatic storage duration, and the variable length array declarators of ordinary identifiers with block scope, are evaluated and the values are stored in the objects (including storing an indeterminate value in objects without an initializer) each time the declaration is reached in the order of execution, as if it were a statement, and within each declaration in the order that declarators appear.

pmor
  • 5,392
  • 4
  • 17
  • 36
  • True, but this particular variable is not `volatile`, and therefore that store is not a "side effect" in the ISO sense of the word. You can't tell if that store happens from within the program, and you can't tell it from the outside. – MSalters May 06 '22 at 11:20
  • This doesn't really answer the question, because thanks to the as-if rule, requirements like this never mandate a particular implementation. The question is whether OP's proposed implementation would behave **as if** your quoted paragraph and the rest of the Standard requirements are satisfied. – Nate Eldredge May 06 '22 at 14:45
  • @NateEldredge Another try: C11, 6.2.2 Linkages of identifiers: "Each declaration of an identifier with no linkage denotes a **unique entity**." As I understood, in OP's proposed implementation it will not be a unique entity. However, what a "unique entity" is? – pmor May 09 '22 at 13:08
1

The standard does not prescribe how local variables are implemented. A stack is a common choice, because it makes recursive functions easy. But leaf functions are easy to detect, and the example is almost a leaf function exact for the side-effect carrying printf.

For such leaf functions, a compiler might choose to implement local variables using statically allocated memory. As the question correctly states, the local variables still need to be constructed and destructed, since they're not static.

In this question, however, char temp[6] has no constructors or destructors. So a compiler which implements local variables in leaf functions as described would have a memcpy to initialize temp.

This memcpy would be visible to the optimizer - it would see the global address, the only use of the same address in printf, and it could then deduce that each memcpy can be moved to program startup. Repeated calls of that same memcpy are idempotent and can be optimized out.

This would cause the generated assembly to be identical to the static case. So the answer to the question is yes. A compiler can indeed generate the same code, and there's even a somewhat plausible way in which it could end up doing so.

MSalters
  • 173,980
  • 10
  • 155
  • 350
0

For C++, although I would expect the answer for C to be equivalent:

If the function with the declaration

const char temp[6] = "Nancy";

is entered recursively, then, in contrast to the variant with static, the declaration will cause multiple complete const char[6] objects with overlapping lifetimes to exist.

Applying [intro.object]/9, these objects may then not have overlapping memory and their addresses, as well as the addresses of their array elements, must be distinct. On the other hand with static, there would only be one instance of the array and so taking its address in multiple recursions must yield the same value. This is an observable difference between the version with and without static.

So, if the address of the array or one of its elements is taken or a reference to either formed and escapes the function body, and there are function calls which may potentially be recursive, then the compiler cannot generally treat the declaration with an additional static modifier.

If the compiler can be sure that either e.g. no pointer/reference to the array or its elements escapes the function or that the function cannot possibly be called recursively or that the behavior of the function doesn't depend on the addresses of the array copies, then it could under the as-if rule treat the array as static.

Because the array is a const-qualified automatic storage duration variable, it is impossible to modify values in it or to place new objects into its storage. As long as the addresses are not relevant to the behavior, there is therefore nothing else that could cause an observable difference in behavior.

I don't think anything here is specific to const char arrays. This applies to all const automatic storage duration constant-initialized variables with trivial destruction. constexpr instead of const would not change anything here either, since that doesn't affect the object identity.


Because of [intro.object]/9, both functions myfunc in your edit are also guaranteed to return 0. The two arrays have overlapping lifetimes and therefore may not share the same address. This is therefore not a method to "detect" this optimization. It causes it to become impossible.

user17732522
  • 53,019
  • 2
  • 56
  • 105