[HN Gopher] Why not just do simple C++ RAII in C?
___________________________________________________________________
Why not just do simple C++ RAII in C?
Author : todsacerdoti
Score : 170 points
Date : 2024-05-22 13:38 UTC (1 days ago)
(HTM) web link (thephd.dev)
(TXT) w3m dump (thephd.dev)
| slaymaker1907 wrote:
| I'd argue it barely works in C++ as well. I've seen so many
| poorly implemented classes that violate the very complicated
| 3/5/0 rule. It's much easier to do RAII correctly in Rust since
| people aren't constantly working with raw pointers and since
| objects which are moved somewhere else are not dropped like they
| are in C++.
|
| One variant that I think might work even better than RAII or
| defer in a lot of languages is having a thread local "context"
| which you attach all cleanup actions to. It even works in C, you
| just define cleanup as a list of typedef
| void(cleanup_function*)(void* context); which is saved
| as into a thread local. Unlike RAII, you don't need to create a
| custom type for every cleanup action and unlike the call-with
| pattern from functional programming, lifetimes of these lists can
| be non-hierarchical.
|
| However, I'm still glad to see defer being considered for C. It's
| a lot better than using goto for cleanup.
| pavlov wrote:
| _> 'a thread local "context" which you attach all cleanup
| actions to'_
|
| Like the autorelease pool found in Objective-C of yore? I
| always liked that solution and sometimes implemented in plain C
| too.
| adrianN wrote:
| Yeah, the example of "what if you make a copy" breaks in C++ in
| exactly the same way if you're not careful.
| d0mine wrote:
| Arena can be used to allocate many times but deallocate exactly
| once. In Zig: test "arena allocator" {
| var arena =
| std.heap.ArenaAllocator.init(std.heap.page_allocator);
| defer arena.deinit(); const allocator =
| arena.allocator(); _ = try allocator.alloc(u8,
| 1); _ = try allocator.alloc(u8, 10); _
| = try allocator.alloc(u8, 100); }
| jay-barronville wrote:
| I'm a fan of Zig, but I just want to point out that creating
| dedicated allocators for managing specific regions/chunks of
| memory or memory within specific application scopes (i.e.,
| arenas) is just _another_ memory allocation strategy rather
| than the ultimate solution to memory management issues. It
| comes with its own trade-offs and depends entirely on your
| use case. Also, it's great that Zig has this battery included
| in its standard library, but arenas aren't unique to Zig nor
| are they difficult to implement in any language that allows
| manual memory management. I'm just pointing this out because
| I keep seeing folks highlight this as a key feature of Zig
| over C.
| anymouse123456 wrote:
| This. I've been loving Zig for some years now, but still
| write a lot of embedded C at work.
|
| I've started to use simple memory arenas in C and it just
| feels so damn _nice_.
|
| There's basically a shared lifetime for most of my
| transient allocations, which are nicely bounded in time by
| a "frame" of execution. Malloc/Free felt like a crazy
| amount of work, whereas an arena_reset(&ctx) just moves a
| pointer back to the first entry.
|
| Another person pointed out that arenas are not destructors,
| and this is a great point to make. If you're dealing with
| external resources, moving an arena index back to the
| beginning does not help - at all.
| OskarS wrote:
| You can do it in C for sure, but "culturally" in C, there's
| a stateless global allocator called "malloc", which is not
| the case in Zig. For instance, if you have a library
| libsomething in C, it will at most (probably) have
| something like this: #ifndef LIB_MALLOC
| #define LIB_MALLOC malloc #end
|
| if it allows you to customize allocation strategy at all,
| which is not a given.
|
| But this only allows you at compile time to provide your
| own stateless global allocator. This is very different in
| Zig, which has a very strong culture of "if something needs
| to allocate memory, you pass it a stateful, dynamically
| dispatched allocator as an argument". You COULD do that in
| C, but virtually nobody does.
| samatman wrote:
| It's 100% a key feature of Zig. Culturally, if it
| allocates, then it takes an allocator as an argument. C
| simply doesn't work that way. You _could_ write C that way,
| but people don 't.
|
| I've written reasonable amounts of both, and it's just
| different. For instance, in Zig, you can create a HashMap
| using a FixedBufferAllocator, which is a region of memory
| (which can be stack allocated) dressed up as an allocator.
| You can also pass it an arena and free all at once, or any
| other allocator in the standard library, or implemented by
| you, or anyone else. Show me a C library with a HashMap
| which can do all three of these things. _Everything_ which
| allocates takes an allocator, third-party libraries respect
| this convention or will quickly get an issue or PR either
| requesting or implementing this convention.
|
| Ultimate solution? No, but also, sort of. The ability to
| _idiomatically_ build a fine-grained memory policy is a
| large portion of what makes Zig so pleasant to use.
| sixthDot wrote:
| allocation is not construction, and deallocation is not
| destruction. The two steps are oftently executed sequentially
| but if you think that they are the same you'll end up with
| leaks, e.g at the level of the operating system (e.g GDI
| handles). What I mean is that arena allocators are not as
| simple as you pretend. That depends on what they allocate.
| The more commonly reason why arena allocators are praised is
| the cache locality.
| Gibbon1 wrote:
| My small brained comment is people use heap allocation when
| they should be using an arena allocation. And heap allocation
| shouldn't return a pointer it should return a handle.
| vlovich123 wrote:
| > that violate the very complicated 3/5/0 rule
|
| Is it actually complicated? There's only the rule of 0 - either
| your class isn't managing resources directly & has none of the
| 5 default methods defined explicitly (destructor, copy
| constructor/assignment, move constructor/assingment), or it
| manages 1 and exactly 1 resource and defines all 5. Following
| that simple rule gives you exception safety & perfect RAII
| behavior. Of all the things in C++, it seemed like the most
| straightforward rule to follow mechanically.
|
| BTW, the rule of 3 is from pre-C++11 - the addition of move
| construct/move assignment makes it the rule of 5 which
| basically says if you define any of those default ones you must
| define all of them. But the rule of 0 is far stronger in that
| it gives you prescriptive mechanical rules to follow for
| resource management.
|
| It's much easier to do RAII correctly in Rust because of the
| ecosystem of the language + certain language features that make
| it more ergonomic (e.g. Borrow/AsRef/Deref) + some ownership
| guarantees around moves unless you make the type trivially
| copyable which won't be the case when you own a resource.
| chipdart wrote:
| > Is it actually complicated?
|
| It is. There is no point in arguing otherwise.
|
| To understand the problem, you need to understand why it is
| also a solution to much bigger problems.
|
| C++ started as C with classes, and by design aimed at being
| perfectly compatible with C. But you want to improve
| developer experience, and bring to the table major
| architectural traits such as RAII. This in turn meant you add
| support for custom constructors, and customize how your
| instances are copied and destroyed. But you also want to be
| able to have everything just work out of the box without
| forcing developers to write boilerplate code. So you come up
| with the concept of special member functions which are
| automatically added by the compiler if they are trivial.
| However, forcing that upon every single situation can cause
| problems, so you have to come up with a strategy that suits
| all use cases and prevents serious bugs.
|
| Consequently, you add a bunch of rules which boil down to a)
| if the class/struct is trivial them compilers simply add
| trivial definitions of all special member functions s that
| you don't have to, but once you define any of those special
| member functions yourself them the compiler steps back and
| let's you do all the work.
|
| Then C++ introduced move semantics. This refreshes the same
| problem as before. You need to retain compatibility with C,
| and you need to avoid boilerplate code, and on top of that
| you need to support all cases that originated the need for
| C++'s special member functions. But now you need to support
| move constructors and move assignment operators. Again, it's
| fine if the compiler adds those automatically if it's a
| trivial class/struct, but if the class has custom
| constructors and destructors then surely you also need to
| handle moves in a special way, so the compiler steps back and
| lets you do all the work. On top of that, you add the fact
| that if you need custom code to copy your objects around,
| surely you need custom code to move them too, and thus the
| compiler steps back to let you do all the work.
|
| On top of this, there are also some specific combinations of
| custom constructors/destructors/copy constructors/copy
| assignment operators which let the compiler define move
| constructors/move assignment operators.
|
| It all makes absolutely sense if you are mindful of the
| design requirements. But if you just start to onboard onto
| C++ and barely know what a copy constructors is, all these
| aspects are arcane and sadistic. If you declare nothing then
| your class instances are copied and moved automatically, but
| once you add a constructor everything suddenly blows up and
| your code doesn't even compile anymore. You spotted a bug
| where an instance of a child class isn't being destroyed
| properly, and once you add a virtual destructor you suddenly
| have an unrelated function call throw compiler errors. You
| add a snazzy copy constructor that's very performant and your
| performance tests suddenly start to blow up because of the
| performance hit if suddenly having to copy all instances
| instead of the compiler simply moving them. How do you sort
| out this nonsense?
|
| The rule of 5 is a nice rule of thumb to allow developers to
| have a simple mental model over what they need to do to avoid
| a long list of issues, but you still have no control over
| what you're doing. Things work, but work by sheer
| coincidence.
| tuyiown wrote:
| > Things work, but work by sheer coincidence
|
| I wouldn't be so dramatic. House of cards don't stay put by
| coincidence !
| vlovich123 wrote:
| > It is. There is no point in arguing otherwise.
|
| Well, I don't know how to respond to this. I clarified what
| the rules actually are (< 1 paragraph) and following them
| blindly leads to correct results. You've brought in a whole
| bunch of nonsense about why C++ has become complex as a
| language - it's not wrong but I'm failing to connect the
| dots as to how the rule of 0 itself is hard to follow or
| complex. I'm kind of taking as a given that whoever is
| writing the code is mildly familiar enough with C++ to
| understand RAII & is trying to apply it correctly.
|
| > The rule of 5 is a nice rule of thumb to allow developers
| to have a simple mental model over what they need to do to
| avoid a long list of issues, but you still have no control
| over what you're doing. Things work, but work by sheer
| coincidence.
|
| First, as I've said multiple times, it's the rule of 0.
| That's the rule to follow to get correct composition of
| resource ownership & it's super simple. As for not having
| control, I really fail to see how that is - C++ famously
| gives you too much control and that's the problem. As for
| things working by sheer coincidence, that's like your
| opinion. To me "coincidence" wouldn't explain how many
| lines of C++ code are running in production.
|
| Look, I think C++ has a lot of warts which is why I prefer
| Rust these days. But the rule of 0 is not where I'd say
| C++'s complexity lies - if you think that is the case, I'd
| recommend you use another language because if you can't
| grok the rule of 0, the other footguns that lie in wait
| will blow you away to smithereens.
| bruce343434 wrote:
| > nonsense ... not wrong
|
| So it's not nonsense?
|
| I think GP clearly laid out the base principles that lead
| to emergent complexity . GP calls this "coincidence" to
| convey the feeling of lots of complexity just narrowly
| avoiding catastrophe in a process that is hard to grok
| for someone getting into C++. GP also gave some scenarios
| in which the rule of 0 no longer applies and you now
| simply have to follow some other rule. "just follow the
| rule" is not very intuitive advice. The rule may be
| simple to follow but the foundations on which it rests
| are pretty complicated, which makes the entire rule
| complicated in my worldview and also that of GP. In your
| view, the rule is easy to follow therefore simple. Let's
| agree to disagree on that. Again, being told "you need to
| just follow this arbitrary rule to fix all these sudden
| compiler errors" doesn't inspire confidence in ones code,
| hence (I think) the usage of "coincidence". If I were
| using such a language, I'd certainly feel a bit nervous
| and unsure.
| astrobe_ wrote:
| > GP calls this "coincidence" to convey the feeling of
| lots of complexity just narrowly avoiding catastrophe in
| a process that is hard to grok for someone getting into
| C++
|
| I think that's what they said themselves:
|
| >> It all makes absolutely sense if you are mindful of
| the design requirements. But if you just start to onboard
| onto C++ and barely know what a copy constructors is, all
| these aspects are arcane and sadistic
|
| IMO not knowing why something works (in any language) is
| an unpleasant feeling. Then if you have the chance you
| can look under the hood, read things - it's exactly why
| I'm reading this thread - and little by little get a
| better understanding. That's called gaining experience.
|
| > Again, being told "you need to just follow this
| arbitrary rule to fix all these sudden compiler errors"
| doesn't inspire confidence in ones code, hence (I think)
| the usage of "coincidence"
|
| That's exactly what other languages like Haskell or Rust
| are praised for. Why does C++ receive a different
| treatment when it tries to do the same thing instead of
| crashing on you at runtime, for once?
| marcosdumay wrote:
| > That's exactly what other languages like Haskell or
| Rust are praised for.
|
| You making a trivial change, and suddenly there are
| entire new classes of bugs all over your code is an
| aspect that does really not receive any praise. People
| using those two languages work hard on avoiding that
| situation, and it clearly feels like a failure when it
| happens.
|
| The part about pointing problems at compile time so the
| developer will know it sooner is great. And I imagine is
| the part you are talking about. But the GP was talking
| about the other part of the issue.
| rcxdude wrote:
| In addition, it's actually pretty easy in most cases
| where you do what a non-trivial constructor and
| destructor to just delete the other 3, and wrap it in
| unique_ptr or similar to manage the hard parts. I think
| I've defined all 5 approximately once, and mostly for the
| fun of it in a side project.
| rcxdude wrote:
| The need to define all 5 has basically nothing to do with
| C++'s heritage. If you allow those operations to be
| defined, they all must be defined when you define one of
| them.
|
| There is a neater design in rust with its own tradeoffs:
| destructors are the only special function, move is always
| possible and has a fixed approach, copying is instead
| .clone(), assignment is always just a move, and
| constructors are just a convention with static methods,
| optionally with a Default trait. But that does constrain
| you: especially move being fixed to a specific definition
| means there's a lot you can't model well (self-referential
| structures), and that's a core part of why rust can have a
| neater model. And it still has the distinction you are
| complaining about with Copy, where 'trivial' structures can
| be copied implicitly but lose that as soon as they contain
| anything with a destructor or non-trivial .clone().
|
| And in C++ it's pretty easy to avoid this mess in most
| cases: I rarely ever fully define all 5. If I have a custom
| constructor and destructor I just delete the other cases
| and use a wrapper class which handles those semantics for
| me.
| chipdart wrote:
| > The need to define all 5 has basically nothing to do
| with C++'s heritage. If you allow those operations to be
| defined, they all must be defined when you define one of
| them.
|
| I'm sorry, that is not true at all.
|
| Nothing forces you to add implementations, at least not
| for all cases. That's only a simplistic rule of thumb
| that helps developers not well versed on the rules of
| special member functions (i.e., most) to get stuff to
| work by coincidence. You only need to add a, say, custom
| move constructor when you need it and when the C++ rules
| state the compiler should not generate one for you.
| There's even a popular table from a presentation from
| ACCU2014 stating exactly in which condition you need to
| fill in your custom definition.
|
| https://i.sstatic.net/b2VBV.png
|
| You are also wrong when you assert this has nothing to do
| with C++'s heritage. It's the root cause of each and
| every single little detail. Special member functions were
| added with traits and tradeoffs for compatibility and
| ease of use, and with move semantics the committee had to
| revisit everything over again but with an additional
| layer of requirements. The rules involving default move
| constructors and move assignment operators are famously
| nuanced and even arbitrary. There is no way around it.
|
| > There is a neater design in rust (...)
|
| What Rust does and does not do is irrelevant. Rust was a
| greenfield project that had no requirement to respect any
| sort of backward compatibility and stability. If there is
| any remotely relevant comparison that would be
| Objective-C, which also took a minimalist approach based
| on custom factory methods and initializes that rely on
| conventions, and it is a big boilerplate mess.
| cozzyd wrote:
| It would be more user-friendly if non-defined members of
| the 5 were automatically deleted, IMO.
| bluGill wrote:
| I've never seen someone get the rule of 5 wrong, but the rule
| of 3 was a reaction to 10 years of hindsight to realise the
| default is wrong. Congradulations to rust for looking to see
| what was done wrong by their predisessors. you can't really
| fault someone for making a mistake when nobody at the time knew
| it was a mistake.
| chipdart wrote:
| > I'd argue it barely works in C++ as well. I've seen so many
| poorly implemented classes that violate the very complicated
| 3/5/0 rule.
|
| I'm afraid you're complaining about entirely unrelated things.
|
| It's one thing to claim that C++ structs have this or that
| trait. It's a entirely different thing to try to pin bugs and
| developer mistakes on how a language is designed.
| rramadass wrote:
| > the very complicated 3/5/0 rule
|
| It is not very complicated at all; just a discipline to follow
| (or not if you know what you are doing) once learnt -
| https://en.cppreference.com/w/cpp/language/rule_of_three
|
| Incidentally i use it as 4/6/0 by including the default ctor in
| the set.
| uecker wrote:
| I was initially interested in defer in C (I am co-author of
| some earlier proposal), but after actually studying its impact
| on code example I was entirely unimpressed about the actual
| improvement compared to goto style cleanup. A lot of people
| seem to like it though, and JeanHeyd's version seems quite
| good, but I personally not terribly convinced about this
| feature anymore.
| gary_0 wrote:
| In other words, C structs and C++ structs are not the same thing
| (although C++ can usually handle C structs too). C structs are
| Plain Old Data. C++ structs are "objects" and there are pages and
| pages of rules in the C++ Standard about what that means, and
| there's no way the C Standard can incorporate all that. And you
| can't drag any C++ struct/class features into C without dragging
| in all those rules with them.
| stonemetal12 wrote:
| Yep. The only difference between struct and class in C++ is
| that class defaults to private while struct defaults to public.
|
| 100% of the using structs like they are C structs vs using
| class as objects is cultural not a part of the language.
| SAHChandler wrote:
| The public vs. private aspect also affects inheritance.
| structs publicly inherit from base types by default, classes
| privately inherit from base types.
| gary_0 wrote:
| Any similarly of keyword naming between C and C++ is purely
| coincidental. :P
|
| C++ is somewhat unique in that it started out as a few extra
| features on top of C before gradually splitting off and
| mutating into a totally separate programming language.
| kazinator wrote:
| Support for POD (plain old datatype) structs in C++ is
| definitely part of the language.
| chipdart wrote:
| > 100% of the using structs like they are C structs vs using
| class as objects is cultural not a part of the language.
|
| I think this take is completely wrong. There is nothing
| cultural about it. C++ was created as a strict superset of C,
| and thus from the inception it supported all features made
| available in C. This design goal remains true up to this day,
| and only started to diverge relatively recently when C was
| updated to include features that were not supported (yet) by
| C++.
|
| When someone declares a plain old struct in C++, they are
| declaring a struct that is perfectly compatible and
| interoperable with C. This is by design. From the inception.
| mianos wrote:
| What if, after 20 years of C++, you spend 10 years doing
| python, only to go back to C++ and realise that all this
| private/protected stuff is a crock and most of the time you
| are doing real work you just use struct and start typing
| your C++, virtual functions, constructors, destructors etc?
|
| Just asking, for a friend.
| josefx wrote:
| > you spend 10 years doing python, only to go back to C++
| and realise that all this private/protected stuff is a
| crock
|
| Just a friendly reminder that two leading underscores
| wont protect your member functions in C++. Even if people
| insist that those are totally not supposed to be private
| in python.
| nhatbui wrote:
| I think OP meant discarding public/private constructs
| entirely, no protection, like in python.
| gpderetta wrote:
| Except python started mangling double underscore in a
| futile attempt to implement private members/methods.
| mianos wrote:
| The underscore prefix is more about communication. It's
| not a bad convention as it makes you feel a bit dirty
| when you are using them outside a class, but, do what you
| want, we are consenting adults.
|
| Whenever I say "I'm no longer attached to all that
| private stuff", people always reply, "wait until you work
| on a large code base". I work on a million line+ code
| base. Whatever.
|
| This argument aside, I'm not a total philistine. RAII is
| awesome but C++ is full to the boot with crusty stuff to
| keep the compatibility. I always feel there is a language
| better than anything trying to come out.
| gpderetta wrote:
| Python will literally mangle the names of double
| underscore members by prefixing them with the class name,
| to make it harder to access from the outside, so it is
| not just about communication.
|
| These days I'm for minimalism, most of my structs are
| aggregates of public members, but sometimes you really
| want to make sure to maintain the invariant that your
| array pointer and your size field are in sync.
| Doxin wrote:
| Using double underscore is advised against, and the name
| mangling is largely considered a mis-feature these days.
| Most style guides will tell you to use a single
| underscore to mark something as not for public
| consumption.
|
| Of course _neither_ double nor single underscore will
| stop anyone who wants to touch your privates badly
| enough. Which is big part of the python philosophy: You
| 're not stopped from doing inadvisable things. Instead
| there's a strong culture around writing "pythonic" code,
| which largely avoids these pitfalls.
| eru wrote:
| And neither does C++'s 'private' stop any other code from
| messing with your data, either, if they want to do that
| badly enough.
| steveklabnik wrote:
| > This design goal remains true up to this day, and only
| started to diverge relatively recently when C was updated
| to include features that were not supported (yet) by C++.
|
| This is not really the case. See https://en.wikipedia.org/w
| iki/Compatibility_of_C_and_C%2B%2B for a non-exhaustive
| list.
|
| It is true that both sides agree that compatibility is an
| important goal, but it's only a goal, not something that's
| 100% the case.
| chipdart wrote:
| > In other words, C structs and C++ structs are not the same
| thing (although C++ can usually handle C structs too). C
| structs are Plain Old Data. C++ structs are "objects" and there
| are pages and pages of rules in the C++ Standard about what
| that means, and there's no way the C Standard can incorporate
| all that.
|
| I think this glances over what structs actually are in C++, and
| unwittingly portrays them as something different.
|
| Structs in C++ are definitely exact like structs in C. Or they
| can be, if that's what you're aiming for. If you include a C
| header file that defines a struct in a C++ program, you build
| it, and you use instances of that struct to pass them to C
| programs, everthing just works.
|
| The detail you need to be mindful of is that C structs support
| a subset of all the features supported by C++ classes, and once
| you start to use those features C++ also allows implementations
| to forego some constraints.
|
| If you expect to use a struct in C++ but still define it in a
| way that you make it include features that are not supported in
| C then you can't pin that on the language.
|
| https://learn.microsoft.com/en-us/cpp/cpp/trivial-standard-l...
|
| Using C-like structs is a very common use case, to the point
| that the standard explicitly defines the concept of standard
| layout and builds upon that to specify the concept of a
| standard layout type. A struct/class that is a standard layout
| type, which means it's a POD type, corresponds exactly with C
| structs. They are explicitly defined in terms of retaining
| interoperability with other languages.
| rramadass wrote:
| Exactly. This is one of the basic things (i.e. PODs) you
| learn in C++ that i am surprised the gp didn't know of it. I
| myself have written systems with C-structs/C-api and wrapped
| the same C-structs (by deriving) in C++ classes (being
| careful with any introduced vptr/vtable) to happily
| provide/extend C-code/libraries via C++ apis.
| gary_0 wrote:
| Of course I know about the POD idiom. You and chipdart are
| misunderstanding me because there's a tension between how
| the C++ Standard defines things and how C++ gets used in
| real life. Because strictly speaking, the Standard doesn't
| really have a concept of POD[0]. What it does have is
| "trivial" classes and the concept of object lifetime. For
| instance, if your class/struct isn't trivially_copyable and
| you memcpy it like a C struct, you're in Undefined Behavior
| country. If your class/struct is such that you must observe
| C++'s lifetime rules, but you are writing its fields by
| casting a char pointer to some bytes, that's UB.
|
| But yes, if you make extra sure (under threat of footgun)
| that your struct only has simple types in it and doesn't
| use virtual or define any ctors/dtors or use
| protected/private or use inheritance and all of its members
| follow those rules etc etc, maybe you can treat it like a C
| struct. But the C++ Standard is telling a different story.
|
| Keep in mind, I'm not blaming you for ignoring all these
| complications if at the end of the day the compiler seems
| to give you the behavior you expect. But the fun of C++ is
| that it's kind of two programming languages in one: the
| language the Standard defines, and the language the typical
| programmer thinks it is.
|
| [0] There was std::is_pod, but it was deprecated because it
| doesn't reflect how the Standard actually defines things. A
| bit of a cruel joke, dangling that in front of us and then
| yanking it away.
| rramadass wrote:
| POD is not an idiom. It was an actual specification (in a
| sense) for interop between C++ and C. Only in the later
| standards (maybe starting at C++14?) did the committee
| refine it further as "POD = Trivial + Standard_Layout"
| but that is just a redefinition without any fundamental
| change in semantics. So you can happily write C++ code
| with just your understanding of POD from C++98 in
| practice and everything will work fine.
|
| References:
|
| 1) Trivial, standard-layout, POD, and literal types -
| https://learn.microsoft.com/en-us/cpp/cpp/trivial-
| standard-l...
|
| 2) No more plain old data -
| https://mariusbancila.ro/blog/2020/08/10/no-more-plain-
| old-d...
| gary_0 wrote:
| They started changing the definitions in C++11 to support
| move semantics. I don't remember much about C++98, that
| was decades ago. If that's what the Standard said back
| then I'll take your word for it, but I wasn't talking
| about historical C++ Standards.
|
| Keep in mind, my original comment was pretty much just
| drawing a line through TFA, which also argues that you
| can't cleanly map C++ object concepts onto C structs. C++
| has some backwards compatibility with C obviously but
| nowadays it's a totally separate language with an
| independent standards body (for better or worse).
| Specifying "do what C does" might have flown in 1998 but
| that changed a long time ago.
| rramadass wrote:
| I am generally not a fan of the standards committee nor
| what it is trying to do with the language. The word
| "Object" used in C++ land has a different meaning than
| the same word used in C land since there is an "Object
| Model" in C++ while there is none in C. Hence trying to
| map C++ object concepts onto C does not even make sense
| in the general case. But because of C++'s evolution
| having started as "C with classes" there is some mapping
| at the set-of-bits level which is where the POD (with all
| its limitations) comes in.
|
| I am fully with Stroustrup in arguing that C++ should
| strive for as much compatibility with C as possible in
| the spirit of the original (see ref. at https://en.wikipe
| dia.org/wiki/Compatibility_of_C_and_C%2B%2B...). But
| sadly the rest of standards committee don't seem to want
| this which i believe is a huge mistake. On the other
| side, the C standards committee should be very careful
| what inspiration they take from C++ in the evolution of
| the language since it was designed as a "minimal"
| language which was one of the main factors in its
| success. Whether people call it "primitive", "well behind
| other languages" etc. does not matter. You definitely
| don't want C turning into C++-lite. Hence IMO the
| conclusions stated in the last few paragraphs of the
| submitted article are quite right.
| gary_0 wrote:
| In my experience the number of C++ developers with nice
| things to say about the Committee is... very small.
|
| In a way, the whole C++ endeavor was doomed from the
| start. C was old and pragmatic and vague, a "portable
| assembly", and it was a shaky foundation to build C++ on
| top of. When the Standard tried to tighten things up, it
| just got more lopsided, full of hacks to fix hacks. But
| the alternate universe where C++ had a more pragmatic,
| laissez-faire design going forward probably isn't any
| better; maybe the "standard" would have become "do
| whatever GCC does"--or in the Darkest Timeline, "do
| whatever MSVC does".
|
| I disagree that C++ "respecting its C roots" is viable.
| The C++11 and later Standards were trying to make the
| best of a bad situation, and that required leaving C
| behind because the C way of doing things doesn't fit with
| a higher-level language like contemporary C++. Especially
| when the language has multiple implementations that need
| to compile the same code the same way. The "C with
| classes" days are long over for most of us who have to
| use libraries expecting std::vector, smart pointers, and
| exception handling. We live in mortal fear of compiler
| writers smiting us for innocent things like punning
| through a union.
|
| > You definitely don't want C turning into C++-lite
|
| I agree. Trying to quickly hack classes or templates or
| whatever back on top of C would just start the whole C++
| nightmare over again.
| rramadass wrote:
| > In a way, the whole C++ endeavor was doomed from the
| start ... I disagree that C++ "respecting its C roots" is
| viable.
|
| Hey! Them's fighting words! :-) "C++ as a better C"
| (which is what it started as) was/is/always will be
| needed and necessary. It gave you the best of both low-
| level and high-level worlds with full control and just
| enough complexity. Instead of implementing structs full
| of function pointers to design dynamic dispatch object
| models you just had the compiler do that for you while
| still retaining full control over other aspects. I still
| have some manuals that came with SCO Unix one of which
| was on the then newfangled C++ language. It had one
| chapter by Stroustrup himself (his original paper
| probably) on the C++ object model showing how
| vptrs/vtables are implemented and thinking it neat that
| the compiler did it for you. Also templates were just
| glorified macros then with none of the shenanigans that
| you see today. Hence moving from C to C++ was easy and
| its usage and popularity exploded. But with the infusion
| of lots of people into C++ land people who were not aware
| of the original vision/design/compatibility goal of the
| language started asking for the inclusion of more and
| more OO and modern language features. The result? The
| standards committee reinventing the language from C++11
| onwards(and changing every freaking 3 years) and
| alienating the old C++ folks who made it popular in the
| first place. No doubt there are some benefits like
| increased design space and modern programming techniques
| but am not sure whether the increased complexity makes it
| all worth it. For me it is still C++98 with the addition
| of the STL and some simple generic programming techniques
| which is the sweet spot.
| celrod wrote:
| > We live in mortal fear of compiler writers smiting us
| for innocent things like punning through a union.
|
| C++20 introduced `std::bitcast`, so I appreciate alias
| analysis getting all the help it can.
| rkangel wrote:
| > Using C-like structs is a very common use case
|
| Not true. Using C structs themselves in C++ is very common -
| when you include the C header file, the relevant declarations
| are wrapped in "extern "C" {}" which gives structs C
| semantics. You can do this because C++ is backwards
| compatible with C.
|
| Most of the time when you use a struct in C++ you're just
| ignoring most of the capabilities of objects (which is
| fine!). If you declare a struct in C++, you're getting an
| object. The only difference between the struct and class
| keywords in C++ is the default privacy of the members.
| gpderetta wrote:
| A C++ structure has exactly the equivalent semantics of a C
| structure if it exists. Extern "C" only affects linkage of
| functions. It has no effect on structure definitions.
| OskarS wrote:
| It absolutely does not. A C++ structure is a much richer
| object: it can have custom copying, moving and assignment
| behaviours, it can have vtables, it has RAII, etc. That's
| the whole point about the "object model" in the article:
| C++ has it, C does not.
|
| What I think you're trying to say is "a POD structure
| with no custom behavior is essentially identical in C and
| C++". That is mostly true, though if the struct contains
| a union, C++ has stricter UB rules (there might be other
| differences as well, but that's the one I can think of at
| the moment).
| gpderetta wrote:
| What I'm saying is that extern "C" has no effect on
| structure compatibility.
| eschneider wrote:
| It would seem that if you want C with RAII, you...use C++ and
| limit the features you use. QED.
| manuel_w wrote:
| Sounds reasonable. In a project I used to work in I disabled
| stdlib, exceptions, RTTI. Not sure what else to disable to
| essentially have C with different syntax.
| legobmw99 wrote:
| extern "C" on everything, if you care about linking
| compatibility, gets you another chunk of the way there by
| also disabling overloading etc.
| 01100011 wrote:
| This is surprisingly common. C++ is huge and filled with many
| features that are only understood by a small subset of folks
| and so many teams have restricted coding standards that define
| which features can be used and how.
| wrs wrote:
| >The utterly pervasive and constant feeling that a lot of people
| - way too many people - are really trying to invent these things
| from first principles and pretend like they were the first people
| to ever conceive of these ideas... it feels pretty miserable, all
| things considered. Going through life evaluating effectively no
| prior art in other languages, domains, C codebases as they exist
| today, just... anything.
|
| Oh man, I hear ya. And in a lot more domains than computer
| language design. Is it inexperience? Impatience? The tendency for
| search results to be filled with low-quality and high-recency
| content? The prioritization of hot-take blog posts and Reddit
| comments over books?
| lpapez wrote:
| I kind of emphatize with the author raging at "just copy C++ bro"
| proposals because at $TWO_JOBS_AGO I had to deal with an
| "Architecture Team" full of Very Senior(tm) people who would show
| up uninvited and give advice like "did you know you can pee and
| poo at the same time?"
|
| Of course, but if you bothered at all to understand the
| constraints, you would have seen it is not actually that simple
| in our case.
|
| And my project was several orders of magnitude simpler than the C
| standard.
| phendrenad2 wrote:
| I feel like this is an archetype. Show up out of nowhere, half-
| understand the problem, make a brain-dead suggestion, and then
| as soon as you point out the problems with that approach,
| they're suddenly too busy with other things to respond.
| meindnoch wrote:
| Architect: you know, I just feel like there should be a way
| to solve this in a proper way
|
| Engineer (thinking): _(No, you idiot, there isn 't, because
| it's broken! I told you, all options have been tried, and
| this was the least painful way of doing it. Yes, it's not the
| ideal solution, but there's no other way, unless the upstream
| vendor decides to fix the issue on their end!)_
|
| Engineer: thanks, I'll look into it :)
| xmodem wrote:
| I had one where the architecture team implemented the brain-
| dead solution, advised leadership of other teams that they
| should adopt the brain-dead solution, and that my team would
| be supporting the brain-dead solution, without consulting us,
| and made me into the bad guy when I pointed out that my team
| did not support the brain-dead solution.
|
| We ended up supporting the brain-dead solution, but that team
| has now experienced 100% turn over since then.
| chipdart wrote:
| > (...) I had to deal with an "Architecture Team" full of Very
| Senior(tm) people who would show up uninvited and give advice
| like "did you know you can pee and poo at the same time?"
|
| It reads like you had experts giving you advise on how to
| improve things, and instead not only did you ignored their
| advise but you went to the extent of mindlessly disparaging
| their help.
| VBprogrammer wrote:
| I suspect if you don't recognise this scenario you may be
| standing too close to the mirror.
| lpapez wrote:
| Nah, the other commenter had described the situation exactly
| right - after dropping the comments, the "A-Team" disappeared
| for a few months and never revisited our responses. It really
| feels like an archetype common at many companies.
|
| They were doing it just to boost their egos and most of the
| teams in the company learned to ignore them. When the company
| ownership changed, the "A-Team" was the first under the
| chopping block because the new owners correctly saw that the
| high status they had was simply due to inertia of being first
| devs at the company and were not fullfiling any meaningful
| role in the present.
| wiseowise wrote:
| Spotted one of the architecture team.
| geraldwhen wrote:
| I've yet to meet an architect I would hire to build literally
| anything.
|
| I've met dozens that don't know their head from their ass.
| And always, always when you describe the problem constraints,
| they mumble and disappear.
| eru wrote:
| My first boss had the title 'architect', but he was
| actually very competent, and very regularly got his hands
| dirty coding.
|
| (But at the time, I basically joined what was still
| essentially a startup just after they had been acquired by
| a larger company. I think the titles like 'architect' might
| have come from the larger company, but the competence came
| from them still being the same people as at the startup.)
| eropple wrote:
| I'm currently at a very large company, and architects
| are, in many lines of business, the only technical folks
| directly employed by the company. Which means a product's
| quality hinges pretty directly on whether your architect
| is somebody technical who can help solve problems both at
| the implementation level _and_ before they get to the
| implementation level (which I certainly try to be, when
| not triple-booked on meetings trying to keep everything
| else on the rails) or the Dilbert version.
|
| We do exist, I promise. ;) But in my case at least, the
| Eye of Sauron can only keep so many things in sight at a
| time...
| jandrewrogers wrote:
| They accurately describe a particular type of person/role
| that exists at many large enterprises. These "architects"
| notionally have a lot of authority, appointed by other not
| very technical people, but are so divorced from the realities
| of the engineering execution that anything they tell you is
| mostly useless. In my experience it tends to be a refuge for
| people that aren't very strong technically but who enjoy
| making slide decks for management.
| wiseowise wrote:
| > I kind of emphatize with the author raging at "just copy C++
| bro" proposals because at $TWO_JOBS_AGO I had to deal with an
| "Architecture Team" full of Very Senior(tm) people who would
| show up uninvited and give advice like "did you know you can
| pee and poo at the same time?"
|
| My sides. This is the most hilarious and accurate summary of
| every org where I've worked.
|
| Thank you.
| ryandrake wrote:
| +1 It's amazing how we've all encountered these guys at least
| once in our careers (and often more, at many companies). A
| lot of times they are "founding engineers" from a decade ago
| who were employee number 2 and 3 or something, and once knew
| quite a bit about the codebase. They are too high-paid to
| code anymore, but they still fly in, dispense their "wisdom"
| all over the junior folks, then flap their wings and
| disappear for a few months. Seagull management[1] without the
| role/responsibility of people management.
|
| 1: https://en.wikipedia.org/wiki/Seagull_management
| keyle wrote:
| "Architects"! I always smile at the term. I'm as much a
| software "engineer" as they're "architects".
|
| At some point we had to wear deodorant and a collared shirt,
| boom we became engineers.
| supahfly_remix wrote:
| Developers will become engineers as soon as the legal
| liability for mistakes falls on them.
| AtlasBarfed wrote:
| I have good news, AI will be the cover for all mistakes
| now, since it can be fingerpointed/scapegoated as the
| source of all code.
| supahfly_remix wrote:
| I know you're joking, but I'm talking about legal
| responsibility like this:
| https://insideevs.com/news/575160/mercedes-accepts-legal-
| res.... In this case, Mercedes, not the the developer, is
| assuming liability, but that responsibility will trickle
| down.
| TeMPOraL wrote:
| That is yet to be determined. There is some pressure in
| the other direction. I know of one large corporation,
| that does a lot of work around the world of the kind
| where programming errors could destroy property or kill
| people, has strict policies wrt. AI-generated code. This
| includes an obligation to clearly mark code that was
| written by AI, as well as restrictions on when it's
| allowed. This is driven not just by potential IP issues,
| but also by security and _export control_.
|
| (Yes, in a large enough corp, export control is a source
| of a surprisingly large amount of extra work...)
| shrimp_emoji wrote:
| Collars?! Never.
| fredrikholm wrote:
| > Very Senior(tm) people who would show up uninvited and give
| advice like "did you know you can pee and poo at the same
| time?"
|
| That gave me a good 5 minutes of chuckling and smiling. Thank
| you.
| autoexecbat wrote:
| Plenty of people struggle with simultaneous action for that.
| Generally doing one after the other within the same sitting
| rowanG077 wrote:
| How dare they try to help the more junior engineers. What
| horrible people.
|
| In all seriousness just accept their advice and see it for what
| it is. Someone trying to help you with limited view of the
| scope. As long as they don't impose their view I think your
| take is extremely bad.
| Xeamek wrote:
| Is RAII even wanted? I mean, in the name, 'Resource acquisition
| is initialization' it talks about the initialization part. But
| while not super versed in cpp, it looks like what everybody wants
| is actually the de-initialization part, which doesn't seem to be
| inherent to RAII, no?
|
| It's a bit confusing to have a 'thing' mention one mechanism in
| its name, but actually being valuable by ensuring some other
| mechanism
| legobmw99 wrote:
| I feel like most RAII fans will openly admit that it's the
| worst name in the world for such an idea. The idea is that any
| time you acquire a resource, you should initialize an object
| with said resource. But I guess "resource allocation should
| always lead to object initialization" is too long.
|
| The reason to do this is precisely so that the resource can be
| cleaned up at destruction of the object. So even if you had an
| acronym like RASALTOI, it would still probably be misleading
| NekkoDroid wrote:
| RAII is the worst name they could have selected and C++ devs
| openly admit that. Its more SBRM, Scope Based Resource
| Management.
| susam wrote:
| > It's a bit confusing to have a 'thing' mention one mechanism
| in its name, but actually being valuable by ensuring some other
| mechanism.
|
| Indeed! When I was first learning C++, I found the term "RAII"
| quite confusing too. However, after years of experience with
| this term, associating "RAII" with its intended meaning has
| become second nature.
|
| Having said that, there is at least one way to make better
| sense of "RAII" and that is considering the fact that in RAII,
| holding a resource is a _class invariant_. The resource is
| acquired during construction (initialisation) and released
| during destruction (which happens automatically when the object
| of the class goes out of scope). Throughout the object 's
| lifetime, from construction to destruction, maintaining
| possession of the acquired resource is an invariant condition.
|
| Although sounds simple in principle, this can get complicated
| pretty quickly, especially in the implementation of the copy
| assignment operator where we may need to carefully delete an
| existing resource before copying the new resource received by
| the operator. Problems like this led to formulating more
| techniques for carefully managing the resources while
| satisfying the class invariant. One such technique is the copy-
| and-swap idiom.
|
| None of this is meant to justify the somewhat arbitrary term
| though. In fact, there are at least two better alternative
| names for RAII: Scope-Based Resource Management (SBRM) and
| Constructor Acquires, Destructor Releases (CADR).
| mjevans wrote:
| CADR is far clearer. Just like re-ordering it as Resource
| Initialization Is Acquisition.
| jokoon wrote:
| I wish there was some way that you could configure a C++ compiler
| to just disable certain features of the language, or enforce good
| practices.
|
| But that's already what linters/static analyzers are doing? But
| then, why not integrate those tools directly in a C++ compiler
| instead?
|
| With cpp2/cppfront, Herb Sutter is already building some sort of
| a "sane" subset of the C++ language, maybe because you cannot
| achieve good practices without having a new syntax.
|
| C++ seems to have the same problem of javascript: it has annoying
| "don't-do-that" use cases, although it seems insanely more
| complicated to teach good C++ practices.
| fweimer wrote:
| You can write a compiler plugin that rejects constructs you
| don't like. Even GCC doesn't immediately lower the more complex
| (more controversial) C++ constructs. There's an existing system
| headers mechanism, so it's probably not that hard to skip this
| kind of feature restrictions for the standard library headers
| (where the banned constructs might be used to implement
| something that looks completely different at the surface).
| ThatGeoGuy wrote:
| This is more or less what Sean Baxter was trying to do with
| https://www.circle-lang.org/.
|
| Of course, this requires buying into a set of tooling and
| learning a lot of specific idioms. I can't say I've used it,
| but from reading the docs it seems sound enough.
| pjmlp wrote:
| That is what static analysers are for.
|
| The issue is developers that think they are useless tools.
| mst wrote:
| Usefully albeit depressingly, these days you can often get
| significantly easier buy-in if you call it a linter instead.
| humanrebar wrote:
| Or taken to an extreme, it's not hard to compose clang-
| queries that find for any arbitrary syntax. You could ban
| 'int' and pointers in your project if you wanted!
| pjmlp wrote:
| Or the way I like it, configure Sonar to break pull
| requests when devs ignore the rules that are supposed to be
| followed.
| jay-barronville wrote:
| C is the ultimate WYSIWYG language (provided you understand the
| semantics of your target architecture and assuming a non-buggy
| compiler). The language is relatively simple. The standard is
| accessible. I'd like it to remain that way. I don't need C to
| adopt any other "modern" language features.
|
| C11 provided a few worthwhile improvements (i.e., a proper memory
| model, alignment specification, standardized anonymous
| structures/unions), but so many of the other additions,
| suggestions, and proposals I've seen will just ruin the minimal
| nature of C. In C++, a simple statement like `a = b++;` can mean
| multiple constructors being called, hidden allocations,
| unexpected exceptions, unclear object hierarchies, an overloaded
| `++`, an overloaded `=`, etc. Every time I wish I had some C++
| feature in C, I just think about the cognitive overhead it'd
| bring with it, slap myself a couple times, and go back to loving
| simple ole C.
|
| Please don't ruin C.
| chowells wrote:
| And presuming you avoid 100% of undefined behavior, which I've
| never seen a non-trivial C program succeed at. C is _way_ too
| complicated in the real world. You don 't want C, you want a
| language that actually gives defined semantics to all
| combinations of language constructs.
| einpoklum wrote:
| You don't necessarily want that. Forcing language-defined
| semantics on everything costs performance. Sorry, it just
| does, we can't have it all. So, you can sacrifice performance
| for well-defined'ness, or you can choose not to - and the
| choice depends on the language _design goals_. As the design
| goals differ, so do the combinations of choices made for
| syntax and semantics.
| bigstrat2003 wrote:
| I think pretty much any amount of performance is worth
| sacrificing in order to get rid of the gnarly things UB can
| cause. Correctness is the first and most important thing in
| programming, because if you can't be certain it works then
| it's not very useful.
| fooker wrote:
| >you want a language that actually gives defined semantics to
| all combinations of language constructs
|
| No, this is wrong. It's a common misconception though. You
| would only want that in a hypothetical world where all
| computers are exactly the same.
|
| Undefined and implementation defined behavior is what allows
| us to have performance at all. Here are some simple examples.
|
| Suppose we want to make division by zero and null pointer
| dereference defined. Now every time you write a/b or *x, the
| compiler will be forced to emit an extra branching check
| before this operation.
|
| Something much more common---addition. What about signed
| overflow? Do you want the compiler to emit an overflow check
| in advance? Similar reasoning for shift instructions.
|
| UB in the language specification allows compilers to optimize
| based on the assumption that the programs you write won't
| have undefined behavior. If compilers are not able to do
| this, it becomes impossible to implement most optimizations
| we rely on. It's a very core feature of modern language
| specifications, not an oversight you can fix by thinking
| about it for 10 minutes.
| grumpyprole wrote:
| I'm sorry I still don't buy it. Can you please show me a
| use case where ignoring null pointer or overflow checks
| makes your product non-viable or uncompetitive?
|
| Some of these checks could be removed by languages with
| better compilers and likely more restrictions. That is the
| better approach. As a user, I don't want to run code that
| is potentially unsafe and/or insecure.
| fooker wrote:
| It's not ignoring about ignoring null pointer or overflow
| checks, it's having to necessarily insert these checks
| everywhere.
| grumpyprole wrote:
| We should build compilers that insert these checks for us
| (if they cannot statically determine them unnecessary).
| The ability to omit these checks doesn't IMHO justify
| undefined behaviour.
| fooker wrote:
| Well, good news is that you have optional modes in most
| compilers that do this.
|
| You would not want to force these by default, nobody
| wants it. You can not statically determine them
| unnecessary in for the vast majority of code, even stuff
| as simple as `print(read(a) + read(b))`.
| daemin wrote:
| So the simplest case for not providing a language
| specification for dereferencing a null pointer is that it
| requires putting in checks everywhere to detect the
| condition and then do something in the case where the
| pointer is null. So what should the null pointer case do
| then? Something like emit an exception, or send a signal,
| or call std::terminate to exit the process?
|
| I know that languages like Java have a
| NullPointerException which they can throw and handle for
| situations like this, but they're also built on a highly
| specified virtual machine architecture that is consistent
| across hardware platforms. This also does not guarantee
| that your program is safe from crashing when this
| exception gets thrown, as you have to handle it
| somewhere. For something as general as this it will
| probably be in the Main function, so you might as well
| let it go unhandled as there's not that much you can do
| at that point.
|
| For a language like C++ it is simpler, easier, and I
| would argue more correct, to just let the hardware handle
| the situation, which in this case would trigger a memory
| error of trying to access invalid memory. As the real
| issue is probably somewhere else in the code which isn't
| being handled correctly and the bad data is flowing
| through to the place where it accesses the null pointer
| and the program crashes.
|
| To add to that in a lot of cases the program isn't
| crashing while trying to access address 0, it's crashing
| trying to access address 200, or 1000, or something like
| that, and putting in simplistic checks isn't going to
| catch those. You could argue that the check should guard
| against accessing the lowest 1k of memory, but then when
| do you stop, at 64k? Then you have an issue with programs
| that must fit within 1k of memory.
|
| Leaving it unspecified is the better choice.
| bigstrat2003 wrote:
| > UB in the language specification allows compilers to
| optimize based on the assumption that the programs you
| write won't have undefined behavior.
|
| Given that has proven to be a completely false assumption,
| I don't think there's a justification for compilers
| continuing to make it. Whatever performance gains they are
| making are simply not worth the unreliability they are
| courting.
| fooker wrote:
| > Given that has proven to be a completely false
| assumption
|
| This part is correct. The problem is in how to deal with
| this. If you want the compiler to correctly deal with
| code having undefined behavior, often the only
| possibility is to assume that all code has undefined
| behavior. That means, almost every operation gets a
| runtime branch. That is completely incompatible with how
| modern hardware works.
|
| The rest is wrong, but again, this is a common
| misconception. Language designers and compiler writers
| are not idiots, contrary to popular belief. UB as a
| concept exists for a reason. It's not for marginal
| performance boosts, it is to enable _any_ compiler based
| transformation, and a notion of portability.
| jay-barronville wrote:
| > Suppose we want to make division by zero and null pointer
| dereference defined.
|
| A good example is WebAssembly*--address 0x00000000 is a
| perfectly fine and well-defined address in linear memory.
| In practice though, most code you'll come across targeting
| WebAssembly treats it as if dereferencing it is undefined
| behavior.
|
| * Of course WebAssembly is a compiler target rather than a
| language, but it serves as a good example of the point
| you're making.
| samatman wrote:
| Zig, a language which is explicitly aimed at the same
| domain as C, has an improved semantics for all of these
| things.
|
| If a pointer can be null, it must be an optional pointer,
| and you must in fact check before you dereference it. This
| is what you want. Is it ok to write a program which
| segfaults at random because you didn't check for a pointer
| which can be null? Of course not. If you don't null-check
| the return value of e.g. malloc, your program is invalid.
|
| But the benefit is in the other direction. Careful C checks
| for null before using a pointer, and keeping track of
| whether null has been checked is a manual process. This
| results in redundant null checks if you can't statically
| prove (by staring at the code and thinking very hard) that
| it isn't null. So in practice you're likely to have a
| combination of not checking and getting burned, and
| checking a pointer which was already checked. To do
| otherwise you have to understand the complete call graph,
| this is infeasible.
|
| Zig doesn't do any of this. If it's a pointer, you can
| safely dereference it. If it's an optional pointer, you
| _must_ check, and then: it 's a pointer. Safe to pass down
| the call stack and freely use. If you want C behavior you
| can always YOLO and just say `yoloptr.?.*`.
|
| Overflow addition and divide by zero are _safety checked_
| undefined behavior, a critical concept in the
| specification. They will panic with a stack trace in debug
| and ReleaseSafe mode, and blow demons out of your nose in
| ReleaseFast and ReleaseSmall modes. There 's also +% for
| guaranteed wraparound twos-complement overflow, and +| for
| saturating addition. Also `@addWithOverflow` if your jam is
| checking the overflow bit. Unwrapping an optional without
| checking it is also safety-checked UB: if you were wrong
| about the assumption that the payload carries a value,
| you'll get a panic and stack trace on the line where you
| did `yolo.?`.
|
| Shift operations require that the right hand side of the
| shift be a type log2(Type.bitwidth) of the left hand side.
| Zig allows integers of any width, so for a: u64, calling a
| << b requires that b be a u6 or smaller. Which is fine: if
| you know values will be within 0..63, you declare them u6,
| and if you want to shift on a byte, you truncate it: you
| were going to mask it anyway, right? Zig simply refuses to
| let you forget this. Addition of two u6 is just as fast as
| addition of the underlying bytes because of, you got it,
| safety-checked undefined behavior. In release mode it will
| just do what the chip does.
|
| There's a common theme here: some things require undefined
| behavior for performance. Zig does what it can to crash
| your program if that behavior is exhibited while you're
| developing it. Other things require that you take some
| well-defined actions or you'll get UB: Zig tracks those in
| the type system.
|
| You'll note that undefined behavior is very much a part of
| the Zig specification, for the same reasons as in C. But
| that's not a great excuse to make staying within the
| boundaries of defined behavior as pointlessly difficult as
| it is in C.
| fooker wrote:
| Yes, you can surely improve things from C. C is not a
| benchmark for anything other than footguns per line of
| code.
|
| The debug modes you mention are also available in various
| forms in C and C++ compilers. For example ASan and UBSan
| in clang will do exactly what you have described. The
| question is, then whether these belong in the language
| specification or left to individual tools.
| pjmlp wrote:
| As proven multiple times throughout the computing
| history, individual tools are optional, and as such used
| less often than they actually should be.
|
| Language specification is unavoidable when using said
| language.
| lstodd wrote:
| mwahahaha. as if there is some divine "language
| specification" which all compilers adhere to on pain of
| eternal damnation.
|
| no such thing ever existed.
| pjmlp wrote:
| Given that one can write Fortran in any language, maybe
| you're right.
| fooker wrote:
| Have you wondered why Rust or Python do not have a
| specification?
|
| For a bunch of languages outside the C-centric world,
| specifications don't exist.
| rcxdude wrote:
| it's not just in debug modes. It should be the standard
| in release mode as well (IMO the distinction shouldn't
| exist for most projects anyway). ASan and UBSan are
| explicitly not designed for that.
| fooker wrote:
| Well, give it a try.
|
| If you can make it work in a way that has acceptable
| performance characteristics, every systems language will
| adopt your technique overnight.
| rcxdude wrote:
| I use rust, which already does this.
| fooker wrote:
| Signed overflow is officially a 'bug' in rust, it traps
| in debug mode but silently follows LLVM/platform behavior
| in release mode.
|
| Huh, doesn't that sound familiar?
| steveklabnik wrote:
| > silently follows LLVM/platform behavior
|
| This is not the case. It's two's compliment overflow.
|
| Also, since we're being pedantic here: it's not actually
| about "debug mode" or "release mode", it is tied to a
| flag, and compilers must have that flag on in debug mode.
| This gives the ability to move release mode to also
| produce the flag in the future, if it's decided that the
| overhead is worth it. We'll see if it ever is.
|
| > Huh, doesn't that sound familiar?
|
| Nope, it is completely different from undefined behavior,
| which gives the compiler license to do anything it wants.
| These are well defined semantics, the polar opposite of
| UB.
| samatman wrote:
| Worth noting that Zig has ReleaseSafe, which safety-
| checks undefined behavior while applying any
| optimizations it can given that restriction.
|
| The more interesting part is that the mode can be
| individually modified on a per-block basis with the
| @setRuntimeSafety builtin, so it's practical to identify
| the performance-critical parts of the program and turn
| off safety checks only for them. Or the opposite:
| identify tricky code which is doing something complex,
| and turn on runtime safety there, regardless of the build
| status.
|
| That's why this sort of thing should be part of the
| specification. @setRuntimeSafety would be meaningless
| without the concept of safety-checked undefined behavior.
|
| I would say that making optionals and fat pointers
| (slices) a part of the type system is possibly more
| important, but it all combines to give a fighting chance
| of getting user-controlled resource management correct.
|
| Given the topic of the Fine Article, it's worth briefly
| noting that `defer` and `errdefer` are keywords in Zig.
| Both the test allocator, and the GeneralPurposeAllocator
| in safe mode, will panic if you leak memory by forgetting
| to use these, or rather, forget to free allocations
| generally. My impression is that the only major category
| of memory bugs these tools won't catch in development is
| double-free, and that's being worked on.
| rperez333 wrote:
| I know nothing about Zig, but this is pretty interesting
| and looks well designed. Linus was recently very mad when
| someone suggested a new semantics for overflow:
|
| ---- I'm still entirely unconvinced.
|
| The thing is, wrap-around is not only well-defined, it's
| _common_ , and _EXPECTED_.
|
| Example: static inline u32
| __hash_32_generic(u32 val) { return
| val * GOLDEN_RATIO_32; }
|
| and dammit, I absolutely DO NOT THINK we should annotate
| this as some kind of "special multiply". ---
|
| Full thread: https://lore.kernel.org/lkml/CAHk-=wi5YPwWA8
| f5RAf_Hi8iL0NhGJ...
| bregma wrote:
| Unsigned integer overflow is not undefined in C or C++.
| You can rely on how it works.
|
| Signed integer overflow, on the other hand, is undefined.
| The compiler is allowd to assume it never happens and can
| re-arrange or eliminate code as it sees fit under that
| assumption.
|
| How many lines will this code print?
| for (int i = INT_MAX-1; i < 0; ++i) printf("I'm in
| danger!\n");
| jcranmer wrote:
| > The thing is, wrap-around is not only well-defined,
| it's common, and EXPECTED.
|
| No, it's really not. Do this experiment: for the next ten
| thousand lines of code you right, every time you do an
| integer arithmetic operation, ask yourself if the code
| would be correct if it wrapped around. I would be shocked
| if the answer was "yes" in as much as 1% of the time.
|
| (The most recent arithmetic expression I wrote was
| summing up statistics counters. Wraparound is most
| definitely _not_ correct in that scenario! Actually, I
| suspect saturation behavior would be _more often_ correct
| than wraparound behavior.)
|
| This is a case where I think Linus is 100% wrong. Integer
| overflow is frequently a problem, and demanding the
| compiler only check for it in cases where it's wrong
| amounts to demanding the compiler read the programmer's
| mind (which goes about as well as you'd expect). Taint
| tracking is also not a viable solution, as anyone who has
| implemented taint tracking for overflow checks is well
| aware.
| cozzyd wrote:
| It depends heavily on context.
|
| For the kernel, which deals with a lot of device drivers,
| ring buffers, and hashes, wraparound is often what you
| want. The same is likely to be true for things like
| microcontroller firmware and such.
|
| In data analysis or monte carlo simulations, it's very
| rarely what you want, indeed.
| jcranmer wrote:
| Is it really?
|
| For example, I opened up https://elixir.bootlin.com/linux
| /latest/source/drivers/firew... as a random source file
| in the Linux kernel, and I didn't see a single line where
| wraparound would be correct behavior.
|
| There are definitely cases where wraparound behavior is
| correct. There are also cases hard errors on overflow
| isn't desirable (say, statistics counters), but it's
| still hard to call wraparound the correct behavior (e.g.,
| saturation would probably work better for statistics than
| wraparound). There are also cases where you could
| probably prove that overflow can't happen. But if you
| made the default behavior a squawk that wraparound
| occurred, and instead made developers annotate all the
| cases where that was desirable to silence the squawk,
| even in the entire Linux kernel, I'd suspect you'd end up
| with fewer than 1000 places.
|
| This is sort of the point of the exercise--wraparound
| behavior is often what you want _when you think about
| overflow_ , but you actually spend so much of your time
| _not_ thinking about it that you miss how frequently
| wraparound behavior isn 't what you wanted.
| cozzyd wrote:
| I think wraparound generally is better for statistics
| counters like the ones in the linked code, since often
| you want to check the number of packets/errors per some
| time interval, which you can do with overflow (as long as
| the time interval isn't so long that you overflow within
| a period) but not with saturation.
| samatman wrote:
| I think it's critical that we do annotate it as a special
| multiply.
|
| If wraparound is ok for that particular multiplication,
| _tell the compiler that_. As a sibling comment says, this
| is seldom the case, but it does happen, in particular,
| expecting byte addition or multiplication to wrap around
| can be useful.
|
| The actual expectation of the vast majority of arithmetic
| in a computer program is that the result will be correct
| in the ordinary schoolyard sense. While developing that
| program, it should absolutely panic if that isn't the
| case. "Well defined" doesn't mean correct.
|
| I don't understand your objection to spelling that `val
| *% GOLDEN_RATIO_32` is. When someone sees that
| (especially you, later, coming back to your own code) it
| clearly indicates that wrapping is expected, or at least
| allowed. That's good.
| pjmlp wrote:
| Even languages like Modula-2 and Ada, among others, had
| better semantics than C, but they didn't come for free
| alongside UNIX.
| dralley wrote:
| And yet somehow languages such as Rust, which have no UB
| (in the safe subset) manages to be within 5% of C and often
| faster in both real-world codebases and microbenchmarks.
| fooker wrote:
| It's just a change in jargon for 'marketing' reasons.
|
| For example: Rust will silently wrap signed integers in
| release mode even when it's considered a bug and crashes
| in debug mode.
| Slyfox33 wrote:
| Signed overflow is not UB in rust. That's not the same
| thing at all.
| fooker wrote:
| It's not.
|
| You have missed my point.
| rcxdude wrote:
| That is pretty much the only example where there's a
| compromise between performance and correctness as a
| difference between release and debug mode, and note that
| it's a) not undefined behaviour and b) does not violate
| any of rust's safety guarantees.
|
| Every other example you mention is done by rust in
| release mode and the performance impact is minimal, so I
| would say it's a good counterexample to your claims that
| defining these things would hamstring performance (signed
| integer overflow especially is an obvious no-brainer for
| defining. Note that doesn't necessarily mean overflow
| checks! Even just defining the result precisely would
| remove a lot of footguns).
| dooglius wrote:
| > Now every time you write a/b or *x, the compiler will be
| forced to emit an extra branching check before this
| operation.
|
| This is wrong, because you would define them to have the
| behavior that the architecture in question does, so no
| changes would be needed. For integer division this would
| mean entering an implementation-defined exceptional state
| that does not by default continue execution (on Linux,
| SIGFPE with the optional ability to handle that signal).
| For dereferencing a pointer, it should have the same
| semantics as a load/store to any other address--if
| something is there it works normally, if the memory is
| unmapped e.g. for typical Linux x86 programs you get
| SIGSEGV (just as you would for accessing any other unmapped
| address).
| fooker wrote:
| Okay, you get half of the story.
|
| Suppose now, there are two architectures with slightly
| differing behavior.
|
| Can the compiler still optimize signed x + 1 > x to true?
| kimixa wrote:
| I feel the meme of "Undefined Behavior" has been massively
| exaggerated on the internet - the vast majority of examples
| appear to be extreme toy examples using the weirdest
| contrived constructs, or things that are _expected_ to fault
| and you 're already using platform-specific information to
| know what that would look like (e.g. expecting a segmentation
| fault). It's a Scary Boogyman That Will Kill You, not
| something that can be understood, managed, and avoided if
| necessary.
|
| And even then there are tools to help define much of that -
| if you want well defined wrapped signed integers, great. If
| you want to trap on overflow, there's an option for that.
| Lots of compiler warnings and other static analysis tools
| (that would just be default-rejected by the compiler today if
| it didn't have historical baggage, but they exist and can be
| enabled to do that rejection).
|
| Yes, there's many issues with the ecosystem (and tooling -
| those options above _should_ be default IMHO), but massively
| overstating them won 't actually help anyone make better
| software.
|
| And other languages often have similar amounts of "undefined
| behavior" - but just don't document it as such, relying on a
| single implementation being "Defined Correct", and hope
| they're not actually being relied on if anything changes.
| Just like C, only undocumentated.
| adrianN wrote:
| I don't feel like the cause of most (all?) memory safety
| bugs has been "massively exaggerated".
| kimixa wrote:
| If you removed every case of "Undefined Behavior" from
| the C spec, you'd still have memory safety bugs. Because
| they're orthogonal (though may be coupled if they come
| from the same core logic error).
|
| This is what I mean by it becoming "meme" - things like
| "Undefined Behavior" or "Memory Safety" have become a
| discussion-ending "Objective Badness", hiding the real
| intent - being "Languages I Do No Like" (or, most often,
| are a poor fit for the actual job I'm trying to do. Which
| is fine, but not rejecting that those jobs actually
| exist).
|
| But they mean real things that we can improve in terms of
| software quality, and safety - but that's rarely the
| intended result when those terms are now brought up. And
| many things we can do _right now_ with existing systems
| to improve things, to not throw away huge amounts of
| already well-tested code. To do a staged improvement, and
| not let "perfect" be the enemy of better.
| adrianN wrote:
| I suppose there are ways to make the undefined behavior
| defined that preserve memory unsafety, so you're
| technically correct. In practice one would probably
| require safe crashes for OOB access etc.
| actionfromafar wrote:
| I can give an example on how to remove all undefined
| behaviour and preserve memory unsafety. First, we decide
| that all compilers compile to a fixed instruction set
| running on a CPU with a fixed memory model. Just pick one
| of the existing ones, like a 68000 or a 80486DX. Then, we
| decide that all unitialized memory is actually 0, always,
| from the operating system and the allocator. That should
| go pretty far or am I missing something?
| throwaway2037 wrote:
| > You don't want C, you want a language that actually gives
| defined semantics to all combinations of language constructs.
|
| So, Zig?
| sixfiveotwo wrote:
| Well, perhaps a subset of it, since it also introduces
| concepts that do not exist in C (eg. exceptions).
| samatman wrote:
| Zig does not have exceptions, what it has is error sets.
| It uses the words try and catch, which does cause
| confusion, but the semantics and implementation are
| completely different.
|
| If a function has an error type (indicated by a ! in the
| return type), you have a few options. You can use `result
| = try foo();`, which will propagate the error out of the
| function (which now must have ! in its signature). Or you
| can use `result = foo() catch default;` or `result =
| foo() catch unreachable;`. The former substitutes a
| default value, the latter is undefined behavior if
| there's an error (panic, in debug and ReleaseSafe modes).
|
| Or, just `result = foo();` gives `result` an error-union
| type, of the intended result or the error. To do anything
| useful with that you have to unwrap it with an if
| statement.
|
| It's a different, simpler mechanism, with much less
| impact on performance, and (my opinion) more likely to
| end up with correct code. If you want to propagate errors
| the way exceptions do, every function call needs a `try`
| and every return value needs a ! in the return type.
| Sometimes that's what you need, but normally error
| propagation is shallow, and ends at the first call which
| can plausibly do anything about the error.
| planede wrote:
| How do you define a buffer overflow?
| theeandthy wrote:
| Agreed 100%. C is what it is and that's a good thing.
|
| However, if I were to request a feature to the core language it
| would be: NAMESPACES. This would clean up the code
| significantly without introducing confusing code paradigms.
| hgs3 wrote:
| Namespaces are nice, but to my knowledge require name
| mangling which isn't a thing in C. I'm curious what you mean
| by "clean up the code significantly" and "confusing code
| paradigms" because in C you typically prefix your functions
| to prevent name collisions which isn't confusing or too noisy
| in my subjective opinion.
| theeandthy wrote:
| Yeah you're right. I guess folks who want C++ stuff should
| just use C++...
|
| I guess I should have reworded. I don't expect that feature
| in C, but if I were to reinvent C today I would keep it the
| same but add namespace and mangling.
|
| Adding an explicit prefix to every function call is a lot
| boilerplate when it's all added up.
| pjmlp wrote:
| Name mangling is an implementation detail to fit into UNIX
| linker design space, not the same approach as other
| compiled languages with modules, with their own linker.
| gpderetta wrote:
| Also name mangling (which in this case would simply be
| appending the namespace name to the identifier) would be
| trivially implementable in C.
|
| In fact on some targets the assembler name of identifiers
| doesn't always match the C name already.
|
| Although as someone almost always explicitly qualifies
| names, typing foo_bar is not very different from
| foo::bar; the only minor advantages are that you do not
| have to use foo:: inside the implementation of foo itself
| and the ability to use aliases.
| planede wrote:
| > which in this case would simply be appending the
| namespace name to the identifier
|
| surely not. How do you differentiate these two functions?
| void fooN(void); namespace N { void
| foo(void); }
| gpderetta wrote:
| [I meant to write prepend, but that doesn't change the
| argument]
|
| You would mangle it as something like foo$N depending on
| the platform.
| gpderetta wrote:
| > a simple statement like `a = b++;` can mean multiple
| constructors being called, hidden allocations, unexpected
| exceptions [...]
|
| Yes, nothing like that is possible in C
|
| https://godbolt.org/z/Ge4EqzznT
| jay-barronville wrote:
| Haha. You can't be serious--what's the likelihood of running
| into C code like this in anything remotely serious (compared
| to the millions upon millions of lines of innocent-looking
| C++ code that does like a dozen different things under the
| hood)?
| gpderetta wrote:
| No true Scotsman.
|
| I assume you haven't looked at the expansion of errno
| lately?
|
| edit: also
|
| https://github.com/KxSystems/kdb/blob/master/c/c/k.h
| quietbritishjim wrote:
| The difference is that in C++ it's expected that you'll
| overload operators, provide implicit conversions and throw
| exceptions. Of course you can write terrible code in C but it
| is not common accepted practice to hide a longjmp in a macro
| disguised as an identifier.
| gpderetta wrote:
| pthread_cleanup_{push,pop}
| jay-barronville wrote:
| The funny thing is, examples of macro craziness only
| strengthen my point, because C++ inherits all of that _in
| addition to_ its hidden behaviors and magical semantics.
| It's rare to find serious C code doing a lot of crazy
| things behind macros. In my experience, the few exceptions
| I can think of include the GMP library and data structure-
| related code trying to emulate generics (mostly hash
| tables).
| gpderetta wrote:
| Yes, C++ is a larger language for sure. But because it
| has better abstraction facilities, macro hackery is less
| common.
| pjmlp wrote:
| Indeed, you hide longjump in a #define macro instead, with
| a do while block trick.
| fargle wrote:
| that's a deliberately unfair comparison. operator
| overloading, constructors, assignments, etc. happen "under-
| the-hood" in c++ and are standard language features.
|
| whereas you can see the user-defined macro definition of "b"
| at the top of the file. you can't blame the c language for
| someone choosing to write something like that. sure it's
| possible, but its your choice and responsibility if you do
| stupid things like this example.
| gpderetta wrote:
| Macros are also standard C features, and good luck figuring
| out that an identifier is a macro without IDE help when the
| definition is buried in some header.
| fargle wrote:
| what you say is partially true (you can also of course
| use -E to check macros) but:
|
| - macros are also standard C++ features too, so this
| point doesn't differentiate between those languages
|
| - i'm failing to adequately communicate my point. there's
| a _fundamental_ difference practically and
| philosophically between macro stupidity and C++ doing
| things under-the-hood. of course a _user_ (you, a co-
| developer, a library author you trusted) can do all sorts
| of stupid things. but it 's visible and it's written in
| the target language - not hard-coded in the compiler.
|
| yes - sure, good luck finding the land-mine "b" macro if
| it was well buried. but you _can_ find it and when you do
| find it, you can see what it was doing. you can #undef
| it. you can write your _own_ version that isn 't screwed
| up, etc.
|
| you can do none of those things for operations in c++
| that occur automatically - you can't even see them except
| in assembly.
| gpderetta wrote:
| > there's a fundamental difference practically and
| philosophically between macro stupidity and C++ doing
| things under-the-hood. of course a user (you, a co-
| developer, a library author you trusted) can do all sorts
| of stupid things. but it's visible and it's written in
| the target language - not hard-coded in the compiler
|
| I specifically reject this. Constructors, exceptions, and
| so on are as similarly visible at the source level as
| macro definitions.
|
| And thanks to macros, signal handling, setjmp,
| instrumentation, hardening, dynamic .so resolution,
| compilers replacing what look like primitive accesses
| with library functions, any naive read of C code, is,
| well, naive.
|
| I'm not claiming C++ superiority here [1], I'm trying to
| dispel the notion that C is qualitatively different from
| C++ form a WYSIWYG point of view, both theoretically and
| in practice.
|
| [1]although as I mentioned else, other C++ features means
| that macros see less use.
| tsegratis wrote:
| wishlist
|
| 1) labels as values in standard 2) control over memory position
| offsets, without linker script
|
| other than that a few more compiler implementations offering
| things like checked array bounds, and a focus on correctness
| rather than accepting the occasional compiler bug
|
| the rough edges like switch fallthrough are rough, but easy to
| work around. They don't need fixing (-pedantic fixes it
| already, etc)
|
| maybe more control over assembly generation, such as exposing
| compilation at runtime; but that is into the wishful end of
| wishlists
| hgs3 wrote:
| > Please don't ruin C.
|
| Exactly this. C++ folks should not approach C like a "C++
| lite". I appreciate the authors candid take on the subject.
|
| As for defer, there is _some_ existing precedent like GCC and
| Clang 's __attribute__((cleanup)), but - at least for me - a
| simple "goto cleanup;" is usually sufficient. If I understand
| N3199 [1] correctly, which is the authors proposal for
| introducing defer in C, then "defer" would be entirely a
| compile-time construct. Essentially just a code transformation
| to inject the necessary cleanup at the right spots. If you're
| going to introduce defer to C then that does seem like the
| "best" approach IMO.
|
| [1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3199.htm
| smallstepforman wrote:
| We C++ devs haved moved away from C decades ago, and frankly
| dont even think of it any more, and will never go back. Its a
| relic of its time, like DOS, Amiga etc. RAII is a big feature
| we can no longer live without. The type system and
| overloading is fantastic. And std::vector is a magnificant
| feature. A language without these features is a relic for us
| C++ devs.
|
| And yes, I also agree that C++ has WTF insanity, like 17 or
| so initialisation quirks, exceptions in general (primarily to
| address failures in constructers, surely there must be a
| better way, also OOM / bad_alloc is a relic from the past),
| and unspecified sizes for default built in types (thats C
| heritage).
| uecker wrote:
| I moved from C++ back to C and found that I am much more
| productive not worrying about a lot of things. But it takes
| a while to figure out how to do things in C because almost
| nothing comes out-of-the-box.
| atn34 wrote:
| > provided you understand the semantics of your target
| architecture
|
| Unless you're writing inline assembly or intrinsics or
| something like that, the semantics of your target architecture
| are quite irrelevant. If you're reasoning about the target
| architecture semantics that's a pretty good indication that
| what you're writing is undefined behavior. Reasoning about
| performance characteristics of your target architecture is
| definitely ok though.
| riku_iki wrote:
| > a simple statement like `a = b++;` can mean multiple
| constructors being called, hidden allocations, unexpected
| exceptions, unclear object hierarchies, an overloaded `++`, an
| overloaded `=`, etc.
|
| its just mean if you need that logic, in C you would write lots
| of verbose less safe code.
| pjmlp wrote:
| Only if you mean C as defined by K&R C, and its original use
| when porting UNIX.
| pornel wrote:
| > "just ban simple automatic storage duration structure copying"
| is a terrible usability and horrific ergonomics decision to make
|
| This sounds like a great idea to me! Rust disables implicit
| copying for structs with destructors, and together with move-by-
| default, it works really well. Unlike PoD structs, you don't need
| to heap allocate them to ensure their uniqueness. Unlike copy
| constructors, you don't need to worry about implicit copies.
| Unlike C++ move, there's no moved-from junk value left behind.
| meinersbur wrote:
| IMHO it would still be a useful feature, just one that is not
| strictly needed. You are not able to to pass around such
| structs, but neither is that possible with the suggested
| `defer` statement. The only advantage of `defer` is that the
| destructor code is inline, rather than in a separate destructor
| function.
|
| But you could gain reusability of headers to be also used in
| C++, not needing to reinvent the wheel with new issues (e.g.
| variable lifetime), and a whole lot of existing experience with
| RAII.
| tialaramex wrote:
| > Rust disables implicit copying for structs with destructors
|
| "Disabling" is maybe not the right way to think about it. Rust
| only has "implicit copying" for Copy types, so you have to at
| the very least #[derive(Copy,Clone)] to get this, it's true
| that you can't (and therefore neither can a derive macro) impl
| Copy on types which implement Drop and that's on purpose but
| you're making a concrete decision here - the answer Rust knows
| is never correct is something you'd have to ask for
| specifically, so when you ask it can say "No" and explain why.
|
| Lots of similar behaviour in C++ is silent. Why isn't my Doodad
| behaving the way I expected? I didn't need to ask for it to
| have the behaviour I expected but the compiler concludes it
| can't have that behaviour, so, it doesn't, and there's nowhere
| for a diagnostic which says "Um, no a Doodad doesn't work like
| that, and here's why!"
|
| Diagnostics are hard and C++ under-values the importance of
| good diagnostics. Rust recently landed work so libraries can
| provide improved diagnostics when you try to call them with
| inappropriate parameters. For example now if you try to
| collect() an iterator into a slice, the compiler notices that
| slice doesn't implement FromIterator and it asks FromIterator
| to explain why this can't work, whereupon FromIterator notices
| you were trying to use a slice and emits a diagnostic for this
| particular situation - if you'd tried to collect into an array
| it explains how you'd actually do that, since it's tricky - the
| slice is impossible since it's not an owning type, you need to
| collect into a container.
| potbelly83 wrote:
| I'm confused why are they trying to implement name mangling in C?
| Are they trying to use the C++ compiler to implement the RAII
| assembly code and then link that back into C? Wouldn't a smarter
| approach be to do a C version of what C++ does?
| jay-barronville wrote:
| I thought the author covered that pretty well. How would you
| make sure that function calls and object lifetimes are managed
| correctly/deterministically while also remaining compatible
| with existing C++ code and compilers without having to have
| `extern "C"` everywhere?
|
| Edit: I just reread this comment and realized the beginning of
| it could come across as a bit condescending even though that
| wasn't at all my intention. I'd edit it out, but I don't like
| doing that, so my apologies if it did come across that way!
| nicebyte wrote:
| they're not. they're saying "if we had constructors in c, we'd
| need a mechanism to allow multiple constructors for the same
| type". in c++ function overloading and mangling are used to get
| that, but it's far from the only way something like that could
| be achieved. imo that whole part could be removed, it's like a
| little distraction and doesn't really have anything to do with
| the core of their argument.
| potbelly83 wrote:
| thanks! appreciate the reply
| einpoklum wrote:
| A C++ code design note:
|
| The initial example in the article is anti-idiomatic, because it
| imbues the larger class with a RAIIness which can be limited to
| just one element of it: struct ObjectType {
| int a; double b; void* c;
| ObjectType() : a(1), b(2.2), c(malloc(30)) { }
| ~ObjectType() { free(c); } };
|
| It's only the c member that really requires any special
| attention. In this particular case. So, there should be something
| like a `class void_buffer` which is a RAII class, and then:
| struct ObjectType { int a; double b;
| void_buffer c; ObjectType() : a(1), b(2.2), c(30)
| { } };
|
| and actually, let's just not sully the set of constructors, but
| rather have: struct ObjectType { int
| a; double b; void_buffer c;
| static ObjectType make() { return ObjectType{ 1,
| 2.2, 30 }; } };
|
| and now instead of a complicated bespoke class we have the
| simplest of structs; the only complexity is in void_buffer.
| nottorp wrote:
| Yes, that's a property of C++. It tempts you to hide complexity
| under several layers of classes so you can have ... more
| classes, i guess? And job security, because it makes the code
| much harder to follow for whoever didn't write it.
| daemin wrote:
| In this case it's not complexity but a useful abstraction.
|
| It abstracts the void_buffer into its own type with proper
| correct functions for creating, (maybe copying), moving, and
| destructing the buffer. With that you get a simple type that
| you can use elsewhere without needing to remember that you
| need to free() the buffer manually before the end of the
| scope, or needing to remember how to correctly copy or move
| the buffer elsewhere.
| HelloNurse wrote:
| But compared to the example code in the article this
| void_buffer would be significantly more difficult to write
| and/or use, more verbose and less readable due to fragmentation
| into multiple classes, and (in view of future evolutions of the
| code) also less general.
| nicebyte wrote:
| I respect Jean-Heyd very much, but I'm unconvinced by this
| article. First, the whole name mangling discussion is completely
| irrelevant to the issue and can be omitted. Second, one could
| tack on both copy and move constructors on to C in order to solve
| the double-free issue, in much the same way regular ctors are
| tacked on in the email proposal. In fact, I would argue that it
| is _necessary_ because A in RAII stands for Acquisition not
| Allocation. "Acquisition" implies ownership, which can be
| transferred or shared, so your copies and moves _have_ to have a
| special meaning to them. The fact that the proposal is bad or
| incomplete does not mean that it is "impossible" to have RAII in
| C. I don't claim that it _is_, but reading this did not reveal to
| me anything fundamental that would preclude RAII, only that all
| the preceding RAII proposals have been sloppy.
| scott_s wrote:
| I found the arguments compelling. The discussion on "Effective
| types" and C not having a proper concept of objects is key.
|
| Another way to think about it: even if you had defined
| constructors and destructors for a struct, you have not solved
| _when to call them_. C++ 's answer to that question is its
| sophisticated object model. C does not have one, and in order
| to answer that question, it must. It's worth noting that RAII
| was not a feature that was intentionally created in C++.
| Rather, astute early C++ developers realized it was a useful
| idiom made possible by C++'s object model.
| indigoabstract wrote:
| Yes, it meanders to much to get to the point. Which is that
| RAII doesn't work in C because unlike C++, which has a
| comprehensive type system mandated by a standard, a C program
| doesn't "know" at runtime that a struct is composed of other
| (typed) fields so it can do a proper deep field copy (or
| destruction). And implementing that type system in C doesn't
| seem feasible for practical and political reasons.
|
| I think the actual question should be "can C get automatic
| memory management like in C++ without having the equivalent of
| C++'s type system"?
|
| Though I can't put my finger on it, my intuition says it can,
| if the interested people are willing to look deep enough.
| orf wrote:
| > a C program doesn't "know" at runtime that a struct is
| composed of other (typed) fields so it can do a proper deep
| field copy (or destruction).
|
| This doesn't make sense: you don't need runtime introspection
| to do this?
| indigoabstract wrote:
| In C++, when you copy a struct instance to another
| instance, the runtime knows if any fields (to whatever
| depth) have manually defined assignment or move operators
| and will call them in the proper order. So it's a deep
| copy. The same information is used for calling any field
| constructors and destructors that are user defined.
|
| Introspection (reflection) would go even further and
| provide at runtime all the information that you have at
| compile time about an object. But that's not required for
| assignment and destruction operations to work.
|
| C doesn't have any of that, so a struct copy is just a
| shallow copy, a bit by bit copy of the entire struct
| contents. Which works pretty well, except for
| pointers/references.
| bregma wrote:
| No. Well, yes, in that if the type of an object is dynamic,
| it's possible that certain functions are resolved at
| runtime usually through a "virtual table". The static type
| of an object is only known at compile time, and all that
| the virtual dispatch does is an indirection through the
| virtual table to the static constructor or destructor as
| required, and the static special functions always know how
| to construct, copy, or destroy any subobjects.
|
| So, no, runtime introspection is not needed, but runtime
| dispatch may be needed.
| sapling-ginger wrote:
| You say "just add copy and move contructors", but that requires
| function overloading, which is exactly why he spent a third of
| the article ranting about name mangling. The point is that
| there is a tangled network of interdependent features that make
| C++ work, and you can't ""just"" take a small piece to put into
| C without dragging a whole bunch of other stuff along.
| Measter wrote:
| Maybe I just suck at reading, but I'm not sure I get the argument
| for why function overloading and constructors are required for
| RAII. Is it some interaction with C and C++'s object models that
| I clearly didn't understand?
| defen wrote:
| Me attempting to summarize the article:
|
| There are 2 ways to get C++-style RAII into C. The first way is
| to wholesale import the C++ object system into C (which means
| name mangling, all the different flavors of constructors,
| destructors, etc). Conceptually this would work, but it's never
| going to happen, because implementing that would be literally
| more work than an entire conforming C99 compiler.
|
| The second way is to just use some special function attributes
| to signify that a function runs when an object is created on
| the stack / popped off the stack. This won't work either
| because the C++ object system also solves lots of other
| problems that this simpler system just ignores (such as, what
| happens when you copy an object that has a constructor
| function).
| Measter wrote:
| Ah, I missed the "copy wholesale" aspect.
|
| When I started reading it, the first thing that came to my
| mind was the issue with copying the structs. The article
| started looking at the issue, but didn't really follow
| further with the changes needed to make it work, which is
| that you start needing to introduce tracking which instance
| is responsible for the resources and providing a way to
| transfer that responsibility (a.k.a. ownership and move
| semantics).
| masklinn wrote:
| Nah the first half of the essay is basically irrelevant, you
| need to start below that, and what I consider the meat of the
| issue is the "copy" section about two thirds down.
| wakawaka28 wrote:
| Because you can just use actual C++ lol...
| lionkor wrote:
| C has scopes. Add destructors. That's pretty much all you need to
| get most of the benefits of RAII.
|
| You can add `defer` instead, but regardless, this has nothing to
| do with C++. You can implement safety features without having to
| copy the arguably worst language in the world, C++. I like C++, I
| wrote many larger projects in it, but it sucks to the very core.
| Just add RAII to C.
| leduyquang753 wrote:
| Did you even read the article? The part where it talks about
| problems with destructors is about halfway through.
| lionkor wrote:
| They assume C++-like destructors. Other languages, like Zig,
| do a good job with syntax like `defer`
| masklinn wrote:
| They don't "assume" C++-like destructor, they're the
| primary author of the N3199 "defer" proposal for C.
|
| This is a response to people contacting / criticising them
| asking for destructors instead of defer.
| jcranmer wrote:
| The entire point of the blog post (written by the author of
| the C defer proposal) is to motivate why C should have
| defer. It is an attempt to summarize one of the most common
| criticisms of the proposal.
| masklinn wrote:
| TBF the essay is rather strangely structured, the entire _two
| thirds_ of the essay covering constructors and overloading
| has only ancillary relevance to the actual problem, Rust has
| neither and does RAII just fine after all (though it does
| have name mangling).
|
| The author even acknowledges halfway through that it's
| basically a strawman:
|
| > It's not a bad argument; after all, the entire above
| argument hinges on the idea of stealing from C++ entirely and
| copying their semantics bit-for-bit.
|
| To me, only after that does it engage with the underlying
| concept in a way which is engaging and convincing. But you've
| had to trawl through 2500 words to get to that point.
| guillaumec wrote:
| I notice more and more pushes to 'improve' C and turn it into
| something it should not become. I feel like the C++ community
| gave up on C++ because of the growing complexity and so turned to
| C with the hope of adding to it the good parts of C++ without the
| ugliness. But this is of course hopeless: every added feature
| will create new issues that will be solved with new features
| until the language becomes too complex for anyone to fully
| understand.
| fch42 wrote:
| The part in it that I don't understand is ...
|
| Again, "traditionally", one could (ab)use C++ as "C with
| extras". And it wasn't uncommon, especially in resource
| constraint usecases, to do just that. C++ without STL or
| templates, or even C++ without new/delete.
|
| This "is not C++", agree. Would a subset be enough for "using
| it like C-with-RAII" ?
|
| Given the details and pitfalls the original author lists, I
| suspect not. It's not just C programmers who "do strange
| things" and make odd choices. The language itself though "lends
| itself to that". I've (had to) write code that sometimes-
| alloca'ed sometimes-malloc'ed the same thing and then "tagged"
| it to indicate whether it needed free() or "just" the implied
| drop. Another rather common antipattern is "generic embedded
| payloads" - the struct definition ending "char data[1]" just to
| be padded out by whatever creates it to whatever size
| (nevermind type) of that data.
|
| Can you write _new_ C code that "does RAII" ? Probably. Just
| rewrite it in rust, or zig :-) Can you somehow transmogrify
| language, compiler, standard lib so that you can recompile
| existing C code, it not to "just get RAII" then at least to
| give you meaningful compiler errors/warnings that tell you how
| to change it ? I won't put money on that.
| actionfromafar wrote:
| New/delete were never that great to begin with and have now
| fallen out of style. Also, C++ is quite useful and powerful
| even without STL.
|
| https://www.youtube.com/watch?v=rX0ItVEVjHc
|
| A classic which touches on such stuff.
| bluetomcat wrote:
| > Can you write _new_ C code that "does RAII" ? Probably.
|
| You can do "manual" goto-based RAII in C, and it has been
| done for decades. The end of your function needs to have a
| cascading layer of labels, undoing what has been done before:
| if (!(x = create_x())) { goto cleanup; }
| if (!(y = create_y())) { goto cleanup_x;
| } if (!(z = create_z())) { goto
| cleanup_y; } do_something(x, y, z);
| cleanup_z: destroy_z(z); cleanup_y:
| destroy_y(y); cleanup_x: destroy_x(x);
| cleanup: return;
|
| It just takes more discipline and is more error-prone
| maintenance-wise.
| gpderetta wrote:
| except this misses the point of RAII.
| rcxdude wrote:
| That's not RAII, that's 'defer'. defer and context managers
| are both implementations of a subset of the kind of
| functionality you can get with RAII (the two missing parts
| are 1) allowing you to place an RAII object in part of a
| larger structure and have confidence it will actually be
| constructed and destructed correctly, and 2) allowing the
| representation of lifetimes which are more complex then
| just 'in this scope' via moves and copies).
| jandrewrogers wrote:
| The STL is pretty dispensable in my experience, even for
| people doing full-blown modern C++, and C++20 has made that
| particularly obvious. The most useful feature somewhat unique
| to C++ is the extensive metaprogramming facility, which only
| recently became non-arcane.
| dgellow wrote:
| Could you be more specifics? What improvements do you mean?
|
| It's not clear if you're talking about defer or RAII
| raydev wrote:
| > until the language becomes too complex for anyone to fully
| understand
|
| Like C, with its many hidden behaviors?
| tempodox wrote:
| Not hidden, the C standard spells it out. And implementation-
| defined behavior can be observed.
| raydev wrote:
| > Not hidden, the C standard spells it out
|
| I would argue that if it needs to be spelled out in a
| separate document from the code you're reading, then it's
| hidden.
| uecker wrote:
| What hidden behaviors? The only hidden behaviors I can think
| of which are somewhat problematic in C are implicit value-
| changing conversions. But one can instruct compilers to
| diagnose those.
| julian_t wrote:
| Is there a law, like Betteridge's Law, that says "the answer to
| any question that says 'why not just' is 'it isn't as simple as
| that'"?
| iainmerrick wrote:
| This is obviously a bit of a rant, and intended as such, but it's
| really well thought through and well-argued too.
|
| I haven't seen this distinction laid out so clearly before:
|
| _Every other language worth being so much as spit on either
| employs deep garbage collection (Go, D, Java, Lua, C#, etc.) or
| automatic reference counting (Objective-C, Objective-C++, Swift,
| etc.), uses RAII (Rust with Drop, C++, etc.), or does absolutely
| nothing while saying to Go Fuck Yourself(tm) and kicking the
| developer in the shins for good measure (C, etc.)._
|
| GC, ARC, RAII or GTFO, those are the options. That's right!
|
| I always come away from these discussions with more respect for
| Objective-C -- such a powerful yet simple language. I suppose
| Swift is the successor but it feels very different.
|
| Although, Obj-C only really came into its own once it finally
| gained _automatic_ reference counting, after briefly flirting
| with GC. At that point it was already being displaced by younger
| and more fashionable languages.
| OskarS wrote:
| I would say there are two more esoteric options: "no memory
| allocation at all outside of the program stack" (like... i
| dunno... lambda calculus?) and fancy-pants computer sciency
| things like linear types. No sane language does either of
| those, though.
| iainmerrick wrote:
| Good points! I tend to think of linear types as a more
| general kind of RAII -- like there's a spectrum that goes
| RAII -> borrow checker -> linear types -- but maybe it does
| warrant its own category.
| yason wrote:
| RAII is just automation and semantic sugar for something like
| this (or the equivalent set of goto labels that do the freeing at
| the end of the function): { void
| *buffer = malloc(SIZE_MAX); if (buffer) {
| if (!do_stuff(buffer)) { free(buffer);
| return; } more_stuff(buffer);
| free(buffer); } return; }
|
| If you wanted something like that in C it doesn't need to emulate
| C++ style RAII with classes and strongly typed constructors. It
| could look like something like, for example, where you just
| define pairs of allocator and free functions:
| allocdef void *autobuffer(malloc, free); ... {
| autobuffer buffer(SIZE_MAX); if (buffer) {
| if (do_stuff(buffer)) { return; }
| more_stuff(buffer); } return; }
|
| The implementation would effectively be a Lisp style macro
| expansion encoded in the C compiler (or preprocessor) that would
| just basically write out the equivalent of the first listing
| above.
| Mesopropithecus wrote:
| In the second example, buffer is still a pointer? If so, when
| does free run, and who decides that? When buffer goes out of
| scope, could do_stuff store the pointer some place else?
|
| I find this an interesting thought experiment, basically types
| that you'd opt in to RAII. Just have a feeling that you'll need
| to define some notion of ownership to make it work.
| kalekold wrote:
| Use D's -betterC flag?
|
| https://dlang.org/spec/betterc.html
| ezoe wrote:
| I really don't understand why the author is so mad at these
| armchair professionals who think they know better.
|
| You are proposing to change the C language. The risk is great
| even the smallest change will break the existing code. If you
| can't convince all of the stakeholders, it's better not to change
| it. Keep the status-quo.
| wizzwizz4 wrote:
| You underestimate the sheer diversity of existing C compilers.
| A specification change is not a significant risk to existing
| code, compared to even clang v.s. gcc compatibility issues.
| rqtwteye wrote:
| I always find it interesting to see calls to mallow and free in
| C++ code. I don't recall using malloc/free in many years in C++.
| It's always new/delete.
| PaulDavisThe1st wrote:
| If you're reading these comments (and maybe contributing to them)
| about various arcane details of C++ and the differences with C,
| there's one thing you're not doing ...
|
| ... actually writing code that gets the job done ... in C++.
| throwawaymaths wrote:
| How about instead of RAII built into the compiler, you define
| destructor functions for each datatypes that you care to and have
| a sidecar or compiler plugin or hell even a linter check that
| those destructors have been called when a variable of that type
| goes out of scope?
|
| If you miss a destructor event, without configuring the addon
| "yes I really meant that", the addon halts the compilatoin at
| best, or returns nonzero for ci at worst.
| paulddraper wrote:
| And then what if you combined the compiler+linter together.
| throwawaymaths wrote:
| Sure, as long as you don't put it in the type system.
| uecker wrote:
| This would also be my preferred solution.
___________________________________________________________________
(page generated 2024-05-23 23:02 UTC)