Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> OK, so you can't use references. Then, as I said before, your pointer replacements have a runtime performance cost worse than GC write barriers.

The library provides three types of pointers - "registered", "scope" and "refcounting". I believe you are referring to the registered pointers, that indeed have significant cost on construction, destruction and assignment. But registered pointers are really mostly intended to ease the task of initially porting legacy code. New or updated code would instead use either "scope" pointers, which point to objects that have (execution) scope lifetime, or "refcounting" pointers. Scope pointers have zero extra runtime overhead, but are (at the moment) lacking the needed "static enforcer" to ensure that scope objects are indeed allocated on the stack. (Their type definition does prevent a lot of potential inadvertent misuse, but not all. And Ironclad C++ does have such a static enforcer.)

> I don't think you understood me. I mean the this pointer. "this" is hardwired into C++ to be an unsafe pointer.

You're right, that's a good point. But really it's a practical issue rather than a technical one. I mean technically, use of the "this" pointer should be replaced with a safer pointer, just like any other native pointer.

For example this is technically one of the safe ways to implement it in SaferCPlusPlus:

    class CA { public:
        template<class safe_this_pointer_type, class safe_vector_pointer_type>
        void foo1(safe_this_pointer_type safe_this, safe_vector_pointer_type vec_ptr) {
            vec_ptr->clear();
            
            /* The next line will throw an exception (or whatever user specified behavior). */
            safe_this->m_i += 1;
        }

        int m_i = 0;
    }
    
    void main() {
        mse::TXScopeObj<mse::mstd::vector<CA>> vec1;
        vec1.resize(1);
        auto iter = vec1.begin();
        iter->foo1(iter, &vec1);
    }
That is, technically, if you're going to use the "this" pointer, explicitly or implicitly, you should pass a safe version of it (in this case "iter"). But yeah, in practice I don't expect people to be so diligent. I wonder how often this type of scenario arises in practice?

So do I understand correctly that the Rust language allows for the same type of code, but the compiler won't build it unless it can statically deduce that it is safe?

> Not possible. It's totally incompatible with existing C++ designs.

Even if you prohibit the unsafe elements? Including (implicit and explicit) "this" pointers?



Hmm, a more practical approach might be to mirror the GC languages and only permit (not-null) refcounting pointers as elements of dynamic containers such as vectors. Ensuring that all references don't outlive their targets, thereby eliminating the implicit "this" pointer issue. I think. Is that how Rust does it?


> Is that how Rust does it?

No, safe Rust only has safe references, and that includes "this" ("self" in Rust). Because the lifetimes are part of the type, it does not require the runtime overhead of reference counting.


Rusts references behave like plain raw C/C++ pointers at runtime, without any bookkeeping code running at all.

The magic all lies in the compiletime borrow checker, which roughly works like this:

    - All data is accessed either through something on the stack or in static memory.
    - Accessing data, say by creating a reference to it, 
      causes the compiler to "borrow" the value for the scope in which the reference
      is alive.
    - The references can be alive for any scope equal or smaller than for which    
      access to the data itself is valid.
    - References track the original scope for which they are alive around as a 
      template-paramter-like thing called "lifetime parameter".
      Note that Rusts use of the word "lifetime" is thus a bit narrower than the
      one used in C++, since it just talks about stack scopes, and not the lifetime 
      of the actual value as would be tracked by a GC or ref counting.
      Example:

      let x = true;
      let r = &x;

      Here, r would infer to a type like `Reference<ScopeOfXVariable, bool>`.
      (The actual type in rust would be a `&'a T` with 
      'a = scope of x, and T = bool).
    - Because the scope is tracked as part of the reference type,
      it is possible to copy/move/transform/wrap references safely, since
      the compiler will always "know" about the original scope and thus can
      check that you never end up in a situation where you accidentally outlive the 
      thing you borrowed, say if you try to return a type that contains a reference 
      somewhere deep down.
    - The borrow itself acts as a compiletime read/write lock on the thing you referenced,
      so for the scope that the reference is alive for the compiler prevents
      you from changing or destroying the referenced thing. Example:

      // This errors:
      let mut a = 5;
      let b = &a;
      a = 10; // ERROR: a is borrowed
      println!("{}", *b);

      // This is fine:
      let mut c = 100;
      { 
          let d = &c;
          println!("{}", *d);
      }
      c = 50;

    - The above examples just use `&` for references, but Rust has two references types:
      - &'a T, called "shared reference", which cause "shared borrows".
      - &'a mut T, called "mutable references", which cause "mutable borrows".
    - Both behave the same in principle, but have different restrictions and guarantees:
      - A mutable borrow is exclusive, meaning no other other borrow to the same data 
        is allowed while the &mut T is alive, but allows you to freely change the T through 
        the reference.
      - A shared borrow may alias, so you can have multiple &T pointing
        to the same data at the same time, but you are not allowed to freely change T through 
        the reference.
      - (If those two cases are too rigid there is also a escape hatch that
        a specific type may opt-into to allow mutation of itself through a shared reference, with 
        exclusivity checked through some other mechanism like runtime borrow counting.)
    - Through these two reference types, Rust libraries can abstract with arbitrary APIs
      without loosing the borrow checker guarantees. Eg, the "reference to vector element"
      example boils down as this:

      let mut v = Vec::new();
      v.push(1);
      let r = &v[0]; // the reference in r now has a shared borrow on v.
      v.push(2);     // push tries to create a mutable borrow of v, which conflicts with the 
                        borrow kept alive by r, so you get a borrow error at compiletime.
      println!("{}", *r);
The important part is that all this is there, per default, for all Rust code in existence, so you can not accidentally ignore it like a library solution you might not know about, or like language features that don't know about the library solutions.


Great explanation. Thanks.




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

Search: