Hacker Read top | best | new | newcomments | leaders | about | bookmarklet login
Writing comments about pointer ownership (gpfault.net) similar stories update story
42.0 points by nice_byte | karma 957 | avg karma 3.41 2015-08-23 05:27:46+00:00 | hide | past | favorite | 42 comments



view as:

On the contrary. My team is all C (I really want to move to C++ however). So I need to leave pointer ownership comments around.

I more or less do what unique_ptr does automatically, except with comments. I note "sources" and "sinks" of pointers... and work with them in that manner.


I really wish they had found a less confusing way to bolt this move semantics / rvalue references stuff onto C++. It's an important addition, but I find myself having to do mental gymnastics every time I see code like this, whereas in Rust it just works.

I agree with you. To be honest, it took me a few tries to understand how move semantics really works (the unfortunate naming of std::move really didn't help). I'm still not sure my understanding is 100% correct according to the standard, but at least my mental model makes sense to me and seems to agree with the behavior displayed by the compiler.

The big difference is that in Rust, if you make an borrow error and try to use something you moved away, it's caught at compile time. In C++11, you dereference zero and crash, because the reference moved from was zeroed out.

>I really wish they had found a less confusing way to bolt this move semantics / rvalue references stuff onto C++.

There's some irreducible complexity with adding ownership semantics and the way standards committee ended up adding it to C++ is probably the most simplified way to accomplish it if you obey the constraint that the new compiler-assisted pointer ownership must coexist with legacy raw pointers and references.

To me, the complexity lies in the 30 years of un-annotated raw pointers in existing C++ code that must not be broken or rewritten if new ownership-style pointers are bolted on to the language.

I also think that another source of complexity/confusion is the label "move" in "std::move()". The word "move" is a verb and it seems to hint to beginners that some bytes changed position but unfortunately, that's not what's really happening. It might have been better to call it "std::rvalue()" -- the "rvalue" is a noun and you're telling the compiler that it is an "expiring value" that you promise to stop using. (Howard Hinnant[1] is the author of the std::move() C++ proposal and he gives good reasons why he called it "move" but I still think it's a confusing name.)

>, whereas in Rust it just works.

In C++, the defaults are "raw pointer" and unannotated references; the special cases are are unique_ptr, shared_ptr, &&, and std::move()

In Rust, the default is explicit borrow pointers, and the special-case pointers are raw pointers inside unsafe blocks.

Rust is "simpler" because it has the luxury of using a different baseline for its default case. C++ doesn't have that luxury.

[1]http://stackoverflow.com/a/21358433


I think most of us understand the historical pressure, but that's small comfort when we're neck-deep in arcane code. Indeed, for me it just leads to dark thoughts about the reign of Worse Is Better and how the good stuff rarely wins.

>I think most of us understand the historical pressure,

It doesn't look that way from the GP's comment: "I really wish they had found a less confusing way to bolt this move semantics / rvalue references stuff onto C++ ..."

... is actually an example of not understanding (or not being aware of) historical pressure.

If you create a new programming language (e.g. Rust) with a blank slate, you have more flexibility to make semantics "simpler" by rethinking the design of the syntax, the sigils, and the implicit defaults.

Rust syntax accomplishes "move semantics" without double ampersands (&&) sigils and "std::move()".[1] The end result is that Rust syntax looks less cluttered to move ownership around.

The key to Rust's clean syntax is the absence of "historical pressure".

Rust starts from a blank slate without worrying about breaking legacy code. Rust wasn't invented in 1982.

C++ move semantics uses C++1982/C++98 as a starting point and can't use naked variables to let the compiler carry out "moves". Legacy code would be ambiguous and compilation would break. The "rvalues std:move()" was proposed in 2002 and finalized for C++11. They had 9 years for others to challenge Howard's proposal and devise a simpler more elegant way that doesn't break existing code. Nobody came up with an alternative. Hence, the uglier C++ syntax bolted on for "moves" is probably as simple as we're going to get.

[1]http://stackoverflow.com/questions/29490670/how-does-rust-pr...


Is this provably safe? I feel like there's been a lot of discussion in the Rust community RE safeness, and my impression from the sidelines is that it's non-trivial.

> Is this provably safe?

No.


Why not use shared_ptr in this case? Why enforce single ownership?

You also need week_ptr, that holds a pointer for a week... just kidding...

But often you need weak_ptr, for caches - where shared_ptr won't free it, and unique_ptr won't allow it to be shared.


Because the example is about an idiomatic way of enforcing exclusive pointer ownership. Maybe it wasn't a very good example though. A better example might be this: in the Builder design pattern, an object is built from various "parts" by the client. It's obvious that the object should have an exclusive ownership of all of its parts. However, at the time of creation, each part belongs to whoever created it, so the ownership needs to be transferred.

  Some people might ask why I chose to make the parameter of BlackBox::store an rvalue reference rather than an lvalue reference and force the client code to use std::move. While it is true that an lvalue reference would also have worked, if I did that, it would be impossible to write code like crate.store(std::make_unique<Gadget>(beeper, flasher));. I also happen to think that forcing the client to use std::move at the call sites is good rather than bad, because it communicates the intended effect to the reader.

Passing unique_ptr by value is the correct option here, it will accept temporaries just fine.

And in my opionion, if you want safety quarantees like that, use Rust. I was a fan of the new C++ standards a while ago, but after having used them for some time, I found C is a lot nicer language. There's just way too much mental overhead when trying to write "idiomatic C++" and the compiler errors you sometimes get are just a waste of developer time.


Someone brought up this point during another discussion and initially I agreed with them. However, someone else suggested that if the function has more parameters, an exception might happen when passing the arguments for the other parameters, so the transfer of ownership won't happen, but the caller will have already lost the ownership. Passing by rvalue reference means the caller retains ownership until the very last possible moment.

Do you mean something like this? http://ideone.com/8tPPHy

Yes. It seems to work, but it might be a coincidence. The order of argument evaluation is undefined. It might be that the exception gets thrown before the ownership is transferred, but it's just luck. I have modified your example a bit, to better trace what's happening: http://ideone.com/tgh0xe

And it won't work when switching the parameter order of byval, so you're correct. That's quite nasty.

Tip: insert manual newlines into verbatim/code blocks to make them more readable:

  Some people might ask why I chose to make
  the parameter of BlackBox::store an rvalue
  reference rather than an lvalue reference
  and force the client code to use std::move.
  While it is true that an lvalue reference
  would also have worked, if I did that, it
  would be impossible to write code like
  crate.store(std::make_unique<Gadget>(beeper, flasher));.
  I also happen to think that forcing the client to use
  std::move at the call sites is good rather than bad,
  because it communicates the intended effect to the reader.

There's just way too much mental overhead

Exactly. Instead of understanding the very simple concept of which object owns, I now have to understand that concept, how std::unique_ptr works, and rvalue references? How does it work with conditional ownage?

