C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Relocation in C++

From: Maciej Cencora <m.cencora_at_[hidden]>
Date: Wed, 2 Mar 2022 11:55:56 +0100
I think you are missing key issue with current move semantics in C++:
std::move(x) doesn't move - it is just a cast!
x is still alive after the call to std::move(x).

What is even worse, given following code:
void consume(X&&);
void bar()
{
    X x;
    consume(std::move(x));
    ...
}

after the consume call, you have no idea whether x was actually
consumed by invoking move-constructor/move assignment operator or not,
without looking at the implementation of consume func.

The only way to solve this, is to introduce actual move/relocation as
language level construct (via e.g. proposed relocate operator), i.e.:
void consume(X x);
void bar()
{
   X x;
   consume(relocate x);
   ...
}
Now after the consume call, x is no longer 'accessible' for further
code, and the user can be sure that the consume function really
'consumed' it.

Regards,
Maciej

wt., 1 mar 2022 o 23:26 Andreas Ringlstetter via Std-Proposals
<std-proposals_at_[hidden]> napisał(a):
>
> > The larger picture here seems to be an effort to make move semantics friendlier and easier to use. I began an informal survey on open-std of papers on move semantics.
>
> Move semantics so far suffer in 2 domains:
> - There is no way in the language standard to notify the compiler that
> an object is trivially deconstrutible as it hasn't been touched after
> a specific class of constructor
> - There is no way to tell the compiler that an object which has just
> been moved shall be implicitly assumed to have returned to such a
> pristine state.
>
> In combination, unless the compiler can fully inline the object's
> destructor as well as the foreign, receiving end of the move, it can't
> *prove* that the object was trivial to destruct. You do not want
> elimination of destruction for trivial states to rely solely on the
> data flow analysis of fully inlined code though.
>
> Being able to designate constructors as "creates trivially
> destructible object" (which doesn't mean that the destructor musn't be
> called, only that it can be safely omitted if the flag can be tracked
> by pure data flow analysis, even prior to inlining) would be the first
> step.
> In the second step, it would need to be possible to also restore that
> flag when accepting an object as r-value, giving the guarantee to the
> caller that the object may be assumed to be trivially destructible
> again.
>
> For a trivially destructible object, the compiler may even shorten the
> life cycle at will, or reuse it without construction of a new object,
> if the guarantees are strong enough.
>
> What guarantees are required for the life cycle optimizations?
> - The moved object must not be aliased.
> - The moved object must be *formally* equivalent to a pristine object
> (even though *not* bitwise identical!) after the r-value expires.
>
> What guarantees must an object give to be even able to be flagged "pristine"?
> - The default constructor must explicitly yield a trivially destructible object.
> - const methods must not introduce non-trivialy-destructible state to
> the object.
>
> Is this limited to move constructors? No, a quick thought experiment
> shows this is actually universally applicable:
>
> struct unique_ptr {
> // Denotes the object to be trivially destructible when this
> constructor was called.
> // This property applies as long as the object remains pristine.
> unique_ptr() noexcept [[post=trivial]] { /* non-trivial constructor */};
>
> // Denotes that other turns trivial after the r-value reference expires
> unique_ptr(unique_ptr&& [[post=trivial]] other);
>
> // Denotes that a new object can be trivially default-constructed
> in the released storage.
> // Applies both if the destructor was called or omitted.
> ~unique_ptr() [[post=trivial]] { /* non-trivial destructor */};
>
>
> // Denotes that this turns trivial again after invocation.
> void reset() [[post=trivial]];
>
> // Implicitly changes destruction behavior back to default.
> void modify();
>
> // Does not change the current destruction behavior.
> operator bool() const noexcept();
>
> // Explicitly gives up the trivial deconstruction and switches
> back to destructor invocation.
> void mutate() [[post=default]] const noexcept;
> }
>
> int main()
> {
> // Trivially destructible, non-trivial construction
> unique_ptr foo_1;
> // No longer trivial, deconstruction behavior reset to default
> foo_1.modify();
> // Explicitly made trivial again
> foo_1.reset();
> // Not used up to the end of the scope, trivial and not aliased,
> so life cycle may now be shortened.
> // Destructor call may be omitted.
>
> // If the life cycle is shortened, the destructor *must not* be invoked!
>
> // Since foo_1 was trivial till the end of the life cycle, foo_2
> may reuse the storage!
> // In that case, foo_2 was trivially constructible.
> unique_ptr foo_2;
>
> {
> // Being aliased doesn't clear the "trivial" flag, but still
> extends the life cycle.
> // If alias can't be proven to expire, "aliased" property sticks.
> // If alias can't be proven to preserve deconstruction
> behavior, "trivial" flag is lost.
> const auto& foo_3 = foo_2;
> }
>
> // foo_2 is no longer aliased, but non-trivial now.
> foo_2.modify();
>
>
> // foo_2 returns to trivial state, foo_4 is non-trivial
> // std::move has a catch, it needs to convey the trivial attribute
> applied to the r-value back to foo_2.
> // It also mustn't accidentally set the aliased flag.
> unique_ptr foo_4(std::move(foo_2));
>
> // foo_2 may end its life-cycle without destructor invocation.
> // foo_4 is guaranteed to have its destructor invoked under all
> regular as-if rules.
> }
>
> No need for ABI changes though, it's all only about signalling the
> caller that cleanup of a usually non-trivial object has just become
> trivial, and providing the formal means to enable optimizations
> without inlining.
>
> And the behavior of the caller really must be controlled by the
> callee, as only the callee can give any guarantees about upholding
> code contracts. It's the exact same pattern as with constness.
>
> Also, everything is only a recommendation / relaxation to the callee,
> so 100% downwards compatible apart from new keywords.
>
> The only thing which smells fishy? That all the other proposals all
> attempted an imperative, caller controlled semantic, while I somehow
> can't find any rationale to do so.
> It's complementary with the "bitcopy move constructor" semantics, but
> it's orthogonal to all the attempts to define any form of "relocation
> with forced destruction".
>
> Am Mi., 2. Feb. 2022 um 10:43 Uhr schrieb William Linkmeyer via
> Std-Proposals <std-proposals_at_[hidden]>:
> >
> > The larger picture here seems to be an effort to make move semantics friendlier and easier to use. I began an informal survey on open-std of papers on move semantics.
> >
> > After reading unrelated papers, though, the thought occurred that move semantics will:
> > 1. move semantics are becoming more implicit (defaulting to move where applicable) and unified (utilities for move-based alternatives in the language are becoming prevalent)
> > 2. we should consider that, in a decade or so, move semantics may become more than a friendly memcpy/delete, and
> > 3. papers on move semantics should be weighed against the requirements they may place *on* the ABI
> >
> > To illustrate the first point:
> > - a proposal for (more) move semantics in views: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2446r1.html
> > - a paper describing move semantics at scale (esp. in containers): http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2329r0.pdf
> > - “proposes a conservative, move-only equivalent of std::function”: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p0288r9.html
> > - a proposal for simpler implicit move in return statements (clarifying c++20’s implicit move): http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p1018r13.html#biblio-p2266r1
> >
> > The second point is speculative by its nature. Papers are often reflective on the past or aspirational for the relatively near future.
> >
> > I am proposing that, in a decade or so, it is not unlikely that:
> > - memory will be far more distributed than it is today
> > - processors, not processor cores, often of various types will share memory — such as a GPU sharing memory with the CPU, or a Docker Swarm of several computers
> > - transactional memory will become more prevalent, perhaps becoming incorporated into the standard with a similar speed as move semantics are today
> >
> > To illustrate these speculations, here are some papers:
> > - module distribution, which blurs the line between platform-specific source code and abstract packages: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2473r1.pdf
> > - freestanding, embeddable c++: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p2338r0.html
> > - minimalist transactional memory: http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2021/p1875r2.pdf
> >
> > These papers are meant to illustrate a trend towards scale-independent processing with highly distributed programs.
> >
> > It would be prudent, therefore, to think in terms of patterns that are implementable in that context so as to avoid painful ABI breaks in the future, imposed on ourselves.
> >
> > I am merely urging people more fluent than myself to consider a set of move semantics generic enough to be future-proof on platforms where it *is* analogous to pass a reloc operator into a function that has a 50/50 chance of actually relocating it or not.
> >
> >
> > WL
> >
> > On Feb 1, 2022, at 4:07 PM, Barry Revzin via Std-Proposals <std-proposals_at_[hidden]> wrote:
> >
> > 
> >
> >
> > On Tue, Feb 1, 2022 at 4:04 AM Gašper Ažman via Std-Proposals <std-proposals_at_[hidden]> wrote:
> >>
> >> Hi Sebastien,
> >>
> >> you sure made a pretty long write-up! What I'm missing on the first skim-through is a thorough review of the currently published papers in the space and answers to the previously surfaced objections.
> >>
> >> Some of the papers in this space:
> >> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1144r5.html
> >> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4158.pdf
> >> http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p1029r3.pdf
> >> http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0023r0.pdf
> >
> >
> > Also, for instance, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0308r0.html has a section on Pilfering. Boost.Json uses that approach, for instance (probably other stuff in Boost too, haven't checked).
> >
> > Barry
> > --
> > Std-Proposals mailing list
> > Std-Proposals_at_[hidden]
> > https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
> >
> > --
> > Std-Proposals mailing list
> > Std-Proposals_at_[hidden]
> > https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

Received on 2022-03-02 10:56:09