r/cpp 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

0 Upvotes

67 comments sorted by

50

u/mr_seeker 2d ago

That's why you should not more than one per line:

C++ Core Guidelines
ES.10 — “Declare one name (only) per declaration”

24

u/LB-- Professional+Hobbyist 2d ago

When I'm feeling lazy I do std::type_identity_t<int*> a, b, c, d, e;

6

u/James20k P2005R0 2d ago

right_to_jail.gif

4

u/LB-- Professional+Hobbyist 2d ago

std::type_identity_t<int (int, int)> a, &b{a}, *c{b};

2

u/[deleted] 1d ago

[deleted]

1

u/LB-- Professional+Hobbyist 1d ago

Huh I never knew you could use decltype on plain types directly like that, neat! I thought it always had to be used on (unevaluated) expressions.

2

u/fdwr fdwr@github 🔍 1d ago edited 1d ago

Doh, I'm mistaken - that indeed doesn't work. Deleting, but 2 other options include: std::type_identity_t<int*> a, b, c; std::add_pointer_t<int> a, b, c; decltype((int*)nullptr) a, b, c; // eww :b

2

u/LB-- Professional+Hobbyist 1d ago

Ahh ok, good to know. Yeah, add_pointer_t is another one I use when lazy sometimes, but I feel worse about it.

1

u/Obvious_Set5239 2d ago

It works! 🤣 Nice way to deal with this situation

2

u/Obvious_Set5239 2d ago

Also directly on int* ptr; vs int *ptr;:

  • "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

And also existence of using as a replacement of typedef highlights the different idea that declaration doesn't follow use in C++:

  • "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.

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

u/Total-Box-5169 1d ago

Nice way to explain it, all those evaluate to int.

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 just x,*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 int and an int* 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

u/JVApen Clever is an insult, not a compliment. - T. Winters 2d ago

They can't, it breaks older code. Though, maybe we could get a compiler warning for it?

1

u/Beneficial_Steak_945 2d ago

We have deprecated things before, like ‘throw()’.

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_ptr and std::shared_ptr, there should also be simple std::ptr for completeness, so you can say safely use std::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? :P

1

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

u/QuirkyXoo 2d ago

No, it's not weird or a bad idea, it's just you not knowing the C syntax.

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/Raknarg 1d ago

ah yeah. I ran into that bug once and ever since I have never done a compound declaration statement. If it wasn't for the legacy code that would break I would say they should just make it illegal at this point lol

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 write int *ptr. Apparently two teams have different opinions

1

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 😅

0

u/AKostur 2d ago

Neither: "int * a;". And never declare more than one variable on a line.

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

u/kevleyski 2d ago

Common bug and why better to separate definitions b needs *

-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.

8

u/STL MSVC STL Dev 2d ago

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.

This was most certainly not the reason.

0

u/[deleted] 2d ago

[deleted]

1

u/Obvious_Set5239 2d ago

My post is not about this

-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.