Declaring C String Constants
eklitzke.org
eklitzke.org
// 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 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.
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:
So basically "const char []" and "char * const" are more logically equivalent here.
Try -Wstrict-prototypes, then `void` is required.
.LC0:
.string "Lorem ipsum"
[...]
movl $.LC0, %esi
movl $ptr2, %edi
xorl %eax, %eax
jmp bogus
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.)
char const * const ptr2 = "Lorem ipsum";
Edit: Said exact same thing as parent but formatted differently. Sorry. :)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,
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.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.
template<char ... chars>
struct const_string {
static std::string get() {
return std::string { chars ... };
}
};
Please don't do this. extern const char arr[];
void do_arr() {
if (arr) {
dummy();
}
}
allows the compiler to optimize out the conditional: https://godbolt.org/g/FwBeWx.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.
Guessing about pipelining, memory access times, and the effect of generated code size is much less valuable than real measurements.
&ptr
and &arr
are different types! The signature for bogus(arr, &arr)
is something like void bogus(const char* arg1, const char (*arg2)[12])
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.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.
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.