C++ Logo

std-proposals

Advanced search

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

From: Sébastien Bini <sebastien.bini_at_[hidden]>
Date: Mon, 2 May 2022 12:23:47 +0200
Hello Giuseppe,

Thank you for your feedback.

> I'm probably missing something, but from what you say above and from the
> TempDir example on page 7, all of this sounds like existing move
> semantics with the additional enforcement that a moved-from object isn't
> ever touched again (not even reassigned / reset).

You could view it like that. It also allows for (a) better optimization
(relocation constructor can be better optimized in some cases, and trivial
relocation (move+destruct optimized into a memcpy) can happen), (b) better
semantics (it better conveys the intent), and (c) the move of const
volatile objects.

The point is also, that from a class design point of view, that the move
constructor may break the class purpose as the moved-from instance must
remain usable.

> (From a syntax point of view, I'm not sure how that is desirable, as one
> could no longer something like `other = reloc obj; delete &obj;`, but I
> don't think it's a particularly compelling use case...)

Indeed, I don't see a case where you would need to reuse the relocated
object. If you do need to, then move semantics are what you need.

> Anyways, a justification for relocation over move semantics on page 3
> says that "the move constructor performs extra operations to ensure that
> the moved-from object remains at a valid state." This is not universally
> true. It is true for the Standard Library (and makes certain std::list
> implementations of move operations expensive / noexcept(false)); that's
> just stdlib's stance.

Indeed it is not universally true. The phrasing may be too general, but
there are indeed cases where the move constructor is:
- either not trivial to implement, because of that moved-from instance that
must remain valid
- either performs extra costly operations that could be avoided, had we
known that the moved-object will not be reused.

> A class author can always state that a moved-from
> object of their class is only partially formed. In this last case, I am
> not sure the paper convincly justifies operator reloc over the status
> quo (which includes clang-tidy "use after move" checks and so on).

Stating that the moved-from object is invalid and should not be touched
again is (in my opinion) a bad class design. You may have a look at
https://herbsutter.com/2020/02/17/move-simply/, which further motivated me
to write this paper. Quoting Herb Sutter: "In C++, an object is valid
(meets its invariants) for its entire lifetime, which is from the end of
its construction to the start of its destruction (see [basic.life]/4).
Moving from an object does not end its lifetime, only destruction does, so
moving from an object does not make it invalid or not obey its invariants."
Relocation allows to make an object invalid before it is destructed, albeit
it is never touched again except by its destructor.

The classes you speak about would be the first one to benefit from the
relocation constructor (which ensures the relocated-instance is not reused)
and could also mark their move constructor as deleted. This would respect
the language philosophy.

The reloc operator introduces a language-level guarantee (no use after
relocation) that will always be better than an external tool that
developers may simply not run.

> The above point wasn't my main criticism. My main criticism was that the
> paper seems to settle on a convoluted syntax for the already existing
> move semantics (plus the p1144 bits). Did I misunderstand something?

Relocation and move semantics cover different needs. Move semantics are
still needed (for instance to move a vector or unique_ptr and reuse them
later). Relocation is here:
- for classes that don't mix well with move-semantics and would benefit
from being relocation-only (no move constructor) ;
- for performance reasons: relocating a variable that will be touched again
may allow for better optimization than using move-semantics ;
- for a safer code: we sometimes use std::move on variables that we intend
to never touch again. reloc enforces that. Besides, we can mark objects as
const and still relocate them, which is not possible with move-semantics.

I understand the reluctance. The paper introduces a new constructor, new
kind of reference and a new operator. This wasn't my first choice, I
intended something simpler. But I always ended-up with something broken if
I removed one of those.

For instance, I tried to implement the relocation constructor on top of
move-semantics, but I got several issues:
1. The relocation constructor with signature: T(std::relocate, T&& rhs);
    Then I fear people would misunderstand its difference with the move
constructor and use it the wrong way. They would especially not feel the
need of the reloc operator:
    T a; T b{std::relocate, std::move(a)}; // Can I still use 'a'?
    However things would be different with the proposed relocation
constructor. Without reloc it would yield:
    T a; T b{const_cast<T~>(a)}; // /!\/!\ not the way relocation should be
done!!!
    People will clearly prefer the use of reloc:
    T a; T b = reloc a;
    which better conveys the intent and also marks the end of life of 'a'.
2. Mark the move constructor as "relocation constructor": T(T&& rhs) reloc;
    However this has several other problems... Classes can no longer have
distinct move and relocation constructors.
    You don't know when you call the move constructor if you are actually
calling the move constructor or its promoted relocation-only version.
And with both approaches, you still cannot move const objects.

I am still open to discuss the syntax. I would be in favor of a solution
that doesn't introduce a new constructor and a new type of reference, but I
still haven't found it :/


On Fri, Apr 29, 2022 at 6:08 PM Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:

