r/cpp • u/Obvious_Set5239 • 2d ago
The weirdest behavior in C++ that came from C
If you're trying to define two pointers in a single statement (what is in general a bad idea), you may want to do it like this:
#include <print>
int main()
{
int x = 10;
int* a, b;
a = &x;
b = &x;
std::println("a={:#x}, b={:#x}", uintptr_t(a), uintptr_t(b));
}
However, it doesn't work as you expect, and won't compile. Because int* a, b; declares only a as int*; b, and all the rest variables will be int. The correct one-statement declaration of two pointers is int* a, * b;
main.cpp:8:9: error: invalid conversion from ‘int*’ to ‘int’ [-fpermissive]
8 | b = &x;
| ^~
| |
| int*
b has type int
This behavior is the reason why some people prefer putting the asterisk next to the variable name, not next to the type
I can understand the logic, that C authors had while making this syntax. It's a sort of reversive/deduction logic. You kinda declare what type it will be after using the dereference * operator, instead of declaring the type being a pointer itself. But I find this logic very-very strange, and overthought
The funny thing, that even the compiler in the error message above, treats * as a sort of type modifier, that is inseparable from int. But, apparently, the C creators had a completely different vision on what pointers are
I personally don't think that this behavior justifies reteaching yourself to write * in front of variable names, and especially in front of function names. I think it's just a not well-thought decision made very long ago in 1970s
7
u/tstanisl 2d ago
It's not a different understanding but rather a different convention.
int a, *b, **c, d();
Means that a, *b, **c, d() returns int.
1
14
u/Supadoplex 2d ago edited 2d ago
I dont think they had a completely different idea of what pointers are. I cannot know what the were thinking, but perhaps their idea was simply that following is a super expressive, concise and convenient way to write a common pattern:
int x, *a, *b;
1
u/Obvious_Set5239 2d ago
To save even more 3 characters to not write this:
int x; int* a, b;I'm not joking :) People were very obsessive in abbreviating everything, so maybe it was actually the reason. (There were reasons, of course, for abbreviations, filenames limitation, for example. And so was the culture)
4
u/no-sig-available 2d ago
People were very obsessive in abbreviating everything
Of course, when you write an entire operating system on a 10 character/second printing terminal, you just don't use long lines of code.
https://www.historyofinformation.com/image.php?id=6430
When you have a gigabit connection, you can make different choices.
1
u/usefulcat 2d ago
I always thought it seemed like a "clever hack", in the "too clever by half" sense of "clever".
But saving a few characters seems equally likely to me.
0
u/Supadoplex 2d ago edited 2d ago
You mean, to not write this ;-)
int x; int* a, *b;And, I doubt people cared about number of characters as much as they may have cared about the number of statements (and thus number of lines while following a decent style).
Remember that at the time, the standard Bell Labs terminal would have had 24 lines of height.
And the space saving is not even the only benefit. The fact that the type name isn't repeated makes the code less brittle, as it's no longer possible for them to mismatch.
3
u/argothiel 2d ago
The spaces used to be much more expensive back in the day! So I would rather expect something like
int x,*a,*b;. And in C it was justx,*a,*b;until they made it illegal.1
u/Obvious_Set5239 2d ago
Sometimes I think how lighter code will look if they return this implicit variable type, but with the new "auto" instead of "int". But in all other aspects beside code lightness - hell no. Horrifying idea
6
u/QuentinUK 2d ago
Similarly for const.
You can typedef or using a type and it applies to all the variables:
using pint = int *;
pint a, b;
I think the original reason was saving characters in file size by having loads of variables defined all on one line.
36
u/Beneficial_Steak_945 2d ago
C++ should just deprecate defining more than one variable in one go this way.
13
u/TheThiefMaster C++latest fanatic (and game dev) 2d ago
At least variables of different types. Existing code that intentionally defines both e.g. an
intand anint*in one line must be rare.Defining multiple ints in one line is common enough to probably need to leave for now.
2
u/elperroborrachotoo 2d ago
... as soon as you can specify compiler version per translation unit, which would mean require getting rid of the header build model.
We are likely talking about billions of lines of code (if we count library duplication, we'll easily catapult ourselves into trillions). "rare" doesn't even begin to have leverage here.
2
u/JNighthawk gamedev 2d ago
C++ should just deprecate defining more than one variable in one go this way.
Wouldn't this also deprecate structured bindings?
7
u/Obvious_Set5239 2d ago
Yes, I agree. It probably was useful in the past, when you were must to declare all variables in the start of the block. But now it should not be used, because variable names should be longer, we don't try to save file size using cryptic abbreviations, and also, we can declare a variable wherever we want
4
4
u/NilacTheGrim 2d ago
No way man. This is how we separate the weak from the strong. Devs that insist that more than 1 on a line is bad are weak and must be slowly culled.
0
u/fdwr fdwr@github 🔍 1d ago
Maybe along with
std::unique_ptrandstd::shared_ptr, there should also be simplestd::ptrfor completeness, so you can say safely usestd::ptr<int> a, b😉 (hmm, I started writing this out as a joke, but now I'm thinking it might be useful elsewhere too...).2
u/n1ghtyunso 18h ago
its spelled
std::add_pointer_t. It's a bit long, i'll give you that, but we arent starved for screen width anymore right? :P1
u/HomelyInterpretation 2d ago
the compiler error always gets me. it's yelling about int a, b` even compiles as two different types in the first place. whoever thought that was a good default for readability was having a rough day
6
u/Sensitive-Talk9616 2d ago
Yeah, it's very much a C coding pattern - declare all variables in bulk at the start.
Ideally, in C++, one would only declare variables where needed, so temporaries should just be directly defined. Even better if they can be scoped so that lifetimes are easier to reason with.
I wouldn't be mad if they simply disallowed declaring multiple variables in a single statement.
6
u/Obvious_Set5239 2d ago
declare all variables in bulk at the start
It actually comes from the past, C89, where you had to declare all the variables at the start. I was very surprised when I wanted to modify something in Wolfenstein 3-D code, and it did not compile :)
6
u/_Ilobilo_ 2d ago
it's because the space for local variables is reserved on the stack at the beginning and the compilers weren't advanced enough to do it for variables defined later
2
u/CornedBee 1d ago
It's not that compilers couldn't have done it, but that
a) it can't be done if you compile line by line, which is useful when your compiler needs to run fast on an extremely memory-starved computer (the alternative is doing multiple passes over the code), and
b) when your target is said extremely memory-starved computer, it's good for the programmer to be able to easily calculate a function's stack size.
2
u/-TesseracT-41 2d ago
At work, even though we use C99, my colleague sometimes writes code this way. It infuriates me. Not to mention, code like this:
int err; err = foo(); if (err == 0) { bar(); }
2
u/Business-Decision719 2d ago edited 2d ago
You're right. This is a really counterintuitive decision C made and C++ inherited. Effectively * is a generic type; it's a pointer, but it needs another type to specify what it's pointing. The * should always attach to its type parameter to generate a new combined type name, but in variable declarations it attaches to the variable name to modify that one variable's type instead.
The same problem exists with arrays.
int x[10], y;
// x is array, y is int
Granted, it's more obvious that only one variable is an array, because the brackets are always separated from the item type and have to go after each individual array variable, but separating the brackets from their type parameter is already bad enough. Then there's array-to-pointer decay which its own abomination.
C's declaration syntax for pointers and arrays is just horrible, and it's one of many problems C++'s containers and smart pointers mitigate very nicely. There's a type int, a type std::shared_ptr<int>, a type std::array<int>, and many more types that could take an int parameter with identical angle-bracket syntax. You can also have a std::shared_ptr<std::array<int, 10>>, keeping pointers and containers orthogonal.
5
u/FallenDeathWarrior 2d ago
I had that discussion with my colleagues what coding style should be used. And they used that example why you shouldn't put the pointer next to the type instead move it to the variable name.
7
u/Beneficial_Steak_945 2d ago
Yeah. It still belongs to the type though, no matter this wart on the standard.
A pointer (usually) is a different size than whatever it is pointing to, hence it’s a different type and the marker belongs to the type.2
u/Obvious_Set5239 2d ago
I knew about this also discussing about * position. Did find it justifying to reteach yourself?
3
u/FallenDeathWarrior 2d ago
I previously mostly worked with java / python and then switched to this C/C++ legacy codebase project. So it wasn't realy reteaching as I adopted to the codebase. Just asked my colleagues why this was the preferred way
3
u/wyrn 2d ago edited 1d ago
I don't think it makes any sense using the language as you wish it were instead of how it factually is.
"Declaration follows use" was a spectacularly dumb idea that confused generations of programmers, yes. But as long as it's part of the syntax, fighting it won't do any good.
EDIT replying to u/usefulcat because OP blocked me for no reason (LOL):
I'd say readability means going with the grain rather than against it. "Declaration follows use" is a dumb rule, but it is the rule, so I'll write my declarations in a way that works with it rather than some imaginary rule that I'd prefer instead.
1
u/Obvious_Set5239 2d ago
Yes, but the language with "Declaration follows use" idea was C. Not C++. Yes, it was designed to be a superset of C89 (primary to hijack its libraries and portion of programmers), but it has the different idea regarding type declaration, and this pointer syntax in particular. And it's supported by "C++ Core Guidelines", the guidelines made by the author of the language himself, Bjarne Stroustrup:
- "NL.18: Use C++-style declarator layout": The C-style layout emphasizes use in expressions and grammar, whereas the C++-style emphasizes types. The use in expressions argument doesn’t hold for references
- "ES.10: Declare one name (only) per declaration": One declaration per line increases readability and avoids mistakes related to the C/C++ grammar. It also leaves room for a more descriptive end-of-line comment.
- "T.43: Prefer using over typedef for defining aliases": Improved readability: With using, the new name comes first rather than being embedded somewhere in a declaration. Generality: using can be used for template aliases, whereas typedefs can’t easily be templates. Uniformity: using is syntactically similar to auto.
3
u/wyrn 2d ago
but it has the different idea regarding type declaration, and this pointer syntax in particular.
If it did, it would've used a different syntax. But "declaration follows use" remains alive and well in C++.
the guidelines made by the author of the language himself, Bjarne Stroustrup:
And, if he really felt strongly about this, he should've changed it. Backwards compatibility has a price.
1
u/usefulcat 2d ago
It's a readability issue. There can be no disagreement about whether it's syntactically valid to put the * next to the variable name. But as as anyone who has ever seen obfuscated C will know, just because you can doesn't mean you should.
1
u/n1ghtyunso 1d ago
this sounds like something that is totally enforcable through tooling alone doesnt it?
Why should anyone reteach himself for this?1
u/BrangdonJ 2d ago
What do you mean, "reteaching"? Some of us learnt C before we learnt C++. I learnt it before C++ existed.
1
u/FallenDeathWarrior 2d ago
I learner C++ in school in the university I got into a lot of programming languages. But mostly worked with python / java and later simple web apps. I also did some certificats in C and C++. But before starting work I wouldn't have said that I am fluent in them
2
u/DreamingPeaceful-122 2d ago
I talked about this in my first SWE job with my coworker. I totally agree about your point.
2
u/NilacTheGrim 2d ago
Confusion what would not have arisen had you been doing the cromulent thing of writing it as Type *var rather than Type* var.
;P
3
1
u/SmokeMuch7356 2d ago edited 2d ago
As I've said a hundred times, we declare pointers as
T *p;
for the exact same reason we don't declare arrays and functions as
T[N] a;
T() f;
Array-ness, function-ness, and pointer-ness are all specified as part of the declarator. This quickly becomes obvious when dealing with pointers to arrays and functions:
T (*pa)[N];
T (*pf)();
or even more complex types:
T *(*apf[N])(); // array of pointers to functions returning pointers to T
The same is true of references, btw; they should be declared as T &r for all the same reasons.
EDIT
And the point I keep intending to make and never remember is that I'm less interested in the type of p than I am of *p; *p is the thing I'm reading and writing to:
T *p = new T;
...
*p = some_value;
std::cout << "value = " << *p;
...
The expression *p acts as a name for an object, and the type of that object is T. From that perspective, T *p feels more natural.
Yeah yeah yeah, references and smart pointers have obviated the need for raw pointers in the vast majority of circumstances. But if you're gonna use raw pointers for whatever reason, the T *p convention will more clearly convey intent and create less confusion in the end.
0
u/Obvious_Set5239 2d ago
In C++ I would write this:
using Func = int (); Func* ptr = &myFunction;Instead of:
int (*ptr)() = &myFunction;About arrays - they are essentially the same pointers, if I understand correctly, it just says the compiler to allocate it on stack
Your examples demonstratively show what I mentioned in the post as "a sort of reversive/deduction logic" of declarations. I understand this logic, and the fact that the creators used it, but I don't understand why, because it's weird. With function pointers it's even weirder
1
u/SamuraiGoblin 2d ago
Yes, I once asked whether people preferred 'int* a' or 'int *a' and got heavily downvoted.
I think it's a valid question, and it doesn't have a simple answer. It's confusing to beginners and it depends on where and how it is used.
5
u/Obvious_Set5239 2d ago
I opened webkit style guidelines, and they are also divided exactly in half: for C++ you must write
int* ptr, but for C you must writeint *ptr. Apparently two teams have different opinions1
u/Obvious_Set5239 2d ago
Upvote ratio is around 45%. Even though my post wasn't exactly about how to write this asterisk. I actually don't care how who writes, I can read any code, and I love that C++ have such a freedom
It's indeed a dividing topic in C++, almost 50/50. I have not expected this 😅
1
u/geon 2d ago
Your formatting is off. It should be
int *a, b;
Not
int* a, b;
2
u/Obvious_Set5239 2d ago
But the compiler error says the type is "int*", not the type is "int" but the thing is a pointer. Btw, read the whole post, I addressed the formatting contravention
0
-2
u/AnyPhotograph7804 2d ago
Yes. When C was invented, even storing sourcecode was not so cheap. And this is the reason why there are so many shortcuts in C like implicit conversions etc. because it saves storage space. And at that time, stuff like implicit conversions was not considered as a potential source of errors. Because the apps did not have millions LOC and hundreds of developers working on one application.
And because C++ is based on C, it inherits all these little quirks. The only thing, you can do, is enable all warnings and treat them like compiler errors.
0
-3
u/knouqs 2d ago
The idea for a lot of decisions in C from that time stemmed from space constraints and "That's how we did it in B." Memory was much more expensive and savings like not repeating the base type were cost-effective. Other decisions like having default fall-through for case blocks in switch statements are known design mistakes. It happens. We get to live with the effects of them and become better developers for the effort.
1
u/zzzthelastuser 2d ago
Memory was much more expensive and savings like not repeating the base type were cost-effective.
cost-effective how? It only saves a couple of bytes on source code. For the binary it makes no difference. I wouldn't even know how it should make the binary larger. The declaration doesn't generate code and the reserved space for the variables should still be n * sizeof(type) more or less...
1
u/Obvious_Set5239 2d ago
cost-effective how? It only saves a couple of bytes on source code
It was actually an issue. I have tandy 102, a laptop from 1986. And it's a real issue that you can run out of space for text files. For basic source code it converts it to and from tokenized format when you open of save files, exactly for this reason
-1
u/knouqs 2d ago edited 2d ago
For starters, https://ourworldindata.org/grapher/historical-cost-of-computer-memory-and-storage?time=1970..latest shows the cost of memory (I've ranged it to start in 1970, when C would have been developed) over time, adjusted to 2000-level costs. $4.90 per byte in 1970, about $1/byte in 1977. Any extra character you type cost real money back then.
Yes, this argues against the "break" consideration, but again, that was a bad design choice.
For a real-world example of the effects of the costs of things, the Y2K problem was rooted in the expense of storage space. If they could save two bytes of data, companies would save millions of dollars. They did, thinking all their code would be obsoleted and replaced long before 2000, when it would be a problem.
Most of the code was, in fact, still in use in 2000, causing the panic that people experienced in 1998 and until January 1, 2000.
50
u/mr_seeker 2d ago
That's why you should not more than one per line: