Hacker News new | past | comments | ask | show | jobs | submit login
C++26: A Placeholder with No Name (sandordargo.com)
92 points by jandeboevrie 1 day ago | hide | past | favorite | 161 comments





I am so shocked at how many people use `auto` in C++. I can not think of a worse thing to do to your code in terms of readability and future maintainability. Maybe it is OK if you use an IDE to identify types for you but I still hate it. I am trying to learn a new library right now with light documentation which means reading the code, and between typedefs, auto, and technical debt, it is a tedious exercise to figure out something's type, to go look up its function, to see what type that is returning.

I have never seen anyone come back to typing types everywhere after using auto for more than a couple months.

Use types when they are needed and use the tools at your disposal (IDEs BT every text editor has clang language server integration nowadays)

> with light documentation which means reading the code,

You have to read the code anyways, documentation is impossible to trust. There isn't one big library for which I didn't have to go read the code at some point. Two weeks ago I had to go read the internals of msvcrt, Microsoft's C runtime, to understand undocumented features of process setup on windows. I had to go read standard library code thousands of times, and let's not talk about UI libraries.


> Use types when they are needed and use the tools at your disposal (IDEs BT every text editor has clang language server integration nowadays)

While I agree that auto is helpful, the amount of times I had to wait for clangd (or whatever the IDE is using) to parse a .cpp file and deduce the underlying type is frustrating. It happens too often with every IDE (Qt Creator, CLion, VS Studio, VS Code, etc...) I've tried whenever I'm programming with a non-desktop machine that's not super beefy.

Plus I often use Github to search for code when I'm trying out a new lib so having the type spelled out is extremely helpful.


Plus assuming that your codebase is clangd parseable. Many aren't out of the box.

Plus assuming that you're not reading/modifying code on a (remote) machine where you don't have access to IDEs but simple editors only.

Otherwise, I also find auto helpful but I use it sparingly, mostly where type is obvious, e.g. can be easily deducted from the local scope.


One approach is use auto while coding, auto convert to full types when finalizing/commiting except where they hurt readability.

There's so much redundancy built into the language if you don't. Imagine:

    std::shared_ptr<T> p = std::make_shared<T>();
Then replace T with a very long type. And that's not the most verbose example, just an early one that popped into mind.

Then you have lambdas. Imagine assigning a lambda into a stack variable without auto, also keeping in mind that std::function adds overhead.


That example isn't what OP is talking about, because it's obvious what the type of p is because it's on the same line:

    auto p = std::make_shared<T>();
whereas the following isn't clear and isn't necessarily correct without looking up what the return type of foo() actually is:

    auto p = foo();

I'll agree that your second example is less readable than the first..

This could be mitigated with the name of foo() being more descriptive.

If the return type is particularly wordy, auto could still be appropriate.


> This could be mitigated with the name of foo() being more descriptive.

welcome back Hungarian notation


auto conn = createConnection();

What does this have to do with hungarian notation?


Alone this looks like a reasonable use of auto. In a real codebase, there may be two (or twenty) different connection-like things, multiple of which may be reasonably to call in this context.

The "Hungarian notation" comment is correct - it's not strictly Hungarian notation, but annotating function (or variable) names when the language has a type system representing this same information is the same idea with the same problems as Hungarian notation.


You’re thinking of it wrong.

This works better:

auto uasStudents = getClassList()

In this case, “uas” prefix standing for an unsafe array of strings.

Then say you validate the list

auto sasStudents = validate(uasStudents)

(Now it’s a safe array of students!)


I feel like I’m missing a joke.

lpwszThanks.

In seriousness, no, that's not what I'm suggesting, and I find it an unusual thing to read from my comment. I'm saying a descriptive name for foo() can give you a hint about what the type is, even if it doesn't literally and directly tell you what the type is.


this is required for writing generic code when the function may return different types

The auto keyword should not go in a public API, but internally it's very useful especially when you create objects like `auto ob = make_unique<VeryLongClassName>(...);` or any other kind of function call where the type is obvious and would be identical on both sides of an assignment.

As for your particular issue, using an IDE is essential, and the typedef keyword is almost obsolete, so I guess you stumbled upon a strange project. I would be curious to know what it is if it's open-source.


It is Slang. A very cool project and it only got its public release relatively recently, so some sins are forgiven but there are so many typedefs.

    typedef struct ShaderReflection ProgramLayout;
    typedef enum SlangReflectionGenericArgType GenericArgType;
https://github.com/shader-slang/slang/blob/master/include/sl...

It looks like C++98 to me: no `pragma once`, `typedef uint32_t SlangUInt32` seems strange, `typedef bool SlangBool` is definitely useless. The auto keyword is the least of my problems here.

> years of collaboration between researchers at NVIDIA, Carnegie Mellon University, Stanford, MIT, UCSD and the University of Washington

Now I understand why, it's the kind of project that you can't upgrade easily.


`#pragma once` is not a panacea, it can still lead to double inclusion in scenarios where the same include file is accessible under different path names (granted, that's a very esoteric scenario). Besides, `#pragma once` is neither part of the C nor C++ standard. It's just a common convention between compiler vendors - so technically any code that uses pragme once as the only include guard is not standard C or standard C++ (but tbf, hardly any real-world code is fully standard compliant).

Typedef'ing common types to your own type names is absolutely fine as long as it is unlikely to collide with the typedefs of other libraries in the same project.


> Typedef'ing common types to your own type names is absolutely fine as long as it is unlikely to collide with the typedefs of other libraries in the same project.

I read the parent post as indicating that "this is C++; we spell this as `using Foo = Bar;` now." Type aliases (or namespace aliases, or using-declarations) are not dead, but the typedef keyword in C++ is largely only retained for backwards compatibility.

The core issue here is that type aliases add a layer of indirection. That can be useful if the user shouldn't need to know the implementation details and can interact with said type purely through the API -- do I care if a returned Handle is a pointer, an int, or some weird custom class? Everyone is used to file descriptors being ints, but you aren't going to do math on them.


#pragma once is a de-facto standard now, and if it’s not in the actual standard then that says more about the failings of the standard writers than anything else.

And there’s absolutely no reason to typedef _standard_ int types anymore. Not in C, and definitely not in C++. That’s just crusty old practices. Maybe if you want to have nice short types like u8, i8, etc, I can understand that. But SlangUint32 is just ugly.


Pragma once isn't in the standard because there are cases where it doesn't work and to standardize it means making the standard significantly longer to catorgize when the compiler is allowed to not work. I've been in discussions and they all conclude it is not worth the effort

Pragma once is widely enough accepted that anyone who argues against its use for that purpose better either be able to tell me the compilers it doesn’t work on, or I’m going to assume they’re being pedantic for the hell of it.

And honestly, anyone who relies on the scenarios Pragma once fails in (Compiling off network shared and symlinks) should really fix those monstrous issues instead. The places Pragma once trips up in are likely to be issues for detecting modifications in incremental builds anyway.


There used to be a couple holdouts, but all major C++ compilers have supported it for years (https://en.wikipedia.org/wiki/Pragma_once).

Prarma once works in the vast majority of cases. However there are some rare ones where it fails. Every attempt to fix those last ones breaks some other rare case or profilings shows significant compile time increases.

use pragma once where it works in internal code but never in headers you ship to someone else is a simple rule that should work well enough.


> However there are some rare ones where it fails.

As I said in my previous comment, those cases are very very often cases where the compialtion model is broken, and it's held together by luck.

> Every attempt to fix those last ones breaks some other rare case

My experience (and I have experiences of this) have been that the cases where pragma once fails, other tools (source control, built tools) cause "random" problems that usually are hand waved away with "oh that problem, just run this other makefile target that someone has put together to clear that file and rebuild".

> or profilings shows significant compile time increases.

Again, my experience here has been that the compile time change is usually a result of a broken project model, and that there are other gotchas like "oh you can only include files a.h and c.h but if you include b.h then you'll break the internal build tool". Also, that taking the hit to make your build correct unlocks optimisations that weren't possible before.

Also, the projects that have these kinds of warts are usually allergic to doing anything new whatsoever, making any changes or improvements, upgrading compilers and libraries. I suspect using C++17 is enough to scare them off in many cases.

If it's good enough for QT or LLVM, it's good enough for me.


It isn't luck - I know exactly what I did to my package manage to get that file installed into two different locatian. I know exactly which -I options I passed to the compiler causes that one file to be found in different locations depending on quotes or brackets to be used. And it works great if I don't use pragma.

i also know why I had to do that aweful abuse to what anyone sane would call wholesome. I don't like it but there are other things going on and I don't want to talk about it anymore.


I've been using #pragma once in my C++ libraries for a decade and nobody ever reported any problem with that. I did get bug report about using perfectly standard C++ features that were not properly implemented in some compilers.

> the same include file is accessible under different path names (granted, that's a very esoteric scenario)

And most likely a build system problem anyway if, say, different versions of the same library get included in your build.


I use it and ran into issuses when I put a file into two different locations using my package manager. The reason I wanted a file in two different locations is something I don't want to talk about.

I agree that the auto keyword should be used sparingly. Things you mention like the output of make_unique and make_shared are a good exception since it is very clear what the resulting type is. Also, one might use auto to store a lambda in a local variable because it does not have a type that you can type.

I write my code with the assumption that the reader does _not_ have access to a "smart" IDE.

I use `auto` when the type is obvious or doesn't really matter, and I seldom create aliases for types.

I feel like having verbose type names at function boundaries and using `auto` for dependent types is the sweet spot. I'll often avoid `auto` when referring to a class data member, so the reader doesn't have to refer to the definition.

    void foo(const std::multimap<double, Order>& sells) {
        for (const auto& [price, order] : sells) {
            // ...
        }
    }
but also

    void foo(const OrderBook& book) {
        const std::multimap<double, Order>& sells = book.sells;
        for (const auto& [price, order] : sells) {
            // ...
        }
    }
`auto` is convenient for iterators. Which of the following is better?

    auto iter = sells.begin();
    std::multimap<double, Order>::const_iterator iter = sells.begin();

Off-topic, but just wanted to note that using floating-point numbers as keys may be generally a bad idea (unless you use a custom comparator that takes into account the error that can accumulate during calculations).

especially for an order book...

Every other language does that by default with "var", "let", or in some languages nothing at all. Within functions it doesn't matter that much and using an IDE takes care of quick lookups anyway.

Even for the non ide folks - vim emacs and vs code all have excellent support for that.

How does vim support this? I thought you had to use custom scripts/extensions to do it?

Sorry I wasn’t clear - all those editors have simple (ish) plugins that support it

Wasn’t typescript created to fix this problem for JS?

Using `let` does not make the expression un-typed in Typescript. It means the type is inferred, and you'll get type warnings if you use it where a different type is expected.

There’s a lot of arguments here where people are saying basically, “auto is bad because you can …” it “auto is great because you can …”, as if the two are mutually exclusive or something.

It’s like saying “knives are bad because you can kill someone” vs “knives are good because they can help make food”… nobody thinks of knives as being an exclusively good or exclusively bad thing; we all understand that context is key, and without context it’s meaningless.

Instead I feel it would be a lot more illuminating if the discussion centered around rules of thumb… which contexts auto is good, vs when it’s bad. There’s probably no complete list, but a few heuristics would be great.

My 2¢:

Explicit type declaration is a form of documentation, used to tell the casual reader (ie. Often in a web browser, code review, or someone seeing it copy/pasted as a snippet[0]) the meaning of a piece of code. It’s even better than comments: it’s guaranteed not to be a lie, or the code wouldn’t compile.

I’ve seen this all the time working in Rust, Swift, typescript, etc… sometimes an expression is super complicated, and the type checker can infer the type just fine, and my IDE even shows the type in an inlay… but I still worry that if these weren’t available, the expression would look super confusing to a reader. So I make the type explicit to aid in overall readability.

When to do this or not is a judgement call… different people will come to different conclusions. But it’s like any other form of line-level documentation. Sometimes the code is self explanatory, and sometimes it’s not. But be kind to the casual reader and use explicit types when it’s very non-obvious what the type would be.

[0] ie. Anyone without immediate access to an IDE or something else that would show the type.


> we all understand that context is key

Unfortunately I think this either this isn't actually the case for many people, or too often they just never even stop to consider that other perspectives might be possible, better or even more common than their own.

In chatting with technical people online for the last 30 years, the biggest issue I have always had is their attitude. IRC seems the worst for it but every platform has this problem in my experience.

God complexes visible from space run rampant, people always speaking in absolutes and seeing things as black and white, complete lack of empathy and humility etc.

I think most arguments in the world, and even other things like crime, might actually just stem from people's inability to communicate with each other effectively.


It's not 1993. IDEs tell you the type if you hover over the auto. Or control and click takes you to the type definition.

You have to weigh up the cost of going through the code and changing all the type declarations

    auto x = foo();
If you change the return type of foo here you don't have to change 300 call sites. Personally i'd rather change it in one place than 300.

What about code reviewers? Show me a code review system that lets you hover over the value to see the type… none of the ones I’ve used can do it.

For that matter anyone reading the code from a web browser in any other context.


Fwiw, I agree. I also pull down the branch under review in parallel to the web code review. You’d (probably) not be surprised by the number of times I’ve done this and the code doesn’t even build.

Even without auto you have the problem.

    return foo().bar();
No `auto` and you still don't know the return type of foo. And knowing the type might not be the only reason you'd want an IDE anyway. What is `foo()` doing? I want to be able to easily jump to the definition of that function to verify that the assumption taken by the calling function are correct.

This was probably rhetorical but metas code review tool runs LSP and gives you clickable types where clicking takes you to the definition.

I wonder if most of the reason people use auto is just to save time when typing... if the IDE could auto-resolve the type in the source code when they use auto... would that be a better compromise?

Editor type deduction is surprisingly unreliable sometimes.

Herb Sutter has a pretty good explanation: https://herbsutter.com/2013/08/12/gotw-94-solution-aaa-style...

‘auto’ (in C++) and ‘var’ (in C# and Java) is a blessing, makes code much less verbose. Also good for refactoring - less code to change.

I’m only a C++ amateur, but IMHO C++ vs C#/Java isn’t really a fair comparison here—the latter doesn’t have template shenanigans and so types are much more transparent to the reader (by which I mean that you don’t have to execute a dynamically-typed program in your head to get from the term on the right-hand side to the type on the left).

Verbosity is not bad. When it makes the code clearer, it is even a good thing.

Complex type parameters make explicit typing highly impractical to be used all the times

I like rust's approach in that it allows a mixture of explicit types and type inference using placeholders

For example: "let x : Result<Foo<int, _>, _> = make_foo();"


Complicated template types, where you have a general idea of the type but you don't want or need to spell it all out (it might be very long) when the compiler can easily do it for you.

Auto is preferred for assignment because it eliminates a whole class of errors involving unintentional construction. Dropping a const is the conanical example.

Auto in a function signature is syntactic sugar for introducing a template parameter. It needs to be monomorphized at some point to generate code.


I can certainly relate to this experience. I remember when it was introduced, I was very wary of getting too much 'auto' into the codebase, certainly as the team could be a bit 'gung-ho' adopting stuff just because it was there.

However, in hindsight, I think I was being overly conservative, and it worked out well, and adoption didn't cause any obvious problems.

Your concerns about learning a new library are valid, but the problem is the library if it's not clear, or well documented. To lay responsibility for this at the door of auto is a stretch. You can write great and terrible code with a number of language features (dubious use of goto is the classic example), and it sounds like you are tackling a library which could do with some love, either in documentation, or to clarify it's conventions.


My rule of thumb is to use auto only when the type is obvious from the context. I think it's a sane compromise between readability and non-verbosity.

Programming without IDE is so 1970's...

Having said this, I usually only use type inference when the types are obvious from context.


Was this tongue in cheek? If you _can_ use inference it was at least obvious enough to the compiler. Otherwise you're just saying "I use it when I feel like it."

   auto x = func(); // no idea about func return type

   auto x = new Widget(); // DRY

   auto sum (auto a, auto b); //  template function without boilerplate 
Use the same principle in other contexts.

Im as shocked as you are that people rely on textual representations and ignore all the powerful tooling available to them for understanding code.

Every editor I use has tools that will provide this information in a single keystroke, macro or cluck. If you actively choose to avoid using tools to read code, I shouldn’t suffer for it.


if you need tools it just means the code is sub-par

That take is so comically nonsensical I question why you’d even contribute it.

I have no doubt you read and write code without any tools.


Grep is a tool. But if I grep foo, it can’t tell me the difference between a function called foo, a variable called foo, or any other types that may have a foo, or a comment with foo in it. Even vscode can do “show me all uses of foo” in a single click, and be perfectly correct,

It's not perfectly correct, and that actually what makes it dangerous.

One area where auto is necessary is in coroutines. The types are so hideous and abstract that writing them out is guaranteed to be less readable than using auto and accepting the your types are some blend of compiler derived and coroutine library magic.

> I am so shocked at how many people use `auto` in C++

Well I blame C++ for calling it "auto" in the first place. Fortunately this is easily fixed:

    #define let auto
    #define var auto
;-)

Using auto in function parameters to have implicit templates is very cursed

I'm pretty supportive of auto and var, etc. in languages but parameters seem like a step too far.

> I am so shocked at how many people use `auto` in C++.

I agree with you! But:

> I can not think of a worse thing to do to your code in terms of readability and future maintainability.

Well, I definitely can. Using macros is one ;)

> I am trying to learn a new library right now with light documentation which means reading the code, and between typedefs, auto

I disagree with you on the typedefs. They're much better than auto. Auto doesn't provide any type checking, it works whatever the type is. Typedef tell you what the expected type actually is.


Like everything in life, it has to be used in moderation.

auto it = data.begin();

Is a lot more readable than

std::vector<std::pair<std::vector<foo>, int>::iterator it = ....


Oh boy! A vector of tuples of vectors and ints? Crazy

I don't know why I feel the urge to point this out but you missed an angle bracket :D

My take is that `auto` is basically a tool to reduce local redundancies rather than typing convenience. Rule of thumb: you should avoid `auto` unless it actually improves readability (e.g. significant reductions of syntactic redundancies), or there is no other option.


Isn't inline more "undeterministic" than auto? That is way older and used everywhere.

I'd like auto functions.


Isn't it mostly meaningless outside of the syntax sugar of putting code in the header?

I am in a middle ground. Usually do not use auto but in the cases like:

   for (auto member : set_of_members)
and some other that are similar by nature auto is a god blessing.

Except this makes a copy of each member in set_of_members, which is probably not what you want.

https://godbolt.org/z/1YnEs1M34


This was to illustrate a point of auto rather than intricacies of copying, referencing. I know what I want and am familiar with auto&, const auto&, auto&& etc. etc.

> it is a tedious exercise to figure out something's type, to go look up its function, to see what type that is returning

To cite your previous sentence, why don't you use your IDE?

Or is this a magnetized needle sort of situation.


Some kinky C++ programmers have such a sexual fetish for using `auto` that they enjoy holding their breath as long as possible while writing code, before ever declaring any explicit type names. That's called auto-erotic asphyxiation!

   int wtf = omgtype(); // and read the compiler error

Honestly, use a good IDE.

Jetbrains can annotate your source with what the actual type is.

and auto can help future maintainability if you need to change concrete types that have the same API surface.


If you don't use an IDE, you are doing it wrong, plain and simple.

Editing png with a text editor is also much harder than editing ppm. But there is no reason to consider this usecase when defining a image format.


You should never write code that's impossible to understand without fancy IDE features. If you're writing such code, the best thing you can do for yourself long term is switch to a text editor without LSP (read Notepad) right now, which will force you to start writing sane code.

This is true for any language, but it's especially true for C++, where most large codebases have tons of invisible code flying around - implicit casts, weird overloads, destructors, all of these possibly virtual calls, possibly over type-erased objects accessed accessed via smart pointers, possibly over many threads - if you want to stand any chance of even beginning to reason about all that you NEED to see the actual, concrete, scientific types of things.


While the C language has a lot of bad implicit casts that should have never been allowed, mainly those involving unsigned types, and which have been inherited by its derivatives, implicit casts as a programming language feature are extremely useful when used in the right way.

Implicit casts are the only reason for the existence of the object-oriented programming languages, where any object can be implicitly cast to any type from which it inherits, so it can be passed as an argument to any function that expects an argument of that type, including member functions.

The whole purpose of inheritance is to allow the programmer to use implicit casts. Otherwise, one would just declare a structure member of the class from which one would inherit in the OOP style and a virtual function table pointer, and one could write an identical program with the OOP program, but in a much more verbose way.

(In the C language, not only the implicit mixed signed-unsigned casts are bad, but also any implicit unsigned-unsigned casts are bad, because there are 2 interpretations of "unsigned" frequently used in programs, as either non-negative numbers or as modular numbers, and the direction of the casts that do not lose information is reversed for the 2 interpretations, i.e. for non-negative numbers it is safe to cast only to a wider type, but for modular numbers it is safe to cast only to a narrower type. Moreover, there are also other interpretations of "unsigned", i.e. as binary polynomials or as binary polynomial residues, which cannot be inter-converted with numbers. For all these 4 interpretations, there are distinct machine instructions in the instruction sets of popular CPUs, e.g. in the x86-64 and Aarch64 ISAs, which may be used in C programs through compiler intrinsics. Even worse is that the latest C standards specify that the overflow behavior of "unsigned" is that of modular numbers, while the implicit casts of "unsigned" are those of non-negative numbers. This inconsistency guarantees the existence of perfectly legal C programs, without any undefined behavior, but which nonetheless compute incorrect "unsigned" values, regardless which interpretation was intended for "unsigned".)


> Otherwise, one would just declare a structure member of the class from which one would inherit in the OOP style and a virtual function table pointer, and one could write an identical program with the OOP program, but in a much more verbose way.

No, you don't have to do that. Once you start thinking about memory and manually managing it, it you'll figure out there's simpler, better ways to structure your program, rather than having a deep class hierarchy with a gazillion heap-allocated objects, each with distinct lifetime, all pointing at each other.

Here's a trivial example. Say you're writing a JSON parser - if you approach it with an OOP mindset, you would probably make a JSONValue class, maybe subclass it with JSONNumber/String/Object/Array. You would walk over the input string and heap allocate JSONValues as you go. The problems with this are:

    1. Each allocation can be very slow as it can enter the kernel
    2. Each allocation is a possible failure point, so the number of failure points scales linearly with input size.
    3. When you free the structure, you must walk over the entire tree and free each obejct one by one.
    4. The output of this function is suboptimal as the memory allocator can return values that are far away in memory.
There's an alternate approach that solves all these problems. If you're thinking about the lifetimes of your data, you would notice that this entire data structure is used and discarded at once, so you allocate a single big buffer for all the nodes. You keep a pointer to the head of that buffer, and when you need a new node, you stick it in there and advance the pointer by its size. When you're done you return the first node, which also happens to be the start of the buffer.

Now you have a single point of failure - the buffer allocation, your program is way faster, you only need to free one thing when you're done, and your values are tightly packed in memory, so whatever is using its output will be faster as well. You've spent just a little time thinking about memory and now you have a vastly superior program in every single aspect, and you're happy.


Memory arenas are a nice concept but I wouldn't say they're necessarily an improvement in every possible situation. They increase complexity, make reasoning about the code and lifetimes harder and can lead to very nasty memory bugs. Definitely something to use with caution and not just blindly by default.

Reasoning about the lifetimes of objects in an arena is as simple as it gets - there's only one lifetime, and pointers between everything allocated on the arena are perfectly safe. The complexity of figuring out what's going on, with with respect to the number of objects and links between is O(1).

There's no universal "God pattern" that you can throw at every problem. I used arenas as an example as I didn't want to write a zero-substance "OOP bad" post, but my point wasn't that instead of always using OOP+inheritance you should always use an arena, it was that if you think about your memory, more often than not there's a vastly superior layout than a bunch of heap objects glued together by prayers and smart pointers.


That's all nice and fun until you want to pass stuff around and some objects might outlive the arena. Do you keep the whole arena around, do you copy, do you forget to do anything at all and spend a few days debugging weird memory bugs in prod?

"Non-negative" unsigneds can be validly cast to smaller types. That's why saturating_cast() exists. There are modular numbers where casting to a smaller value is likewise unsafe at a logical level. Your LCRNG won't give you the right period when downcast, even if the modulus value is unchanged.

inheritance isn't required for object oriented programming. the primary facet of oop is hiding implementation details behind functions that manipulate that data.

adding values to a dict via add() and removing them via remove() should not expose to the caller if the underlying implementation is an array of hash indexed linked lists or what. the implementation can be changed safely.

inheritance is orthogonal to object orientation. or rather, inheritance requires oop, but oop does not require inheritance.

golang lacks inheritance while remaining oop, for instance, instead using interfaces that allows any type implicitly defining the specified interface to be used the.


"Hiding implementation details" means the same as "hiding the actual data type of an object", which means the same as "performing an implicit cast whenever the object is passed as an argument to a function".

Using different words does not necessarily designate different things. Most things that are promoted at a certain time by fashions, like OOP, abuse terminology by giving new names to old things in the attempt of appearing more revolutionary than they really are.

Most classic works about OOP define OOP by the use of inheritance and of virtual functions a.k.a. dynamic polymorphism. Both features have been introduced by SIMULA 67 and popularized by Smalltalk, the grandparents of all OOP languages.

When these 2 features are removed, what remains from OOP are the so-called abstract data types, like in CLU or Alphard, where you have data types that are defined by the list of functions that can process values of that type, but without inheritance and with only static polymorphism (a.k.a. overloading).

The example given by you for hiding an implementation is not OOP, but it is just the plain use of modules, like in the early versions of Ada, Mesa or Modula, which did not have any OOP features, but they had modules, which can export types or functions whose implementations are hidden.

Because all 3 programming language concepts, modules, abstract data types and OOP have as an important goal preventing the access to implementation details, there is some overlap between them, but they are nonetheless distinct enough so that they should not be confused.

Modules are the most general mechanism for hiding implementation details, so they should have been included in any programming language, but the authors of most OOP languages, especially in the past, have believed that the hiding provided by granting access to private structure a.k.a. class members only to member functions is good enough for this purpose. However this leads sometimes to awkward programs where some classes are defined only for the purpose of hiding things, for which real modules would have been more convenient, so many more recent versions of OOP languages have added modules in some form or another.


1994 "Object-Oriented Programming in Oberon-2" seems to cover what you discuss.

https://ssw.jku.at/Research/Books/Oberon2.pdf


I'll readily admit the languages were marketed that way, but would argue inheritance was a functional, but poor, imitation of dynamic message dispatch. Interfaces, structural typing, or even simply swapping out object types in a language with dynamic types does better for enabling function-based message passing than inheritance does, as they avoid the myriad pitfalls and limitations associated with the technique.

Dynamic dispatch can be accomplished in any language with a function type by using a structure full of functions to dispatch the incoming invocations, as Linux does in C to implement its file systems.


I am actually ok with the conversions and C and think they are quite convenient. Unsigned in C is modular. I am not sure what you mean by the "latest C standards specify". This did not change. I also do not understand what you mean by the "implicit cast of unsigned are those of non-negative numbers". This seems wrong. If you convert to a larger unsigned type, the value is unchanged and if you convert to a smaller, it is reduced modulo.

In older C standards, the overflow of unsigned numbers was undefined.

In recent C standards, it has been defined that unsigned numbers behave with respect to the arithmetic operations as modular numbers, which never overflow.

The implicit casts of C unsigned numbers are from narrower to wider types, e.g. from "unsigned short" to "unsigned" or from "unsigned" to "unsigned long".

These implicit casts are correct for non-negative numbers, because all values that can be represented as e.g. "unsigned short" are included among those represented by "unsigned" and they are preserved by the implicit casts.

However, these implicit casts are incorrect for modular numbers, because they attempt to compute the inverse of a non-invertible function.

For instance, if you have an "unsigned char" that is a modular number with the value "3", it is incorrect to convert it to an "unsigned short" modular number with the value "3", because the same "unsigned char" "3" corresponds also to 255 other "unsigned short" values, i.e. to 259, 515, 781, 1027 and so on.

If you have some very weird reason when you want to convert a number modulo 256 to a number modulo 65536 by choosing a certain number among those with the same residue modulo 256, then you must do this explicitly, because it is not an information-preserving conversion.

If on the other hand you interpret a C "unsigned" as a non-negative number, then the implicit casts are OK, but you must add everywhere explicit checks for unsigned overflow around the arithmetic operations, otherwise you will obtain erroneous results.


The C89 standard has "A computation involving unsigned operands can never overflou. because a result that cannot be represented b! the resulting unsigned integer type is reduced modulo the number that is one greater thnn the largest value that can be represented by the resulting unsipned integer type" (OCR errors) You can finde a copy here: https://web.archive.org/web/20200909074736if_/https://www.pd...

Mathematically, there is no clearly defined way how one would have to map from one residue system in modular arithmetic to the next, so there is no "correct" or "incorrect" way. Mapping to the smallest integer in the equivalency class makes a lot of sense though, as it maps corresponding integers to itself when going to a larger type and and the reverse operation is then the inverse, and this is exactly what C does.


> You should never write code that's impossible to understand without fancy IDE features

with Rust that ship has sailed


I code Rust just fine without any fancy IDE you should give it a shot. The languages I find hardest to code without fancy IDE features are C and C++ due to their implicit casts. Rust is typically easy to code without IDE features due to its strong type system, lifetimes and few implicit casts.

Rust is one of my favorite new languages, but this is just wrong.

> few implicit casts

Just because it doesn't (often) implicitly convert/pun raw types doesn't mean it has "few implicit casts". Rust has large amounts implicit conversion behavior (e.g. deref coercion, implicit into), and semi-implicit behavior (e.g. even regular explicit ".into()" distances conversion behavior and the target type in code). The affordances offered by these features are significant--I like using them in many cases--but it's not exactly turning over a new leaf re: explicitness.

Without good editor support for e.g. figuring out which "into" implementation is being called by a "return x.into()" statement, working in large and unfamiliar Rust codebases can be just as much of a chore as rawdogging C++ in no-plugins vim.

Like so many Rust features, it's not breaking with specific semantics available in prior languages in its niche (C++); rather, it's providing the same or similar semantics in a much more consciously designed and user focused way.

> lifetimes

How do lifetimes help (or interact with) IDE-less coding friendliness? These seem orthogonal to me.

Lastly, I think Rust macros are the best pro-IDE argument here. Compared to C/C++, the lower effort required (and higher quality of tooling available) to quickly expand or parse Rust macros means that IDE support for macro-heavy code tends to be much better, and much better out of the box without editor customization, in Rust. That's not an endorsement of macro-everything-all-the-time, just an observation re: IDE support.


Have you actually tried coding Rust without IDE support? I have. I code C and Rust professionally with basically only syntax highlighting.

As for how lifetimes help? One of the more annoying parts of coding C is to constantly have to look up who owns a returned pointer. Should it be freed or not?

And I do not find into() to be an issue in practice.


Not everyone reading your code will be using an IDE. People may be passively searching your code on GitHub/gerrit/codesearch.

val/var/let/auto declarations destroy the locality of understanding of a variable declaration without an IDE + a required jump-to-definition of a naive code reader. Also, a corollary of this problem also exists: if you don’t have an explicit type hint in a variable declaration, even readers that are using an IDE have to do TWO jump-to-definition actions to read the source of the variable type.

eg.

val foo = generateFoo()

Where generateFoo() has the signature fun generateFoo(): Foo

With the above code one would have to jump to definition on generateFoo, then jump to definition on Foo to understand what Foo is. In a language that requires the explicit type hint at declaration, this is only one step.

There’s a tradeoff here between pleasantries while writing the code vs less immediate local understanding of future readers / maintainers. It really bothers me when a ktlint plugin actually fails a compilation because a code author threw in an “unnecessary” type hint for clarity.

Related (but not directly addressing auto declarations): “Greppability is an underrated code metric”: https://morizbuesing.com/blog/greppability-code-metric/


If you accept f(g()), you've already accepted that the type of every expression is not written down.

I don’t particularly accept f(g()). I like languages that require argument labels (obj-c, swift). I would welcome a language that required them for return values as well. I’d even enjoy a compiler that injected omitted ones on each build, so you can opt to type quickly while leaning on the compiler for clarity beyond build time.

Argument labels are equivalent to variable names. You still have them with auto. In either case you don't see the actual type.

I do not agree that using an IDE matters.

If you cannot recognize the type of an expression that is assigned to a variable, you do not understand the program you are reading, so you must search its symbols anyway.

Writing redundantly the type when declaring the variable is of no help when you do not know whether the left hand side expression has the same type.

When reading any code base with which you are not familiar, you must not use a bad text editor, but either a good text editor designed for programmers or any other tool that allows fast searching for the definitions of any symbols encountered in the source text.

Adding useless redundancy to the source text only bloats it, making reading more difficult, not easier.

I never use an IDE, but I always use good programming language aware text editors.


The argument is tautological.

I want to use a text editor => This is the wrong tool => Yes, but I want to use a text editor.

These people do use the wrong tooling. The only way to cure this grievance is to use proper tooling.

The github webui has some ide features, such as symbol search. I don't see any reason why not use a proper ide. github.dev is a simple click in the ui away. When you use gerrit, do a local checkout, that's one git command.

If you refuse to use the correct tools for the job, your experience is degraded. I don't see a reason to consider this case when writing code.


Have you ever worked in a large organization with many environments? You may find yourself with a particular interface that you don’t know how to use. You search the central code search tool for usages. Some other team IS using the API, but in a completely different environment and programming language, and they require special hardware in their test loop, and they’re located in Shanghai. It will take you weeks to months to replicate their setup. But your goal is to just understand how to use your version of the same API. This is incredibly common in big companies. If you’re in a small org with limited environments it’s less of an issue.

If you are unable to find the type of a right-hand-side expression that appears in an assignment or initialization, then the environment does not allow you to work and it must be changed.

The redundant writing of the type on the left-hand side does not help you, because without knowing the type of the right-hand side you cannot recognize a bug. Not specifying the type on the left-hand side can actually avoid many bugs in complex environments, because there is no need to update the code that uses some API, whenever someone changes the type of the result, unless the new type causes some type mismatch error elsewhere, where it would be reported, allowing to make fixes at the right locations in the source code, not at the spurious locations of variable definitions, where updating the type will not prevent the real bugs to occur at the points of use of that variable.

The only programming languages that could be used without the ability of searching the definition of any symbol, were the early versions of FORTRAN and BASIC, where the type of a symbol was encoded in the name of the symbol, by using a one-letter prefix in FORTRAN (like IVAR vs. XVAR) and a one-symbol suffix in BASIC (like X vs. X$ vs. X%).

The "Hungarian" convention for names used in early Microsoft Windows has been another attempt of encoding the types of the symbols in their names, following the early FORTRAN and BASIC style, but most software developers have disliked this verbosity.


I have worked in big environments. My idea about "big" might be naive, environments spanning different Oses and different, including old languages like fortran and pascal. But I never been in a situation where I couldn't check out said code, and open it in my ide and build it. If you can't that sounds like a another case of deficient tooling. Justifying deficient tooling.

These where not some SWE wonderlands either. The code was truly awful at times.

The Joel test is 25 years old. It's a industry standard. I, and many other people consider it a minimum requirement for software engineering. If code the "2. Can you make a build in one step?" requirement i should be ide-browsable in one step.

If it takes weeks to replicate a setup the whole environment is deeply flawed. The one-step build is the second point on the list because Joel considered it the second most important thing, out of 12.


My situation: hardware company, over 100 years old. I’ve found useful usage examples of pieces of software I need to use, but only on an OS we no longer ship, from a supplier we no longer have a relationship with, that runs on hardware that we no longer have. The people that know how to get the dev environment up are retired.

In those cases, I’m grateful for mildly less concise languages that are more explicit at call and declaration sites.


> if you don’t have an explicit type hint in a variable declaration, even readers that are using an IDE have to do TWO jump-to-definition actions to read the source of the variable type.

This isn’t necessarily the case. “Go to Definition” on the `val` goes to the definition of the deduced type in the IDEs and IDE-alikes I’ve ever used.


Can we turn down the dogmatism please? I think you will find that there are other equally valid perspectives if you look around, and that the world is not so black and white.

Makes me wonder when '_' was first used as a token to denote unused information. Prolog ? ML ?

If we're counting only programming languages (and not fill-out forms), Prolog had it in 1971, before ML. ASCII didn't include _ until its 1963 draft, so it's probably somewhere in that time.

I'll guess it's probably Prolog, but maybe Planner (Prolog's predecessor) had it too.


ASCII 1963 was the first version of the standard and it lacked _ https://dl.acm.org/doi/10.1145/366707.367524

The 1965 draft had _ https://dl.acm.org/doi/10.1145/363831.363839

The first standard edition with _ was 1968 https://www.rfc-editor.org/info/rfc20

The 1977 version is also available https://nvlpubs.nist.gov/nistpubs/Legacy/FIPS/fipspub1-2-197...


I think this predates computers entirely.

I think so, yes. English novels written in the 1800s would reference "Mr A------ from ------shire". As if they were redacting personally identifying information. (1)

Underscores and dashes are not that different - especially as this is not just pre-computer but pre-typewriter.

And now, underscores are a logical choice when the dash is already in use as a minus sign.

1) https://literature.stackexchange.com/questions/1944/why-are-...

https://forums.welltrainedmind.com/topic/141704-jane-austens...


Underscore has been used in programming for the first time by the language IBM PL/I, in 1964-12, where it replaced the hyphen/minus character that was used by COBOL to make more readable the long identifiers.

Replacing hyphen/minus with underscore has been done precisely for removing the ambiguity with the minus operator (In COBOL the ambiguity was less annoying, because the arithmetic operations were usually written with words, e.g. ADD, SUBTRACT, MULTIPLY and so on).


Good point, this is clearly something that could go back centuries.. if not all the way back to clay tablets.

What a convoluted mess.

Seems pretty clean to me? Do you mean the current state of affairs is a mess?

I don't think the one character solution introduced at the bottom of TFA is convoluted. Could you explain why you think the solution - introduced at the bottom of TFA - is convoluted?

My thought exactly. But that's C++, a bolted-on mess of crap whose redeeming features are (a) roughly the same execution performance as C and assembly, and (b) it's safer and higher productivity than C or assembly.

At this point, it feels like a matter of time before Rust replaces C/C++.


There are decades and decades of code written in cobol that run the modern banking system.

Multiply that code base size by like, 78.3, and you’re possibly in the same galaxy as the all the c++ codebases out there that will be maintained for the next 50 years.

Rust may eat the lunch of c++ moving forward, the language will never go away.


>the language will never go away

Just like COBOL! Seriously, _just like COBOL_. The language will fade in importance over time. C++ will be relegated to increasingly narrow niches. It will eventually only be found in "legacy" systems that no one wants to spend to rewrite. No one young will bother at all to learn C++ at all. Then, in a few decades, they'll be pulling folks like you and me out of retirement to maintain old systems for big bucks.


I doubt this analysis. The base of computing where C++ is used is exponentially larger than the base where COBOL was used. In particular the compilers we currently use are written in it.

Abso-fkn-lutely! Stable computing protocols of all sorts don’t go away overnight and that is something everyone in IT should absolutely get used to. I expect C/C++ to live long beyond 2050.

Tbf, Rust is catching up fast to become the same mess (especially in the stdlib).

> it's safer and higher productivity than C or assembly

Debatable - in C and even more so in assembly, the unsafety is at least directly in your face, while in C++ it's cleverly diguised under C++ stdlib interfaces (like dangling std::string_views, all the iterator invalidation scenarios etc... meaning that memory corruption bugs may be much harder to investigate in C++ than in C (or even assembly) - which in turn affects productivty (e.g. what's the point in writing higher level buggy code faster when debugging takes much longer).

> it feels like a matter of time before Rust replaces C/C++

It may replace C++, but C will most likely outlive at least C++, and I wouldn't be surprised if Rust too - even if just as C API wrappers to allow code written in different modern languages to talk to each other ;)


> Tbf, Rust is catching up fast to become the same mess (especially in the stdlib).

I've never seen any users who avoid using Rust's stdlib on principle. The closest thing is the use of the parking_lot crate over the standard Mutex types, but that's getting better over time, not worse, as the stdlib moves closer to parking_lot in both implementation and API. Other than that, there's only been one "wrong" API that needed to be deprecated with a vengeance (mem::uninitialized, replaced by mem::MaybeUninit). Especially compared to C++, the fact that Rust doesn't have a frozen ABI means it's free to improve the stdlib in ways that C++ can only dream of. While I do wish that the Rust stdlib contained some more things (e.g. a standard interface to system entropy, rather than making me pull in the getrandom crate), for what it provides the Rust stdlib is quite good.


I avoid the Rust stdlib. The stdlib uses panics and dynamic memory allocation, neither of which are great for embedded usecases or writing software adhering to safety-critical coding practices. no_std is common for embedded targets, and IIRC the linux kernel couldn't use stdlib because of panics.

Ironically c++ as well refused to define an ABI. That’s not why the language is bad. It’s the lack of editorial tools within the language the standard body has so it has no evolution path and thus ossifies under its own inertia. It’s amazing they still haven’t done anything about it.

> Rust is catching up fast to become the same mess (especially in the stdlib)

Care to provide examples?

I think they generally do a very good job at curating the APIs to feel very consistent. And with editions, they can fix almost any mistakes they make whereas c++ doesn’t have that. In fact, the situation in c++ is so bad that they can’t fix any number of issues in basic containers and refuse to introduce updated fixes because “it’s confusing”. The things they keep adding to the stdlib aren’t even core useful things that come out of the box. Like missing a networking API in 2025. The reason is they have to get it perfect because otherwise they can’t fix issues and they can’t get it perfect because networking is a continuously moving target. Indeed we managed to get a new tcp standard before c++ even though it had even worse ossification issues. Or not caring about the ecosystem enough to define a single dependency management system because the vendors can’t get agreement or a usable module system. Or macros being a hot mess in c++ some 20 years after it was already known to really suck.

Now it’s possible given enough time rust will acquire inconsistent warts over time similar to c++, but I just don’t think it’ll ever be as bad in the standard library due to editions and automated migration tools being easier against rust. Similarly, I think editions give them the flexibility to address even many language level issues. Inertia behind ways of doing things are the harder challenges that are shared between languages (eg adoption of Unsend might prevent the simpler Move trait from being explored and migrated to), but c++ is getting buried under its own weight because it’s not a maintainable language in terms of the standard because the stewards refuse to realize they don’t have the necessary editorial tools and keep not building themselves such tools (even namespace versions ended up being aborted and largely unused).


Just look at the Option type and all the methods on it, the whole concept of optionals should be built into the language, not stdlib:

https://doc.rust-lang.org/std/option/enum.Option.html

Same with Result, Box, Rc, Arc, Cell, RefCell, (plus even more *Cells), Iter, ... the resulting 'bread crumbs syntax' is IMHO the main reason why typical Rust code is so hard to read. This philosophy to push what should be language features into the stdlib is the same as in C++ (and IMHO one of the main problems of C++), it's probably also a reason why both languages are so slow to compile.


> At this point, it feels like a matter of time before Rust replaces C/C++.

I expect Rust's successor might have a shot at replacing C++.

By the time C++ was as old as Rust, it had conquered the world. If Rust coulda, it woulda.


I think you're right.

As soon as you start writing big(ger) software in Rust, its lacking ergonomics really become apparent.

.as_mut().unwrap().unwrap().as_mut().do_stuff() gets really old after a while.

And I am not convinced that borrow checking is the panacea that it's made out to be. Region-based memory management can accomplish the same safety goals (provided there's a sane concurrency model), without forcing people into manually opting into boxing and/or reference counting on a per-reference basis.

Throw into that the pain of manual lifetime management (it's not always elided, and when it needs to change, it's painful), I honestly believe it's far more reasonable to ask programmers to throw shit into two or three arenas and then go hog-wild with references than it is to expect them to deal with the tediousness of the way Rust does things.

We are just cargo-culters (no pun intended).


I find the C vs C++ battle amusing after having lived in the 90s and the Pascal/Delphi vs C/C++ holy wars.

Rust is a C++ replacement, but not a C replacement, for many C use cases. Language that may replace C is Zig.

What are those use cases?

C++ is safer than C? How?

You can write whole applications that compiles to the same assembly code without using any kind of memory management thanks to the destructors, smart pointers, and all the objects of the STL.

This just pushes the problem into the C++ stdlib, which has plenty of memory-corruption issues too but just calls it 'undefined behaviour' (see things like dangling std::string_view, missing range checks in containers or iterator invalidation). In C you avoid those issues by reducing dynamic memory allocations to a minimum, prefering value types over reference types and using function APIs as 'safety boundaries' (instead of directly poking around in random data). Different approach, but not any less safe than C++.

> In C you avoid those issues by reducing dynamic memory allocations to a minimum, prefering value types over reference types and using function APIs as 'safety boundaries'

"In Rust that's just pushing the problem to the borrow checker and codegen which has plenty of memory corruption issues too but just calls it "bugs". In C++ you avoid those issues by reducing dynamic memory allocations to a minimum, and using checked APIs as 'safety boundaries' instead of directly poking around random arrays. Different approach but not any less safe than C++".