I also mostly use C instead of C++, but I admit that automatic constructor/destructor calls and virtual functions (as long as the class hierarchy doesn't become too deep) are quite useful. Lambda expressions too. I think that as long as you can use C++ while still having an idea what the language is doing implicitly what you would've had to do explicitly in C, you can get the advantages without the disadvantages.


> Exactly. Instead of understanding the very simple concept of which object owns, I now have to understand that concept, how std::unique_ptr works, and rvalue references?

While one concept is simpler, the other is controlled by compiler, while object ownership is only controlled by the developer himself.


Unfortunately this advice fails for forward references, which is one of the most common reasons for using a pointer (e.g. the pimpl idiom). An example:

    #include <memory>
    struct Gadget;
    struct BlackBox {
        std::unique_ptr<Gadget> contents;
        ~BlackBox();
    };
    
    void func() {
        BlackBox b;
    }

This will fail to compile (for reasons I don't understand - can anyone explain?) But replace the unique_ptr with a raw pointer and it will be fine.

I think it should work if you define the ctor and dtor for BlackBox in a separate .cpp file where you include gadget.h.

The reason it fails to compile is probably that the implicitly generate constructor of BlackBox (which is inline, I guess) may need to call the dtor of unique_ptr in case something in the constructor throws an exception.


You're right, that works! Thanks, I would not have arrived at that on my own.

This is required by the standard. unique_ptr<Gadget> in your example uses default_delete<Gadget> deleter, which in turn requires that type Gadget is complete at the point where this deleter is used, otherwise program is ill-formed.

Intention is to avoid undefined behaviour in situation where you delete an object with incomplete class type at the point of deletion, that has a non-trivial destructor or a deallocation function.

Edit: An deleter is used because of default generated copy constructor.


That doesn't sound right. unique_ptr does not have a copy constructor so there should not be an implicit copy ctor (am I right?) Exception handling in the default ctor I think is right.

The reason why that code fails is explained by gcc's unique_ptr implementation:

    /// Primary template of default_delete, used by unique_ptr
    template<typename _Tp>
      struct default_delete
      {
    ...
        void
        operator()(_Tp* __ptr) const
        {
          static_assert(!is_void<_Tp>::value,
                        "can't delete pointer to incomplete type");
          static_assert(sizeof(_Tp)>0,
                        "can't delete pointer to incomplete type");
          delete __ptr;
        }
      };
Basically, invoking "operator delete" on a pointer to an incomplete type (aka opaque pointer) is usually undefined. Calling "operator new" on an incomplete type is not possible (because its size is 0). Since unique_ptr deals with allocation/deallocation, and doesn't just copy pointer values around, it's not fully comparable to a plain pointer.

nice_byte proposed a good solution: if you can, abstract the new/delete logic to another file, in which the complete type of "Gadget" is known.


That's true, but it's not obvious why anything in that code calls new or delete, since the destructor is defined elsewhere. So why does the constructor invoke operator delete?

The missing puzzle piece is apparently exception handling: if the implicit constructor throws (which of course it cannot) it must delete this object (which of course will be null). Sadly this silly requirement persists even when exception handling is disabled at the compiler level.


Ugh, if you bring up Rust in this thread it's clear you have little to no experience actually writing large amounts of system-level code. Rust has been around for all 2-3 years now? 1.0 for what? Months? What large successful Rust code bases have you worked on? What have you actually built with Rust?

C++ is for people who work on large mature codebases, deal with practical issues, and actually build stuff. Talk to me about the virtues of Rust when you actually have real experience.


> What large successful Rust code bases have you worked on? What have you actually built with Rust?

> C++ is for people who work on large mature codebases, deal with practical issues, and actually build stuff. Talk to me about the virtues of Rust when you actually have real experience.

According to the GitHub stats, I've added 293,494 lines of Rust code to Servo and 381,084 lines of code (mostly Rust) to the Rust compiler. I find that Rust's pointer ownership rules help improve the reliability and security of my code significantly.


Your Rust stats are irrelevant. You're literally the creator of Rust.

Outside of you, the majority of "users" of Rust are script-kiddies who think it's "cool" (like Haskell cool) and will likely never write any native application of any significance using it (or any low-level language for that matter).


Given that you seem to think that pcwalton is the creator of Rust, I'm guessing that your professed knowledge of the demographics of the Rust community is also entirely fantastical. Would you like to cite a source for your ad-hominem throwaway comments?

I think rust has its merits, don't get me wrong. I just think it has a large cargo cult following of inexperienced script kids who have never used it for anything other than pretending to know what they're taking about.

And what makes you think that?

Well I have no source proper since it's just an opinion I hold and not a fact. But just looking at some of the comments in thread, they essentially boil down to:

"Dur I'm so smart, Rust makes this obsolete, C++ got it 'wrong'"

Like man, do you want a gold star? WE ALL KNOW ABOUT THE BORROW CHECKER. You're not informing anyone, you're just publicly masturbating on HN. 99% chance it's just some know-it-all talentless n00b who's proficient in JS and loves react.js.

"Wah!!! Move semantics make my bwain hoyt!! I got a segfault :( C++ makes me feel bad about myself. But it's okay because it's old and obsolete, Rust is better."


> C++ is for people who work on large mature codebases

Sure, C++98 or C++03.


While it's true that most people have to deal with lots of legacy code, as long as their compilers are up-to-date, it doesn't prevent them from using new language features.

In practice it often does, because introducing brand new patterns alongside existing ones may be frowned upon in a large project.

Consider a codebase that uses the "comment indicates ownership transfer" pattern everywhere. Would it improve the code quality if one developer suddenly introduced unique_ptr, std::move and rvalue references in just one place in the API, when every other call would still be using the old style?

To my taste, the mental overhead of having two different ownership patterns would outweigh the advantage of the new compiler checks (which in this case would be mostly theoretical, since the devs working on this project would presumably feel at home with the old pattern).


    > Rust has been around for all 2-3 years now? 1.0 for what? Months? 
Rust as a project has been around for eight years in total, about twoish in a similar fashion to what it is today. It's been 1.0 for three months.

I believe bringing up Rust in a discussion like this is completely warranted. After all, both Rust and C++ try to achieve similar goals (C++ also has a goal of maintaining backward compatibility with decades worth of code, which Rust doesn't yet have). Rust might not be quite there yet, but I believe the future is either Rust (or other similar language) becoming "mainstream", or C++ changing radically (and offering a "compatibility mode", perhaps).

The worse thing about comments about pointer ownership are the big teams with lots of programmer attrition.

Eventually no one knows any longer how those comments map to the real code, because lots of those ever changing developers never update them.

Which leads to days delving into pointer related bugs.

If one is lucky to work in a shop that allows for modern C++, then it is great being able to use all the available features to avoid this type of situation.


Legal | privacy