C++ Logo


Advanced search

Re: A new qualifier for "uniquely referable" object

From: Omer Rosler <omer.rosler_at_[hidden]>
Date: Thu, 18 Mar 2021 21:34:05 +0200
Thanks for the detailed response.
Here is my response:

This is very similar to `restrict`/`noalias`, and has the same issues (plus
> a few more issues that I think are easily severed).
> https://www.lysator.liu.se/c/dmr-on-noalias.html

Didn't know about `noalias` and reading this (was an interesting read), it
is very similar.

Quoting the article:

> The utter wrongness of `noalias' is that the information it seeks to
> convey is not a property of an object at all. `Const,' for all its
> technical faults, is at least a genuine property of objects; `noalias' is
> not, and the committee's confused attempt to improve optimization by
> pinning a new qualifier on objects spoils the language. `Noalias' is a
> bogus invention that is not necessary, and not in any case sufficient for
> its apparent purpose.

`noalias` is not a property of an object, but in C++ we have references,
and `unique_referable` is essentially a property of a reference.
This is exactly like the distinction between a `prvalue` and it's result
object. Therefore I don't think the same argument applies for this
"variant" of the idea.

1. Allow more aggressive copy elision rules:
>> ```c++
>> pair<non_copyable, int> mrv_function()
>> {
>> unique_referable non_copyable a = {};
>> //fill a
>> return {a, 42}; //OK: copy elision is mandatory here
>> }
>> ```
>> The rules to allow this are completely analogous to `prvalue`
>> materialization conversion, only we delay materialization for an lvalue.
> You seem to be assuming that `return {non_copyable{}, 42}` works with
> std::pair today. It does not, because std::pair is not an aggregate.
> https://godbolt.org/z/x6fWEW
> So forget #1; your proposal doesn't solve it.

You are correct about `std::pair`, my mistake. Consider instead any
aggregate type, and the point still stands.

2. Prevent dangling references.
>> ```c++
>> int& get_dangling()
>> {
>> unique_referable int a = 42;
>> return a; //Compile error: Can't bind reference to uniquely referable
>> object
>> }
>> ```
> This returns a dangling reference even without the extra keyword. This
> will become a hard error (again, without the need for any extra keyword)
> after P2266 "Simpler implicit move" lands. We just had an EWG telecon on
> P2266 yesterday. My impression (as the paper's author) is that we will
> likely see P2266 adopted for C++23... *contingent* on someone producing a
> reference implementation so that people can see what existing code is
> broken by it. (Interested in producing such an implementation? Send me an
> email. I am attempting to coordinate, but not implement, as implementation
> is far above my pay grade.)
> https://wg21.link/p2266 (R1 is in the March mailing, which isn't out
> quite yet)
> So forget #2; it's already solved in the core language.

Didn't know about this paper. Good to know this problem will be solved.
As for implementation, it is above my pay grade as well :)

3. Help compiler escape analysis:
>> ```c++
>> extern void foo( unique_referable int& ref);
>> int bar()
>> {
>> unique_referable int a = 42;
>> foo(a); // OK: in `foo`, the unique way to refer to the underlying
>> object is through `ref`
>> return a; //Mandatory copy elision
>> }
>> ```
> When I use my fingers to count up `a` and `ref`, I end up with two
> fingers. That's not very "unique," is it?
> (This is the main problem with `restrict`/`noalias`.)
> Copy elision already kicks in here, today, without any extra keywords;
> there's no need to make it "mandatory."

Well, within `foo` the only way to refer to the underlying object is
through `ref`, and outside `foo` the unique way is through `a`.
The `unique_referable` applies to the reference to the object, not the
underlying one (similar to the distinction between a `prvalue` and its
associated "result object").

Furthermore, URVO *explicitly cannot apply* to register types such as `int`
> (which are trivially copyable and suitably small), and so NRVO (copy
> elision) can't be made "mandatory" to apply to them either. The next time
> you present this example, use a non-trivial type such as `std::string`, not
> `int`.

You are correct, my mistake.

And then I still don't see why you need an extra keyword just to trigger
> copy elision. There are existing proposals for "mandatory NRVO," such as
> Anton Zhilin's P2025 "Guaranteed copy elision for return variables."
> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2025r1.html

> 4. Allow destructive moves
>> ```c++
>> struct A
>> {
>> A( unique_referable A&&a) = default;
>> };
>> ```
>> This move is always destructive because `a` is the unique way to access
>> the underlying object and is an rvalue-reference, therefore this move is
>> always destructive.
> No, that's not what "destructive" means at all. "Destructive" means "the
> operation is responsible for calling the destructor, i.e., the operation *takes
> ownership* of the source *object* itself." You should watch this talk,
> whose second half surveys the existing literature on "destructive move and
> relocatability."
> https://www.youtube.com/watch?v=SGdfPextuAU
> You can't have a reference type that says "give me a reference to your
> object, and btw I'm going to destroy it," unless the caller can somehow
> tell that this is going to happen and therefore refrain from
> double-destroying the object itself. And of course this might be
> path-dependent. So you end up having to make the compiler do control-flow
> analysis, and add boolean guard variables, and so on. It's a huge mess.
> Consult P2025 for some interesting examples.

Actually, the operation of binding an object to an* rvalue*
`unique_referable` reference, is essentially

> a reference type that says "give me a reference to your object, and btw
> I'm going to destroy it,"

The caller either explicitly `std::move`s, or use the implicit move rules,
which your paper P2266 refines.

This extends copy elision way beyond P2025.

For example, see how it resolves CWG #1579 <http://wg21.link/cwg1579>:

template<typename T>struct optional //overly simplified
    bool has_value;
    byte storage[sizeof(T)];

    constexpr optional(unique_referable T&& obj): has_value(true), storage()
         new (storage) T(std::move(obj)); //triggers a materialization
conversion here

    constexpr optional(T&& obj): has_value(true), storage()
//materialization occurs at reference binding
         new (storage) T(std::move(obj));

    operator bool() const
        return has_value;

    T& operator*()
        return *reinterpret_cast<T*>(storage);

optional<T> foo()

     unique_referable T t;
     //fill t
     return t;


This is achievable because the `optional` constructor takes an rvalue
`unique_referable` ref. The compiler elides the move because no
materialization occurs until the constructor body (of course this is only
allowed because the constructor is `constexpr`).
This is precisely why "destructive move"* is* covered by this proposal.

The proposal on its own doesn't solve the cases where control flow analysis
is needed.
But we could allow the compiler to add `unique_referable` to local
variables as an optimization (similar to the way NRVO is explicitly allowed
in the standard) and make this a QoI issue.

On Thu, Mar 18, 2021 at 7:31 PM Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>

> HTH,
> Arthur

Received on 2021-03-18 14:34:20