Both statements are pretty ridiculous. It's pretty clear that moving up in terms of the safe abstractions that are possible makes your code safer because the safety is there when you reach for that kind of programming paradigm & the programming paradigms you need are typically a property of the problem domain rather than the language (it's it's the intersection of "how is this problem solved" and "what tools does the language give you"). In C it gives you few tools and you either twist into a pretzel trying to stick to that or you write our own tools from scratch every time and make the same but different mistakes over and over. No language is perfect and all will guide you to different takes on the same problem that better suit to its paradigm, but there are intractable parts that will force you to do things (e.g. heap allocation and more involved ownership semantics are not uncommon). Moreover C very limited ability to manage your own codebase to define API boundaries - the only tool is opaque types with a defined set of functions declared in 2 different files.


The opaque types in C are great though. And because no info leaks via header, you have very fast compilation. The y"ou write from scratch every time" is a weird statement. I do not delete my own code after each project and there exist plenty of libraries.

Somewhat separating owning and non owning memory in the type system goes a long way. Also a much better standard library and a stricter typing discipline.

The fact that it's mostly backwards compatible means you can reproduce almost all issues of c in c++ awell, but the average case fares much better. Real world C++ does not have double-frees, for example. (As real world C++ does not have free() calls).


"In addition, there are some variables such as locks and scope_guards that are only used for their side effects"