> Hello everyone,
>
> I reworked my first proposal about relocation (with a dedicated
> constructor and reloc operator). You can find the updated proposal here:
> https://github.com/SebastienBini/cpp-relocation-proposal/blob/main/relocation.pdf
>
> The main changes are:
> - added comparison against related existing proposals.
> - in order to preserve the ABI: the relocation constructor and reloc
> operator no longer early destruct instances.
> - put a better emphasis on possible memcpy optimization (trivial
> relocation, building on top of P1144R5 from Arthur O’Dwyer).
> - also, build up a case for classes that shouldn't be copyable nor
> movable, but can be relocatable.
>
> About the last point, here is a quick overview: consider a class that
> guarantees (unique) ownership over some resources (i.e. allocated in
> constructor or otherwise throws, and deallocates in destructor). Basic
> examples are a non-null pointer class or a socket wrapper class. In a pure
> class design perspective:
>
> - those classes shouldn't be copied as they provide unique access to
> the resource.
> - they could be movable but that's not ideal. The move constructor may
> leave them in a dirty state or else rely on complicated mechanisms not to
> do so. The move constructor for such classes:
> - either breaks the class guarantee. That is the case if the
> resource is moved from one instance to the other, leaving the original
> instance with an invalid resource. As stated this breaks our class
> invariant, which is to always offer unique access to some resource.
> - either needlessly complicates class design to work around the
> naive implementation described above. This may include allocating new
> unique resources to the moved instance in the move constructor, or to do it
> lazily at a later stage the next time the moved instance is reused.
> - may not be appropriate. What would you do with a moved-from
> socket wrapper instance? You don't know whether it owns any socket, and if
> it does, it will probably be on something you don't want. Most of the time,
> C++ programmers just discard moved-from instances anyway as they are unsure
> of what they contain.
> - it is a legitimate need to be able to "move" them around.
> - the relocation constructor fits this need: the classes can be
> relocated to another location. As the relocated instance is guaranteed
> never to be touched again, the relocation constructor can leave the
> relocated instance in a dirty invalid state (which the move constructor
> cannot).
>
> Thank you & best regards,
> Sébastien
>
> On Wed, Feb 2, 2022 at 10:53 AM William Linkmeyer <wlink10_at_[hidden]>
> wrote:
>
>> Or, for specifics on how I propose we think about this, Maciej’s comments
>> here:
>>
>> Since we do not introduce new types of references, or new types of
>> member functions, we can gradually migrate code from calling std::move
>> to using reloc operator (while preserving ABI and API compatibility).
>> This will also allow to finally have a optimal construction for types
>>
>>
>> Are appropriate. It is forward-looking, stable and minimal.
>>
>> Full context:
>>
>> Hi,
>>
>> I don't think adding yet another reference type and yet another
>> special member function is a good way to solve this, as the complexity
>> in this area is already big. I think the solution should reuse as much
>> as possible of existing syntax.
>> What I think would be better, is that we kept move-constructors, but
>> we add a new syntax to mark if a class is relocatable:
>> MyClass(MyClass&& other) = relocate;
>>
>> Marking such a constructor as relocate (no user-provided definition
>> allowed), would indicate to compiler two things: 1) moving is just a
>> trivial memcpy, 2) moved-from object is left in a state (e.g. default
>> constructed) where destructor call has no-side effect.
>> So current code:
>> MyClass newObject(std::move(other));
>>
>> becomes (pseudo-code):
>> MyClass newObject = uninitialized;
>> new (&newObject) MyClass(other); // or memcpy
>> new (&other) MyClass();
>> // when 'other' goes out of scope, its destruction can be skipped
>> because it is a noop.
>>
>> So far it does not change anything w.r.t. what we have now.
>> But if we add Sebastian's proposed operator reloc with such a
>> semantics that it will call move-constructor, and mark source object
>> as already destroyed we get the semantic checking that moved-from
>> object cannot be used anymore.
>> Operator reloc can be called on any type that is move-constructible,
>> it is just that for types marked as relocatable such an operation can
>> be better optimized.
>>
>> MyClass newObject = reloc other;
>> // now other cannot be referenced any more
>>
>> Since we do not introduce new types of references, or new types of
>> member functions, we can gradually migrate code from calling std::move
>> to using reloc operator (while preserving ABI and API compatibility).
>> This will also allow to finally have a optimal construction for types
>> with user-defined constructors:
>>
>> struct Person
>> {
>> Person(std::string firstName, lastName)
>> : firstName(reloc firstName)
>> , lastName(reloc lastName)
>> {}
>>
>> std::string firstName, lastName;
>> };
>>
>> Person p1("John", "Doe"); // no temporaries, no move constructors
>> Person p2(p1.firstName, p2.lastName); // one copy, no temporaries, no
>> move constructors
>>
>> Regards,
>> Maciej
>>
>> wt., 1 lut 2022 o 11:04 Gašper Ažman via Std-Proposals
>> <std-proposals_at_[hidden]> napisał(a):
>>
>>
>> WL
>>
>> On Feb 2, 2022, at 4:43 AM, William Linkmeyer <wlink10_at_[hidden]> wrote:
>>
>> 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
>>
>>

Received on 2022-05-02 10:24:00