> The size of the array needs to be known at compile time. In C++, a const variable can be a constant expression, meaning it can be evaluated at compile time. In C, this is not the case, and we must instead use a pre-processor macro:
Enums are also good here in C land for specifying compile time constants.
It's worse than that. The type of a C enumeration is implementation defined. It must cover the full range of defined values, but it definitely doesn't have to be 'int'. (§ 6.7.2.2 (4)):
> Each enumerated type shall be compatible with [ed: one of] char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined but shall be capable of representing the values of all the members of the enumeration.
One possible source of confusion is that explicit values in an enum (i.e., '42' in your example) must have values representable as int. (§ 6.7.2.2, (2)).
obj is of type "enum foo", which is compatible with some implementation-defined integer type, but the constants "this", "that", and "the_other" are of type int.
In C++, the constants are of type "enum foo", which can also be referred to as "foo".
> I'm suspicious of restrict. It seems like playing with fire, and anecdotally it seems common to run into compiler optimisation bugs when using it because it's exercised so little.
On the contrary, I wish C++ had restrict in the standard, for exactly the reasons mentioned: it can help the optimizer in certain cases.
This is all totally fair -- I've never used restrict, so it's hard for me to have a useful opinion on it.
I think it's easy to dismiss things that can help optimisation when you've never seen their effect first-hand. I imagine if I'd had an experience where I'd used it to great advantage I'd be wishing it was in the C++ standard too :)
It was a superset, once. C++ started as a C preprocessor. But the languages have diverged. It's enough of a superset, though, that C++ hasn't deleted bad features of C, like pointers and arrays being the same thing.
Which is often just meaningless pedanticism. There are a lot of questions about C APIs where it's irrelevant whether the answer uses C or C++. The latter might not be an exact superset, but in most cases it's easily close enough.
I think it's quite a common thing for people to say if they're not actually experienced in writing the languages. I've heard it from multiple people.
As I say in the blog post, it's common knowledge for people who are experienced in writing C and/or C++ that this isn't true. But it's a misconception that persists among other programmers.
Have you seen practical cases where there has been issues because C code is not compatible with C++? The blog has nice toy examples, but would be nice to know if they actually occur in the wild.
I thought it was until just now. I've never done anything of substance in either, but I've tried both. Now I'm wondering if it was something I heard, or if I just assumed based on my extremely limited experience with both languages.
is said to have undefined behavior, but in fact the initialization of `bar` is a constraint violation, requiring a diagnostic. (Some compilers will issue a non-fatal warning, which is allowed by the C standard but IMHO is unfortunate.)
Another example: it says that this:
const size_t buffer_size = 5;
int buffer[buffer_size];
will not compile in C, but it's valid at block scope in C99, which introduced variable-length arrays. (C11 made them optional.)
"In C, this would compile, albeit likely with warnings about implicit conversion:"
int main() {
auto x = "actually an int";
return x;
}
The "implicit int" rule was dropped in C99, and even before that the language did not define an implicit conversion from char* to int. Again, some compilers might support it with a warning, but it's a constraint violation.
I haven't been able to find anything in the C11 standard about the initialisation of bar being a constraint violation. In 6.7.3, the only constraints for type qualifiers listed are for _Atomic and restrict. Could you let me know where you got the information you talk about here from? I could be missing part of the standard.
N1570 6.7.9p11 says that the constraints and conversions for simple assignment apply to scalar initializers.
6.5.16.1p1 says that, for pointers, "the type pointed to by the left has all the qualifiers of the type pointed to
by the right".
In this case, the right operand is a pointer to a const-qualified type and the left operand is a pointer to a non-const-qualified type.
(Without this restriction, you could silently discard const qualification just by assigning or initializing a pointer, which would largely defeat the purpose of const.)
This is true, but C++ is mostly a superset of C, which is "good enough" for the vast majority of developers. It's enough of a superset that we were able to seamlessly integrate our legacy C libraries with our modern C++ applications without hassle (and we didn't run into any of the corner cases listed in the article).
Not that I've worked in a lot of places, but everywhere that I've worked that uses C at all treats it this way. Writing new C code is considered a no-no.
I write code for expensive embedded processors and it's still C all the way, with C++ recently starting to make a (small) appearance. As such I'm acutely aware of the wonderful incompatibilities between C and C++. My favourite is this:
struct point { int x; int y; };
int main() { struct point p = { .y = 0, .x = 0 }; }
error: designator order for field ‘point::x’ does not match declaration order
Yes, embedded development has always been stuck in the stone age. C is actually a recent, and major advance over assembly. It happened only because vendors discovered they could steal accounts from competitors by being source-compatible, even though their ISA was different. C++ does not offer that advantage for them, no matter how much it might benefit developers, so they have no intention of ever supporting it.
But Arduinos are programmed in a dialect of C++, so it will become necessary to enable it in the near future, i.e. by 2030. Civilization might fall first.
But competent C++ programmers are expensive. It is harder to tell whether a C programmer is competent, because the code they write has less room to be better than minimally tolerable.
You're high. In industry C and C++ are very widespread. Go and Rust are ... not. You'll have an easier time finding a job that requires FORTRAN than one requiring Rust, and Go is only a bit more popular.
Let's touch base in five years and see if anybody remembers Rust. It is likely they will, but not assured.
Consider that the number of programmers adopting C++ every day far, far exceeds the number adopting Rust, and the rate is increasing faster, in absolute numbers, than ever before, it is hard to imagine any scenario where C++ is less popular, relative to Rust, than today. Yes, C++ usage will less than double, while Rust usage will multiply, but the absolute difference in number will be much larger than today.
You seem to underestimate the vast amounts of existing C++ and C code. That code is not going anywhere, especially in only 5 years. Companies are not going to rewrite stuff in Rust just because of memory and type safety.
Touch base in 5 years... Rust will still be a niche language due to its complexity. (IMO, Go has a better chance of at least picking up a few percentage points.)
I write C every day in embedded RISC-V. I think Rust looks neat, and would find it cool to use. But I just convinced the team to move off of gnu90, and a huge amount of my work is in assembly.
Rust offers the idea of safety and security, but at the same time hedges itself with unsafe - fine, sure. However, my work would be nearly 100% in unsafe. So it results in an unpredictable, complex, unsafe language - a non starter.
C isn't just legacy, it's fundamentally broken. Rust is the future. C++, while it mitigates some of the brokenness of C, favored backward compatibility with C over fixing the brokenness once and for all.
Yes, I think I made an embarrassing mess up here by using a compiler that doesn't support VLAs, and not fully understanding VLAs. Thanks for pointing it out.
This is what I was going for in the blog post, but I got confused after seeing these comments as Clang _does_ compile this when the buffer is of static storage class.
Which I don't think is standard -- but it's not using VLAs. I wondered if it just has constant expression semantics for const variables.
Weirdly, adding a _Static_assert to test this theory proves it for c99 but not c11 :/
>I wondered if it just has constant expression semantics for const variables.
That would be my guess also, for applicable const variables. File scope const variables are quite constexpr-y in C anyway, since C requires all file scope variable initializers to be constant expressions (C++ only requires that for constexpr variables).
Toying around with clang in Godbolt, it seems that there are some quirks regarding this. The following is accepted:
static const int n = 0;
int buf[n]; // invalid zero-length array is accepted
// probably another non-standard extension
But the following is not, despite n having the same zero value:
static const int n;
int buf[n]; // complains about a file scope VLA
Well, they are trying to slim down, so to speak, the gap between C and C++ for the sake of a safer interoperability between the languages and for the sake of safer and secure code.
If C could fix _Generic from C11 and make it behave more like a lightweight version of templates, then I could say we have a huge potential of having a safer version of what we currently have.
> Are they trying to make C literally become the same as C++ except classes and templates?
As a C programmer, aren't classes, templates, and exceptions the things that have classically differentiated C and C++?[0] I don't see anything obviously objectionable about nullptr[1], auto, __has_include, or constexpr. (I don't have a ton of experience with them, either.)
I'll admit I don't really grok what "make false and true first-class language features" means — maybe make them reserved keywords? (int)true must still evaluate to 1, in any event.
[0]: "C++ is C with classes!"
[1]: C's "NULL" has this obnoxious wart in that it is implementation-defined whether or not it is a pointer type. I.e., it can be "(void *)0" or just "0". This means that it cannot be used safely in portable incantations of variadic functions that expect pointer arguments.
They're not the "problems" with C++, they're what make C++ great. It's like C is slowly realizing C++ features are actually useful, but doing so as slowly as molasses, while still trying to pretend like this isn't the case...
> [Classes, templates, and exceptions are] not the "problems" with C++, they're what make C++ great.
We're going to have to agree to disagree on that one.
> It's like C is slowly realizing C++ features are actually useful, but doing so as slowly as molasses, while still trying to pretend like this isn't the case...
I think it makes sense to adopt C++ features that do some combination of (1) providing a useful feature, (2) aiding C++ compatibility, and (3) not majorly increasing the conceptual size of the C language. Just like it makes sense for C++ to adopt C99-compatibility (structure literals has taken y'all like 20+ years to adopt).
Templates, classes, and exceptions are a huge huge addition to the complexity of the language. If C programmers wanted them, with all of the pitfalls of manual memory management and C-style lifetime safety, they'd just use C++. Obviously, they don't.
Personally, if I had to choose a language other than C I'd pick something like Zig, Rust, or Go over C++.
C with templates (not classes), STL algorithms/ranges and small things like nullptr, auto, lambdas, stronger basic types, etc is actually a powerful configuration.
> This means that it cannot be used safely in portable incantations of variadic functions that expect pointer arguments.
Why only variadic functions? The answer to that may make my next question obsolete, namely: Wouldn't only C++ complain about this? In C, isn't the input to anything coerced to the data type it will represent, in actual complete disregard of the input type?
In a fixadic function the integer literal would be promoted to a pointer, because the caller knows that the argument should be a pointer. In a variadic function call, the caller guesses the type of each argument based on what is passed. A variadic function needs to know exactly the size of its arguments and sizeof(int) != sizeof(pointer) on many systems.
This works:
void f(char *p);
f(0); /* integer literal auto promoted to pointer */
This probably doesn't, at least not as the number of arguments to f is increased:
void f(...);
f(0); /* assume type of function is void f(int) */
> aren't classes, templates, and exceptions the things that have classically differentiated C and C++?
The key difference is the destructor. Classes, templates, and exceptions extend the reach and value of the destructor. Absent the destructor, the other stuff is of little value (cf. Java).
I do not really care about new features to C - I would just use C++ for these features - but what is your specific argument against something like auto? Or just in general on new features.
does anyone have more information on this? only reference I can find is that blog post; I can't find the specific section of the linked specs that discusses such a utility
Ah yes, what I meant was that more optimisations can be made on memmove if we restrict src and dst, as the overlap case no longer needs to be considered.
I've never used restrict, so I could be missing something, but this is what I meant in the blog post by mentioning memmove.
Been saying this for years. The weird part is that it's a good thing. C++ redefined auto and it has never been the same.However it's clearly a better language than it was in 2010
The C++ specification has an entire appendix devoting to listing incompatibilities with C. Some features missing from this list:
* A char literal is an expression of type int in C, but type char in C++.
* String literals are const in C++ but non-const in C (although attempts to modify them are undefined behavior).
* This program is legal C but not C++:
int i;
int i;
* structs and unions occupy a different name space in C than they do in C++.
* main cannot be recursive in C++, but it can in C.
* C++ allows lvalues in a few more places. Usually, this amounts to a compiler error, but there are a few places where the additional lvalue-to-rvalue conversion is legal and produces a different result.
* There are some cases where C++ requires an explicit cast that C permits an implicit cast (void* being the most well-known)
You are also contributing to global warming. It takes a energy to transfer this data all over the world. And poor people will will run out of their monthly phone data limit.
Designated initializers were added in c++20, I was just looking at a recent change to clang's semantic analysis that warns on the differences between C99 and C++20 designated initializers (Todo: post link when not mobile.)
One recent difference I saw was valid in (via GNU C extension) but invalid in c++:
The designated initializers example will actually compile in recent versions of GCC, Clang, and Visual C++, even if that's not part of the standard prior to C++20. A better example (i.e. one that doesn't compile in C++ but does in C) would be
int arr[3] = { [1] = 5 }
or even
struct A c = {.x = 1, 2}
or another example where the designators are not declared in order of the struct members, or where the designators are nested. The version of designated initializers standardized in C++20 only allows the simple case that's currently implemented in all the major compilers.
It would only be a VLA if defined in a block, I intended the code snippet to be at file-scope, giving the buffer size and array static storage duration.
But it does indeed compile in Clang, and I'm looking into why. I think this line in the C11 standard might be key: "An implementation may accept other forms of constant expressions."
The first example doesn't produce undefined behaviour as c++ - it just won't compile - you can't initialise that pointer to a non const int from a const int without doing something naughty like a const_cast.
The first example won't compile in c++ (can't get a ptr to a non const from a const without something naughty like a const_cast) - that's not undefined behaviour is it?
I love that roughly 35 years after its creation we're still arguing about what c++ is or is not in relation to C. I'm looking forward to the debates and blog posts of my grandchildren on Perl 6 vs Perl 5
C++ was once a near superset of C. However, due to language divergence, the situation is that both C and C++ are large supersets of an intersection.
What is in that intersection depends on which C and C++ dialect pair your intersect.
E.g. a newer C++11 dialect has "long long", so if intersected with C99, "long long" is in the dialect. If we intersect C++ older than C++11 with C, or C older than C99 with C++, then we don't have "long long".
(Except as a conforming extension from a compiler, which we could detect with a configure script and use anyway.)
The thing is that the intersection languages are basically fully fledged C: you can easily develop in them and do everything you'd want from C, if you're willing to live without a few frills here and there like C99 designated initializers, and variable length arrays (dropped from being required in C in C11) and whatnot.
If you require complex numbers, that could get hairy.
A long-time C90 programmer will not find anything amiss, though.
113 comments
[ 6.9 ms ] story [ 255 ms ] threadIf yes, indeed.
I had to view in Reader mode via Firefox.
I could not read it, I got dizzy by its colors; my sensitive vision couldn't stand it -_-
Enums are also good here in C land for specifying compile time constants.
For example, this is valid:
But C enumeration constants are always of type int, so you can't define a constant of some other integer type this way.> Each enumerated type shall be compatible with [ed: one of] char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined but shall be capable of representing the values of all the members of the enumeration.
One possible source of confusion is that explicit values in an enum (i.e., '42' in your example) must have values representable as int. (§ 6.7.2.2, (2)).
For example, given:
obj is of type "enum foo", which is compatible with some implementation-defined integer type, but the constants "this", "that", and "the_other" are of type int.In C++, the constants are of type "enum foo", which can also be referred to as "foo".
On the contrary, I wish C++ had restrict in the standard, for exactly the reasons mentioned: it can help the optimizer in certain cases.
I think it's easy to dismiss things that can help optimisation when you've never seen their effect first-hand. I imagine if I'd had an experience where I'd used it to great advantage I'd be wishing it was in the C++ standard too :)
Sure, the first version was a preprocessor on top of C and certainly that is common knowledge. But a superset? Never heard it.
ObjC on the other hand...
I doubt that C++ was ever a strict superset of C. Has
ever been a valid declaration in C++?This means your "once" was almost certainly "never".
A frightening amount of [c][c++] tagging is expunged there every day.
As I say in the blog post, it's common knowledge for people who are experienced in writing C and/or C++ that this isn't true. But it's a misconception that persists among other programmers.
Off the top of my head, I've run into:
- C++ does not support 'foo([static N])' function declarations
- C++ does not support 'struct A a = { .b = c, ... }' initializers
The conflict arises when these are used in C headers, and then C++ programs attempt to include them.
- C++ is a superset of C, and that virtually any legal C program is a legal C++ program. https://www.tutorialspoint.com/cplusplus/cpp_overview.htm
- "C is a subset of C++." / "C++ is a superset of C." https://www.geeksforgeeks.org/difference-between-c-and-c/
- "C++ is a superset of C; (almost) anything you can do in C, you can do in C++." https://www.cprogramming.com/begin.html
- "As you recall C is a root for C++ and C++ is a superset of C." https://www.c-sharpcorner.com/article/similarities-and-diffe...
For example, this:
is said to have undefined behavior, but in fact the initialization of `bar` is a constraint violation, requiring a diagnostic. (Some compilers will issue a non-fatal warning, which is allowed by the C standard but IMHO is unfortunate.)Another example: it says that this:
will not compile in C, but it's valid at block scope in C99, which introduced variable-length arrays. (C11 made them optional.)"In C, this would compile, albeit likely with warnings about implicit conversion:"
The "implicit int" rule was dropped in C99, and even before that the language did not define an implicit conversion from char* to int. Again, some compilers might support it with a warning, but it's a constraint violation.GCC 5.1.0 reports:
6.5.16.1p1 says that, for pointers, "the type pointed to by the left has all the qualifiers of the type pointed to by the right".
In this case, the right operand is a pointer to a const-qualified type and the left operand is a pointer to a non-const-qualified type.
(Without this restriction, you could silently discard const qualification just by assigning or initializing a pointer, which would largely defeat the purpose of const.)
But Arduinos are programmed in a dialect of C++, so it will become necessary to enable it in the near future, i.e. by 2030. Civilization might fall first.
I would add, more enlightened places.
But competent C++ programmers are expensive. It is harder to tell whether a C programmer is competent, because the code they write has less room to be better than minimally tolerable.
Consider that the number of programmers adopting C++ every day far, far exceeds the number adopting Rust, and the rate is increasing faster, in absolute numbers, than ever before, it is hard to imagine any scenario where C++ is less popular, relative to Rust, than today. Yes, C++ usage will less than double, while Rust usage will multiply, but the absolute difference in number will be much larger than today.
Touch base in 5 years... Rust will still be a niche language due to its complexity. (IMO, Go has a better chance of at least picking up a few percentage points.)
Rust offers the idea of safety and security, but at the same time hedges itself with unsafe - fine, sure. However, my work would be nearly 100% in unsafe. So it results in an unpredictable, complex, unsafe language - a non starter.
const size_t buffer_size = 5; int buffer[buffer_size];
compiles fine in c, but not for the same reason. C99/C11 has dynamic arrays
https://en.wikipedia.org/wiki/Variable-length_array#C99
Which I don't think is standard -- but it's not using VLAs. I wondered if it just has constant expression semantics for const variables.
Weirdly, adding a _Static_assert to test this theory proves it for c99 but not c11 :/
https://godbolt.org/z/q-bb-n c99 with clang https://godbolt.org/z/ad14Ah c11 with clang https://godbolt.org/z/xJSDQa c11 with gcc (which is the only one giving the output I'd expect)
That would be my guess also, for applicable const variables. File scope const variables are quite constexpr-y in C anyway, since C requires all file scope variable initializers to be constant expressions (C++ only requires that for constexpr variables).
Toying around with clang in Godbolt, it seems that there are some quirks regarding this. The following is accepted:
But the following is not, despite n having the same zero value:Try "clang -std=c11 -pedantic-errors".
They are in discussions to introduce the following in C:
and lots of other goodies [1].https://gustedt.wordpress.com/2018/11/12/c2x/
If C could fix _Generic from C11 and make it behave more like a lightweight version of templates, then I could say we have a huge potential of having a safer version of what we currently have.
As a C programmer, aren't classes, templates, and exceptions the things that have classically differentiated C and C++?[0] I don't see anything obviously objectionable about nullptr[1], auto, __has_include, or constexpr. (I don't have a ton of experience with them, either.)
I'll admit I don't really grok what "make false and true first-class language features" means — maybe make them reserved keywords? (int)true must still evaluate to 1, in any event.
[0]: "C++ is C with classes!"
[1]: C's "NULL" has this obnoxious wart in that it is implementation-defined whether or not it is a pointer type. I.e., it can be "(void *)0" or just "0". This means that it cannot be used safely in portable incantations of variadic functions that expect pointer arguments.
We're going to have to agree to disagree on that one.
> It's like C is slowly realizing C++ features are actually useful, but doing so as slowly as molasses, while still trying to pretend like this isn't the case...
I think it makes sense to adopt C++ features that do some combination of (1) providing a useful feature, (2) aiding C++ compatibility, and (3) not majorly increasing the conceptual size of the C language. Just like it makes sense for C++ to adopt C99-compatibility (structure literals has taken y'all like 20+ years to adopt).
Templates, classes, and exceptions are a huge huge addition to the complexity of the language. If C programmers wanted them, with all of the pitfalls of manual memory management and C-style lifetime safety, they'd just use C++. Obviously, they don't.
Personally, if I had to choose a language other than C I'd pick something like Zig, Rust, or Go over C++.
Why only variadic functions? The answer to that may make my next question obsolete, namely: Wouldn't only C++ complain about this? In C, isn't the input to anything coerced to the data type it will represent, in actual complete disregard of the input type?
For variadic functions, the compiler doesn't know the parameter type.
So to print a null pointer, you need to do this:
or The %p format requires an argument of type void*, so you have to convert to that type if necessary.This works:
This probably doesn't, at least not as the number of arguments to f is increased:For non-variadic functions, the function declaration provides the correct parameter type, and caller arguments are coerced to the correct type.
> In C, isn't the input to anything coerced to the data type it will represent, in actual complete disregard of the input type?
In short: no. Implicit casts between incompatible types are warnings/errors.
The key difference is the destructor. Classes, templates, and exceptions extend the reach and value of the destructor. Absent the destructor, the other stuff is of little value (cf. Java).
Now thats exciting
I've never used restrict, so I could be missing something, but this is what I meant in the blog post by mentioning memmove.
* A char literal is an expression of type int in C, but type char in C++.
* String literals are const in C++ but non-const in C (although attempts to modify them are undefined behavior).
* This program is legal C but not C++:
* structs and unions occupy a different name space in C than they do in C++.* main cannot be recursive in C++, but it can in C.
* C++ allows lvalues in a few more places. Usually, this amounts to a compiler error, but there are a few places where the additional lvalue-to-rvalue conversion is legal and produces a different result.
* There are some cases where C++ requires an explicit cast that C permits an implicit cast (void* being the most well-known)
One recent difference I saw was valid in (via GNU C extension) but invalid in c++:
See https://stackoverflow.com/a/29337570 for more info and examples.
Funny that I say C is not a subset, while this says C++ not a superset.
But mine doesn't go too deep, don't reference any standards.
I love things that explore dark corners of languages like this, look forward to digging deeper.
Also, I like the web design, kinda cyberpunk.
1. http://faehnri.ch/how-c-is-not-a-subset-of-cpp/
In C99 the example you give does indeed compile and silently get turned into a VLA.
But it does indeed compile in Clang, and I'm looking into why. I think this line in the C11 standard might be key: "An implementation may accept other forms of constant expressions."
What is in that intersection depends on which C and C++ dialect pair your intersect.
E.g. a newer C++11 dialect has "long long", so if intersected with C99, "long long" is in the dialect. If we intersect C++ older than C++11 with C, or C older than C99 with C++, then we don't have "long long".
(Except as a conforming extension from a compiler, which we could detect with a configure script and use anyway.)
The thing is that the intersection languages are basically fully fledged C: you can easily develop in them and do everything you'd want from C, if you're willing to live without a few frills here and there like C99 designated initializers, and variable length arrays (dropped from being required in C in C11) and whatnot.
If you require complex numbers, that could get hairy.
A long-time C90 programmer will not find anything amiss, though.