...

"This solution is also similar to other languages’ features or conventions"

As far as I know, in Rust you can't use "_" for that, as the value will be dropped right away, so the mutex/resource/etc. won't live for the scope.


No, it lives until the end of its last scope regardless of name

This is not a name, it's the specific choice not to assign it to any name, and so your parent was correct that it's dropped immediately.

https://rust.godbolt.org/z/P1z7YeP4Y

As you see, Rust specifically rejects this code because it's never what you meant, either write explicitly "I want to take and then immediately give away the lock" via drop(LOCK.lock()) (this really might be what you wanted, the Linux kernel does this in a few places) or write an actual named placeholder variable like in my third example function.


But on the other hand the motivating problems noted in C++ don't exist. It is 100% legal to completely rebind a variable in the same scope, it never warns that `_` variables are unused nor about unused `_` prefixed variables. I think `_` being immediately dropped is maybe one of those unfortunate decisions we'll look back in 20 years and regret.

Ever heard of `-Wunused-variable`?

> rebind a variable in the same scope

But only re-assigning values of the same type. Otherwise:

  int x = foo();
  int x = bar();
  error: redefinition of 'x'
and,

  int x = foo();
  long x = bar();
  error: redefinition of 'x'
What are you talking about?

> This is not a name, it's the specific choice not to assign it to any name

Yeah, it's the same in c#. This is noticeable when in the same scope you can have multlple "_" vars. If these were actual names, they would be a name clash.

One of the uses is take some parts of a tuple return and ignore the others.

e.g.

var (thingIwant, _, _, _) = FunctionThaReturnsATupleOfFourThings();

There are three "_" in that expression, and they are different vars with different types. They don't really have the same name, they don't have names at all.


every time i see stuff like this, I hope I never have to work on c++ projects again.

See c#:

"A discard communicates intent to the compiler and others that read your code: You intended to ignore it.

You indicate that a variable is a discard by assigning it the underscore (_) as its name."

https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals...

2020: http://dontcodetired.com/blog/post/Variables-We-Dont-Need-No...


Yep, Microsoft took only almost 50 years to do what Prolog could do in 1971, and the C++ standards committee took ~5 years longer.


Not every "problem" needs a solution.

At this point only LLMs will be able to decipher every intricacy of C++.


I swear some of you read 'C++' in the submission title and immediately assume it's tagging on inscrutable, additional ways of doing things. This is making C++ code _less_ intricate.

No, they cannot, they are word prediction machines.

What a wise display of complete ignorance.



Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: