for the same experiment done with a const pointer to const data,
const char * const ptr2 = "Lorem ipsum";
(Incidentally, this kind of thing is why I prefer to write 'char const STAR const' rather than 'const char STAR const', and 'char const STAR' rather than 'const char STAR'; it systematically places the 'const's.)
(PS. HN really doesn't work well for trying to write either unicode or ascii star characters.)
Why would it be a joke? Using `#define` for a string constant is perfectly reasonable for C code, IMO I'd argue it's the idiomatic and best way to do it. String constants that are `#define`d can take advantage of C's automatic string-literal concatenation, allowing you to avoid lots of unnecessary string operations in some cases. If you declare it as a pointer or array instead, then you can't do that.
A certain influential segment of the C and C++ development community, the kind that tends to worship "best practices" and "consistency", has been preaching for years that C preprocessor macros are evil, even in cases where they allow for much clearer code.
I'm not sure these people were born with the ability to experience joy.
The biggest problem with C macros is that they are too opaque. The compilation stack will complain about code that is different from what you have, and leave you with little hint about what in your code is generating the broken code. It's also confusing when debugging.
That said, yes they can lead to large gains so when they are worth it, go for them. Just mention in the documentation that they are macros, in big letters.
And you pay for that with global namespace pollution and utter confusion if you ever reuse that name in a scope where you didn't even know you included the original header file.
Any programming language that needs an entire blog post about how to declare string constants "the right way" is a programming language that needs to disappear.
Haha, real understanding. Any language which requires a standard of 700 pages should be seem suspicious. And any language, which lead to thousands of vulnerabilities needs to disappear.
C is not that far removed from assembly, and gives the programmer strong control over how the program operates. It is a tool, and if you need safety over control / performance / portability / etc., then don't use C.
I'm not sure what you mean by "broken". The programmer must provide 100% correct code, as in any language. It is harder to write secure code in C than in other languages, and in return you get near-maximum control over the program.
The point is, if it's possible to have maximum control over the program without hardness of writing secure code. And it is, as Rust and Ada are showing.
I don't know about you, but I always found Rust a real pain to use. One could say Rust is "secure", but you can still break Rust too. Incompetency knows no bounds. It's better just to learn to be competent than complain it's not dumbed-down enough for one's way of thinking. Hence, I should still learn Rust, and you can still learn C.
I think there so reason to be competent, whatever you mean it, to complain about C.
What I learned about C is that you can never be competent enough for it. And that's the problem. A technology which isn't designed to get under control by humans under a reasonable amount of time is bad.
C is an old language, designed with the knowledge in the 70 and influenced by the needs of the later decades. At its time it was revolutionary, but today it's not fulfilling our needs.
I'm not talking about theoretic problems of C. C vulnerabilities are hunting us from day to day. Everyone is knowing that, but people still find it cool to code in C, want to be competent in C or dare to write security-critical applications in C, as though nothing had happened.
I don't want to be competent in C, because it's overly complex and it's error prone for humans to write safe, secure and readable C code. And every good software developer should know that's wise to don't use overly sophisticated technologies.
I hope you're not also against programming languages that don't allow you to specify if a string reference needs to be constant or not, or there'd be no programming languages left.
You're totally right. But wannabe C experts will never accept that. Stupid broken language. The only valid reason why it should be used is for legacy reasons.
I find it funny that your bashing the C language but your screen name is "irundebian" you realize that Debian is almost entirely written in C right?
> The only valid reason why it should be used is for legacy reasons.
Also there are many projects even today where C is the only choice, like embedded programming, some micro controllers only have a C compiler available.
And because I hate C I should run an operating system written in Rust or Ada or what? Don't understand what's so funny about that.
> Also there are many projects even today where C is the only choice, like embedded programming, some micro controllers only have a C compiler available.
> You clearly like software written in it, ie Debian.
Yes, but not because it's written in C, but because of its functionality, which can of course be implemented in other languages.
> Starting a new software project wether it be embedded programing or programing on big X86-64 machines is not legacy software.
The problem is why you should start a new software project, which uses C.
Most used reason probably is, that C is the standard, there exists only a toolchain for C and other legacy reasons.
I think that C is an excellent choice for -some- projects. I also like that it is easy to see how things work at a low level, since C is relatively close to the metal and maps quite well to assembly.
While I do enjoy this sort of analysis, I feel the decision to choose one method over another should be based on benchmarks (preferably with more than one size of string.) After benchmarking, then do an analysis of the assembly code.
Guessing about pipelining, memory access times, and the effect of generated code size is much less valuable than real measurements.
While they do point to the same data, it's pretty rare to actually want to declare a
const char (*)[N]
as a function argument - template magic code excepted, you don't gain much over a
char *
and it's not the same as a
char**
The two lines do different things, so of course they will generate different code. What do you know, the one that actually lets you do useful things is a few bytes longer.
Arrays and pointers are not the same in C, nor have they ever been. The difference is more than that of mere syntax choices for equivalent concepts.
In the pointer version of the code, the pointer is reassignable. Only the data it references can't be modified, the pointer can. To prevent the pointer from being modified, the declaration should have been:
const char * const ptr = "Lorem ipsum";
With the array version, there is no pointer to be modified and so no need or ability to have a second const.
All this talk about do_ptr being slower because it copies from main memory is misleading. Yes, it's slower, because it's doing something different than what do_arr does. It's not just a slower version of doing the same thing. So trying to compare do_ptr and do_arr makes no sense. And the whole thesis of this piece, that declaring string constants as arrays is better, has no supporting evidence. You know what's better? Don't dereference a pointer if you don't need to. That's it.
Wasn't that sort of the point or using arrays instead of pointers? There is less dereferencing to get to the data, so you need to dereference fewer pointers.
I would like to see more empirical measurement in articles like this. There has to be a reason both compiler decided to emit that pointer based code, surely there is some advantage.
> Wasn't that sort of the point or using arrays instead of pointers? There is less dereferencing to get to the data, so you need to dereference fewer pointers.
No, and that's why the article is confusing. It makes it seem like that's true but it's really not.
If you remove everything else and just look at passing the pointer itself, the fundamental difference between the pointer version and the array version is the pointer version isn't constant, and therefore has one level of indirection. This is because you can reassign to the `ptr` variable. But the array is constant, you can't reassign `ary`. All you really have to do here is put `const` on the `ptr` variable and suddenly you get the exact same code. Or in other words,
const char * const ptr = "Foo";
behaves the same way as
const char ary[] = "Foo";
And the only practical difference between the two at this point is how the sizeof() operator works.
I wonder how the results would have differed if those were automatic variables instead of static? That's a much more real-world situation. You wouldn't be able to ignore the overhead of re-initializing the array, since it would be occurring at run time instead of compile time.
// Bogus function, just to see how arguments are passed.
void bogus();
// Invoke bogus using ptr.
void do_ptr() { bogus(&ptr, ptr); }
// Invoke bogus using arr.
void do_arr() { bogus(&arr, arr); }
"&ptr" and "&arr" are not the same. &ptr gives you a pointer to a pointer (char * *), but &arr just gives you a pointer to the array data. That's why the code is different.
This would normally show up as a compilation error, since &ptr and &arr have different types. It's disguised here by the variadic bogus() function.
(I didn't know that no-arg functions in C are implicitly variadic! That's bizarre. I would have expected modern compilers to disable that by default, but I can't get Clang to warn me about it even with -Wall.)
>(I didn't know that no-arg functions in C are implicitly variadic! That's bizarre. I would have expected modern compilers to disable that by default, but I can't get Clang to warn me about it even with -Wall.)
I didn't know that either. I wonder if `void bogus(void)` behaves any differently?
Good to know. I never bothered with `void` inside the argument list in C before (I primarily write C++, very rarely C). I will have to remember to be more explicit when I do write C code in the future.
Yes, in C you must use 'void' to indicate that the function takes no arguments. (This is deprecated in C++ in favor to an empty argument list - which breaks backward compatibility with C).
It's not really variadic in the sense that you can't use va_start and friends to access the arguments. Instead, if the actual definition doesn't match the call, it's undefined behavior and the compiler can't catch it.
It really seems like the C standard maintainers and compiler vendors should just get together and sort that whole mess out. It can give you a few extra percent in some benchmarks, sure, and help C keep its mostly-deserved reputation as the fastest language, but the cost is huge.
If these non-type-checked functions are really useful in some very unusual use case, give it a special weird syntax. Don't make it the default and inflict it on everybody whether they want it or not.
It's hardly a complete solution, though, since they kept the zero-arg functions around as a special case!
ANSI C is the version I first learned. I didn't know that weird exception for zero-args functions still existed, or had forgotten it. I could be wrong, but I bet most C programmers don't know about it.
I think it's backward compatibility, not speed. Functions with no arguments lists were supported in C89, so changing it now will break someone's code somewhere.
In fact not specifying a parameter list may make your code slower, as (IIRC) default argument promotions are always applied to parameters passed to such a function.
> It really seems like the C standard maintainers and compiler vendors should just get together and sort that whole mess [Undefined behavior] out.
Why do you think there is undefined behavior? Because committee members are lazy?
Not every machine is a C machine (PDP-11). I used to program machines with bytes that could range from 1-36 bits (these are older than the PDP-7/PDP-11 that C was designed for). A joy to program, but the C model did not apply. You may consider these architectures obsolete, and they are, but many decisions in the design of IP, UDP and TCP (and some higher level protocols) still reflect these machines.
Early C standardization efforts bent backwards to support the architectures committee members cared about. Some constraints (undef behavior etc) were removed in later standards (though the C++ committee is more aggressive in this regard than the C committee).
And you well could argue that C's tight coupling to the architecture of the PDP-11 and the programming standards of the 70s holds back CPU design. Many of these undefined areas are places where compilers have the opportunity to take advantage of CPU features (e.g. vector hardware).
The committee mailing list and other discussions are open -- have a read. And if you feel strongly you can even join the committee.
I realize it's there for a reason -- as I understand it, to avoid constraining compilers into generating unnecessarily awkward code for unlikely edge cases. And that fits with one of C's main advantages, being fast and lightweight.
But in the last few years I've read many, many articles about problems that crop up when optimizing C compilers do weird things to reasonable-looking code, citing "undefined behavior" as an excuse. I can't say for sure if I've hit issues like that myself, but I've certainly used libraries that have.
Maybe the problem lies squarely with the compiler vendors, but the language spec enables them.
> do weird things to reasonable-looking code, citing "undefined behavior" as an excuse.
If the behaviour is undefined then the code is broken. Programmers need to learn the language instead of putting together hail mary code expecting it to somehow work in spite of the language specifically telling them that they should not expect any defined behavior out of it.
People who whine about undefined behavior are like kids who insist in jamming their fingers in a electric socket and whine when they get a shock.
AFAICT, "undefined behavior" refers to two entirely-separate classes of behavior:
1. cases where the compiler explicitly recognizes a certain permutation of compilation-state as matching some pattern for what the standard calls "undefined behavior"—and then is unconstrained as to what it does in response, but indeed does something in response to detecting that state;
2. cases where the compiler has no compile-time static-analysis approach available for detecting a certain edge-case (e.g. integer overflow), and, further, is inhibited by the standard from generating code to detect that edge-case at runtime either; so the compiler is forced to leave the response to that condition either up to the programmer to explicitly handle, or up to the target architecture to respond to however it likes when it happens.
It's clear to me that undefined behavior of the second class is sort of just part of what C "is": a language where the complexity-classes of all the primitives [well, except FP math] are constrained at compile-time, so a line of code that's O(1) on one arch won't get runtime-polyfilled into being O(N) on another in order to "make the arch sane."
But my question, then, is this: would it break anything about support for the "not C machine" architectures, if the standard said that everything falling into the first class—explicit undefined behavior—was instead defined behavior, defined as "abort compilation"?
> I don't know, but I would dearly love to use a compiler that does that.
Well you're in luck, since gcc -Werror when used with many of its -W flags will do precisely that.
You guys should read the minutes of committee discussions and position papers. "undefined behavior" is not something introduced to make programmers' lives hard!
Also look at IEEE734 which does define things to an incredible degree -- it's extremely hard to use (as it needed to be!). It's one of the very best standards of all time, and still has, yes, undefined behavior.
> Well you're in luck, since gcc -Werror when used with many of its -W flags will do precisely that.
That's not lucky; I'd argue it's the opposite. An ecosystem built around compilers that default to not using -Werror (or rather: `-Werror=pedantic -pedantic-errors`) is an ecosystem that encourages people to write code that relies on the optimizations compilers make in response to undefined behavior. Which, undoubtedly, "makes [maintenance] programmers' lives hard," when they change one little thing and suddenly the code gets 10x slower because now e.g. a dead loop isn't being elided.
If all compilers defaulted to something like `-Werror=pedantic -pedantic-errors`—and it was highly discouraged to turn those flags off—then jumping in to contribute to a portable, sprawling codebase would be infinitely simpler, because the codebase would have been written in a far more rigorous way up to that point.
Sunlight is the best disinfectant—so why do we allow our compiler-vendors to default the lights to off?
To clarify, it's not actually undefined behavior per se that I'm complaining about, it's the modern trend of highly optimizing compilers which take undefined behavior as an excuse to do all sorts of ridiculous things to your code.
"You may have thought you were checking for overflow here, but technically speaking the result of that operation could be undefined, therefore this code could do literally anything, or nothing. So I'll just remove it all. Congratulations, now it runs faster!"
I honestly hadn't realized that was a controversial view. Doesn't almost everyone dislike that? Are there a lot of C programmers who are happy to accept that risk in exchange for slightly better optimization?
Strange that that isn't included in -Wall! I knew -Wall isn't the same as -Weverything, but I'm always surprised at what it omits. Why wouldn't you want that warning?
Edit to add: Aha, my 'gcc' was actually Clang because I'm on OS X. Installing the real GCC, I find:
gcc -Wstrict-prototypes does what I want and disallows this feature. But -Wall doesn't include that flag, and GCC doesn't seem to have -Weverything.
Clang accepts -Wstrict-prototypes but doesn't catch no-arg functions! That's bad. Even with -Weverything, Clang lets you do anything you want with no-arg functions. Very bad.
Additionally, he keeps talking about dereferencing the pointer, which I don't think is right. The pointer never gets deferenced in the code shown.
I'm not an x86 guru, but I think that "movq ptr(%rip), %rsi" is different because ptr needs to be moved from relative to the instruction pointer (because it is on the stack, as a non-const variable).
It's a global variable, it's not on the stack. It's in the data section.
$arr is copying the address of "arr", so it must use an immediate move (possibly 64-bit). It could also use RIP-relative with the "lea" (load effective address) instruction.
$ptr(%rip) is accessing memory, so it uses RIP-relative addressing with the "mov" instruction.
This is a little off topic, but does anyone have any good resources to help me wrap my head around pointers in C? Right now they are very confusing to me.
Imagine all memory is a big array. A pointer is just an index into that array. A pointer dereference is like accessing something at an array index. A pointer to a pointer is array index to a location where you'll find another array index.
The code is more interesting if you -do- actually dereference the pointer, too. I've changed the prototype of "bogus" to take a single char as the first argument, and dereference the two pointers inside the do_arr/do_ptr functions:
So the "do_arr" version "knows" what value is at * arr because "arr" is immutable, so the compiler can just choose to load a constant. The "do_ptr" version has to load from memory instead.
But what if we tell the compiler that the value of "ptr" won't change (i.e. "char * const" instead of "const char * ")? The code becomes the same:
> I didn't know that no-arg functions in C are implicitly variadic! That's bizarre. I would have expected modern compilers to disable that by default, but I can't get Clang to warn me about it even with -Wall.
BTW -Wall doesn't give you all warnings. It gives you a specific set, which will never change because of all the legacy codebases using -Wall that Clang doesn't want to cause problems with when introducing new diagnostics. The flag that really gets you all of the diagnostics is -Weverything.
As others have noted, this is a little bizarre as the two functions are doing different things; one being less performant than the other is not really surprising. That said, there can be advantages to using array syntax rather than pointers for strings (even when also declaring the pointer to be constant), e.g. the compiler "knows" the array "pointer" is non-null:
Another difference: if you use sizeof, the pointer version gives you the size of a pointer, but the array version gives you the actual number of bytes in the string.
The author took the time to write a blog post, but not trying the code for anything else than that bogus() function. Had they done that, they would have realized their mistake.
Other comments have already deconstructed much of the article, but I'll add this. The primary factor will be your CPU architecture. Can you put the string in the same cacheline as the code or an adjacent cacheline? If so, awesome. However, if you have a microcontroller with a harvard architecture, such as a cortex M3, it's may be better to put your strings in data memory rather than code memory. The processor can simultaneously load your string and the next instructions via the two memory ports.
What I find funny is that through all this he never actually mentioned the fact that sizeof() a string constant includes the NUL character at the end.
This has bitten me more times than I care to admit.
Nowadays, I almost always use the array form and actually declare my strings character by character so that I see the NUL if I actually meant to use it.
86 comments
[ 15.0 ms ] story [ 2033 ms ] thread(PS. HN really doesn't work well for trying to write either unicode or ascii star characters.)
A certain influential segment of the C and C++ development community, the kind that tends to worship "best practices" and "consistency", has been preaching for years that C preprocessor macros are evil, even in cases where they allow for much clearer code.
I'm not sure these people were born with the ability to experience joy.
That said, yes they can lead to large gains so when they are worth it, go for them. Just mention in the documentation that they are macros, in big letters.
The macros themselves are usually in BIG_LETTERS already... that generally gives it away.
What I learned about C is that you can never be competent enough for it. And that's the problem. A technology which isn't designed to get under control by humans under a reasonable amount of time is bad.
C is an old language, designed with the knowledge in the 70 and influenced by the needs of the later decades. At its time it was revolutionary, but today it's not fulfilling our needs.
I'm not talking about theoretic problems of C. C vulnerabilities are hunting us from day to day. Everyone is knowing that, but people still find it cool to code in C, want to be competent in C or dare to write security-critical applications in C, as though nothing had happened.
I don't want to be competent in C, because it's overly complex and it's error prone for humans to write safe, secure and readable C code. And every good software developer should know that's wise to don't use overly sophisticated technologies.
> The only valid reason why it should be used is for legacy reasons.
Also there are many projects even today where C is the only choice, like embedded programming, some micro controllers only have a C compiler available.
> Also there are many projects even today where C is the only choice, like embedded programming, some micro controllers only have a C compiler available.
Yes, as I said, for legacy reasons.
Do you hate C? You clearly like software written in it, ie Debian.
> Yes, as I said, for legacy reasons.
Starting a new software project wether it be embedded programing or programing on big X86-64 machines is not legacy software.
> You clearly like software written in it, ie Debian.
Yes, but not because it's written in C, but because of its functionality, which can of course be implemented in other languages.
> Starting a new software project wether it be embedded programing or programing on big X86-64 machines is not legacy software.
The problem is why you should start a new software project, which uses C. Most used reason probably is, that C is the standard, there exists only a toolchain for C and other legacy reasons.
C/C++ is there to stay for a looong time.
Guessing about pipelining, memory access times, and the effect of generated code size is much less valuable than real measurements.
In the pointer version of the code, the pointer is reassignable. Only the data it references can't be modified, the pointer can. To prevent the pointer from being modified, the declaration should have been:
const char * const ptr = "Lorem ipsum";
With the array version, there is no pointer to be modified and so no need or ability to have a second const.
I would like to see more empirical measurement in articles like this. There has to be a reason both compiler decided to emit that pointer based code, surely there is some advantage.
No, and that's why the article is confusing. It makes it seem like that's true but it's really not.
If you remove everything else and just look at passing the pointer itself, the fundamental difference between the pointer version and the array version is the pointer version isn't constant, and therefore has one level of indirection. This is because you can reassign to the `ptr` variable. But the array is constant, you can't reassign `ary`. All you really have to do here is put `const` on the `ptr` variable and suddenly you get the exact same code. Or in other words,
behaves the same way as And the only practical difference between the two at this point is how the sizeof() operator works.This would normally show up as a compilation error, since &ptr and &arr have different types. It's disguised here by the variadic bogus() function.
(I didn't know that no-arg functions in C are implicitly variadic! That's bizarre. I would have expected modern compilers to disable that by default, but I can't get Clang to warn me about it even with -Wall.)
I didn't know that either. I wonder if `void bogus(void)` behaves any differently?
It really seems like the C standard maintainers and compiler vendors should just get together and sort that whole mess out. It can give you a few extra percent in some benchmarks, sure, and help C keep its mostly-deserved reputation as the fastest language, but the cost is huge.
If these non-type-checked functions are really useful in some very unusual use case, give it a special weird syntax. Don't make it the default and inflict it on everybody whether they want it or not.
ANSI C is the version I first learned. I didn't know that weird exception for zero-args functions still existed, or had forgotten it. I could be wrong, but I bet most C programmers don't know about it.
In fact not specifying a parameter list may make your code slower, as (IIRC) default argument promotions are always applied to parameters passed to such a function.
Why do you think there is undefined behavior? Because committee members are lazy?
Not every machine is a C machine (PDP-11). I used to program machines with bytes that could range from 1-36 bits (these are older than the PDP-7/PDP-11 that C was designed for). A joy to program, but the C model did not apply. You may consider these architectures obsolete, and they are, but many decisions in the design of IP, UDP and TCP (and some higher level protocols) still reflect these machines.
Early C standardization efforts bent backwards to support the architectures committee members cared about. Some constraints (undef behavior etc) were removed in later standards (though the C++ committee is more aggressive in this regard than the C committee).
And you well could argue that C's tight coupling to the architecture of the PDP-11 and the programming standards of the 70s holds back CPU design. Many of these undefined areas are places where compilers have the opportunity to take advantage of CPU features (e.g. vector hardware).
The committee mailing list and other discussions are open -- have a read. And if you feel strongly you can even join the committee.
But in the last few years I've read many, many articles about problems that crop up when optimizing C compilers do weird things to reasonable-looking code, citing "undefined behavior" as an excuse. I can't say for sure if I've hit issues like that myself, but I've certainly used libraries that have.
Maybe the problem lies squarely with the compiler vendors, but the language spec enables them.
If the behaviour is undefined then the code is broken. Programmers need to learn the language instead of putting together hail mary code expecting it to somehow work in spite of the language specifically telling them that they should not expect any defined behavior out of it.
People who whine about undefined behavior are like kids who insist in jamming their fingers in a electric socket and whine when they get a shock.
1. cases where the compiler explicitly recognizes a certain permutation of compilation-state as matching some pattern for what the standard calls "undefined behavior"—and then is unconstrained as to what it does in response, but indeed does something in response to detecting that state;
2. cases where the compiler has no compile-time static-analysis approach available for detecting a certain edge-case (e.g. integer overflow), and, further, is inhibited by the standard from generating code to detect that edge-case at runtime either; so the compiler is forced to leave the response to that condition either up to the programmer to explicitly handle, or up to the target architecture to respond to however it likes when it happens.
It's clear to me that undefined behavior of the second class is sort of just part of what C "is": a language where the complexity-classes of all the primitives [well, except FP math] are constrained at compile-time, so a line of code that's O(1) on one arch won't get runtime-polyfilled into being O(N) on another in order to "make the arch sane."
But my question, then, is this: would it break anything about support for the "not C machine" architectures, if the standard said that everything falling into the first class—explicit undefined behavior—was instead defined behavior, defined as "abort compilation"?
Well you're in luck, since gcc -Werror when used with many of its -W flags will do precisely that.
You guys should read the minutes of committee discussions and position papers. "undefined behavior" is not something introduced to make programmers' lives hard!
Also look at IEEE734 which does define things to an incredible degree -- it's extremely hard to use (as it needed to be!). It's one of the very best standards of all time, and still has, yes, undefined behavior.
That's not lucky; I'd argue it's the opposite. An ecosystem built around compilers that default to not using -Werror (or rather: `-Werror=pedantic -pedantic-errors`) is an ecosystem that encourages people to write code that relies on the optimizations compilers make in response to undefined behavior. Which, undoubtedly, "makes [maintenance] programmers' lives hard," when they change one little thing and suddenly the code gets 10x slower because now e.g. a dead loop isn't being elided.
If all compilers defaulted to something like `-Werror=pedantic -pedantic-errors`—and it was highly discouraged to turn those flags off—then jumping in to contribute to a portable, sprawling codebase would be infinitely simpler, because the codebase would have been written in a far more rigorous way up to that point.
Sunlight is the best disinfectant—so why do we allow our compiler-vendors to default the lights to off?
"You may have thought you were checking for overflow here, but technically speaking the result of that operation could be undefined, therefore this code could do literally anything, or nothing. So I'll just remove it all. Congratulations, now it runs faster!"
I honestly hadn't realized that was a controversial view. Doesn't almost everyone dislike that? Are there a lot of C programmers who are happy to accept that risk in exchange for slightly better optimization?
Edit to add: Aha, my 'gcc' was actually Clang because I'm on OS X. Installing the real GCC, I find:
gcc -Wstrict-prototypes does what I want and disallows this feature. But -Wall doesn't include that flag, and GCC doesn't seem to have -Weverything.
Clang accepts -Wstrict-prototypes but doesn't catch no-arg functions! That's bad. Even with -Weverything, Clang lets you do anything you want with no-arg functions. Very bad.
I'm not an x86 guru, but I think that "movq ptr(%rip), %rsi" is different because ptr needs to be moved from relative to the instruction pointer (because it is on the stack, as a non-const variable).
$arr is copying the address of "arr", so it must use an immediate move (possibly 64-bit). It could also use RIP-relative with the "lea" (load effective address) instruction.
$ptr(%rip) is accessing memory, so it uses RIP-relative addressing with the "mov" instruction.
And because it's a const, it's in the .rodata section... (read-only) Playing too liberally with the data often leads to a SEGV (as it should).
It's old-fashioned but I assume (maybe others can correct me) the latest edition is up to date enough that it won't teach you any outright bad habits.
https://goo.gl/jZYx0f
So the "do_arr" version "knows" what value is at * arr because "arr" is immutable, so the compiler can just choose to load a constant. The "do_ptr" version has to load from memory instead.
But what if we tell the compiler that the value of "ptr" won't change (i.e. "char * const" instead of "const char * ")? The code becomes the same:
https://goo.gl/iNaUHe
So basically "const char []" and "char * const" are more logically equivalent here.
Try -Wstrict-prototypes, then `void` is required.
This has bitten me more times than I care to admit.
Nowadays, I almost always use the array form and actually declare my strings character by character so that I see the NUL if I actually meant to use it.