D1031R1 draft 1 LLFIO with proposed C++ object model and lifetime changes for mapped memory support

776 views
Skip to first unread message

Niall Douglas

unread,
Jul 31, 2018, 3:08:27 PM7/31/18
to ISO C++ Standard - Future Proposals
So after many weeks of pondering and writing this outside of work each day, please find attached an early draft of revision 1 of P1031 destined for San Diego. Changes since R0:
  • Wrote partial draft TS wording for deadline, handle, io_handle, mapped, mapped_view, native_handle_type and file_io_error.
  • Added impact on the standard regarding potential changes to the C++ object model.
  • Added impact on the standard regarding the proposed [[no_side_effects]] et al contracts attributes.
Note that the partial draft TS wording assumes Deterministic Exceptions + SG14 standard error object + in-progress _Fails calling convention implementation of P0709.

I am very aware that modifying the C++ object and lifetime model is a huge ask of WG21. This is why memory maps are pure UB in current C++, despite the fact that you can't implement dynamic memory allocation on the major platforms without something like them.

Please before you blow holes in my proposed changes, can you think of a better alternative beforehand? I suspect that most "better alternatives" have a very good technical reason why I couldn't choose them, but it would do no harm to ask clever "stupid questions" at this stage in this proposal's lifecycle. Better out than in.

My thanks in advance, especially for feedback on such a profound and complex part of the C++ standard.

Niall

P1031R1_file_io_draft1.pdf

Niall Douglas

unread,
Aug 1, 2018, 2:01:23 PM8/1/18
to ISO C++ Standard - Future Proposals
So assuming people were put off by the length of the paper, below is the section I'd really like feedback upon.

Niall


3 Impact on the Standard

Listed at the end of this section are the in-flight WG21 papers this proposal is dependent upon, and which would need to enter the standard before this library can be considered. However a bigger issue involves the potential changes to the C++ object model, plus new contracts with which to indicate the limited side effects of i/o functions in order to enable improved optimisation.


3.1 Potential changes to the C++ object model

Firstly I wish to make it clear that there is the viable option of simply making no changes at all to the object model, and any placement of objects into mapped memory is declared to be undefined behaviour, as it is at present5 .


However I also think that an opportunity missed. Via memory maps, the proposed low level file i/o library herein enables object lifetime to exceed that of the program. It also enables an object to appear in multiple instances of concurrent C++ program execution, or indeed at multiple addresses within a C++ program. And finally one can mark individual pages of memory with different access permissions than read-only or read-write (e.g. no access i.e. unreachable), or kick individual pages out to swap, or throw away their contents. In other words, individual pages of memory can become unreachable with the objects within that storage still being alive.


All four things are problematic for the current C++ standard’s object model, not least that objects cannot currently outlive the program; that there is no awareness of it being possible for multiple concurrent C++ program instances to exist in the current standard; that objects not marked with [[no_unique_address]] and which are not bits in a bitfield must currently always have a single, unique address in the process; that all alive objects are equally reachable from anywhere in the program; and finally that objects are always available at their unique address, and are not also elsewhere.


Furthermore, there are reordering constraints peculiar to mapped memory if a modification to an object is to persist at all, or indeed in a form which is usable. This is because in most implementations of C++, atomic reordering constraints only affect apparent ordering to CPU cores within the same symmetric multiprocessing cluster, and may have no effect on the actual ordering of when modifications are sent to main memory and/or the storage device. Thus, if a C++ program modifies an object, and then modifies it again, some of the changes of the latter modification may land in main memory before some of the changes of the earlier modification, irrespective of any atomics or fences or anything else which a C++ program currently can legally do. This has obvious implications for the integrity of that object across a program restart.


I therefore suggest some changes to the C++ object model, listed below.


3.1.1 The relevant parts of the current standard

There are presently four kinds of storage duration. As [basic.stc] describes it:


The storage duration is the property of an object that defines the minimum potential lifetime of the storage containing the object. The storage duration is determined by the construct used to create the object and is one of the following:

  1. static storage duration
  2. thread storage duration
  3. automatic storage duration
  4. dynamic storage duration

[basic.life] describes in some detail the lifetime model, and it essentially reduces down to one of these possible states for each object (and apologies for the over-simplification for purposes of terse exposition):

  1. Unconstructed (from P0593: and considered unreachable by the compiler during analysis), but storage is of the right size and alignment, and pointers and glvalues to the storage of the object type are permitted to be used under limited circumstances.
  2. Unalive, in the process of being constructed, for types with non-trivial constructors only.
  3. Alive, fully constructed.
  4. Unalive, in the process of being destructed, for types with non-trivial destructors only.

You will note that due to their triviality, memory storing trivial types such as std::byte can either be unconstructed, or fully constructed, and no lifetime state exists apart from those two. It is important to understand that currently speaking, using allocated memory in which nothing has been constructed is undefined behaviour, so the common method of reinterpret casting the return from malloc() to a pointer of the type you want is presently undefined behaviour.


This is being dealt with in [P0593] Implicit creation of objects for low-level object manipulation. Amongst its many recommendations, it makes unconstructed memory unreachable by default. You can make it reachable by (i) constructing objects into it (ii) via the proposed std::bless() which tells the compiler that a region of unreachable memory (i.e. unconstructed) is now reachable (and contains alive trivial objects of some form). You can restore it to being unreachable by calling the destructor of the objects, even if those objects are completely trivial. P0593 doesn’t mention it explicitly (yet!), but given std::launder’s special casing for std::byte, it seems likely that calling the destructor for std::byte on a region with unknown trivially destructible types of objects in it will always mark it unreachable. And finally, P0593 names various functions as always returning memory which is reachable i.e. containing an array of alive objects of some unspecified trivial type.


This new concept of reachable vs unreachable memory is an important one, and it is leaned upon heavily in the proposed extensions.


3.1.2 A new storage duration: mapped

As always, naming is very hard, but mapped storage duration seems a reasonable name for memory which can be shared between concurrently running processes, or between multiple start-stop cycles of the same program (note that a program using an object from mapped storage which it itself did not store there is undefined behaviour).


The storage for objects with mapped storage duration shall last for the duration of the filesystem entity (see TS definitions below for precise meaning) referred to by the section_handle instance which represents the storage. section_handle instances may refer to filesystem entities whose lifetimes automatically end when the program ends (‘anonymous inodes’), or which may persist for an indeterminate period, including across subsequent executions of the C++ program.


Mapped storage has the most similarity to dynamic storage, but with the following differences:

  • It has allocation and alignment granularities with architecture specific coarse sizes (‘memory page’). 4Kb/2Mb/1Gb page sizes are common.
  • New allocations are guaranteed to be all bits zero on creation.
  • It has map on first read semantics. This means that the first read from a memory page can take hundreds of CPU cycles, and a TLB shootdown causes an interrupt for other CPUs.
  • It has allocate on first write semantics. This means that the first write to a memory page can take thousands or even hundreds of thousands of CPU cycles, plus a TLB shootdown.
  • Usually, but not always, mapped storage is a memory cache of equivalent storage a high latency storage device. Hence they can be individually pushed to storage, their contents thrown away, deallocated, given different access permission or caching strategies, and lots of other interesting (i.e. potentially game changing for large STL containers) operations. Individual pages can be:
    • Read-only, read-write, or copy-on-write. These are self describing, and these are hard characteristics: violating them means program failure.
    • Committed or uncommitted. This indicates whether the page counts towards the resources used by the C++ program. Uncommitted memory is inaccessible, and acts as a placeholder for later use (i.e. it is reserved address space, useful for expanding large arrays without content copying).
    • Dirty or clean. This indicates whether the page contains data not yet mirrored onto its backing storage.
    • Allocated or unallocated. This indicates whether storage backing the page has been allocated on the storage device. If not, the first write to a clean page may be very expensive as the page may need to be copied by the kernel and/or space allocated for it on the backing storage device.

The storage represented by a section_handle instance can be mapped into (i.e. made available to) a C++ program by creating a map_handle sourcing the storage from a section_handle instance. The storage mapped by the low level map_handle shall represent unconstructed and unreachable memory, and will require the use of std::bless() or map_view to make it reachable (alternatively, use the convenience class mapped on a section_handle instance which bundles the aforementioned low level operations on your behalf).


Destroying a map_view does not make the storage unreachable. Decommitting individual pages does, as does destroying a mapped or map_handle.


3.1.3 A new lifetime stage: unreachable

I propose adding a new separate lifetime status for objects: unreachable. Under P0593, unconstructed objects are unreachable, but there is no possibility for a partially constructed, alive or partially destructed object to be unreachable. I propose that this new status ought to be added such that objects can now have the following lifetime states:

  1. Unconstructed, always unreachable.
  2. Unalive, in the process of being constructed, for types with non-trivial constructors only. Always reachable.
  3. Alive, fully constructed, reachable. Has a single, unique address in memory (unless marked with [[no_unique_address]]).
  4. Alive, fully constructed, unreachable. Does NOT have a single, unique address in memory (it may have many, or none). May change whilst unreachable (i.e. reload it after marking it reachable at some address).
  5. Unalive, in the process of being destructed, for types with non-trivial destructors only. Always reachable.

P0593 proposed these functions to mark regions as reachable:


// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly creates objects within the denoted region.
void std::bless(void *start, size_t length);
 
// Effects: create an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation.
template<typename T> T *std::bless(void *p);


Thus the obvious corrollary for marking regions as unreachable:


// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly uncreates objects within the denoted region.
void std::unbless(void *start, size_t length);
 
// Effects: uncreate an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation.
template<typename T> void *std::unbless(T *p);


A gain of this new unreachable lifetime status is that it neatly solves the problem of objects appearing at multiple addresses in a running program. We can now say that only one of those addresses can be reachable for an alive object at a time. If a program wishes to change an object’s current reachable address, they unbless the old location, and bless the new location.


It should be emphasised that similarly to blessing, unblessing of trivial types causes no code emission. It simply tells the compiler what is now unreachable.


Finally, it may seem that unreachability is a bit of a big sledgehammer to throw at this problem. However, there are a number of other problems elsewhere in C++ where having unreachable but alive objects would be very useful – thread local storage on a million CPU core compute resource is an excellent example6 . That said, this is a big change to lifetime, and if strong arguments are made against it then I am happy to propose something more conservative.


3.1.4 Blessing and unblessing polymorphic objects

Currently P0593 makes no mention of polymorphic objects i.e. ones with vptrs in many implementations. I can see lots of reasons why it would be useful to bless and unbless polymorphic objects i.e. update the vptr to the correct one for the currently running program, or clear the vptr to the system null pointer.


Imagine, for example, a polymorphic object with mapped storage duration whose constructing program instance terminates. Upon restart of the program, the storage is mapped back into the program, but now the polymorphic object has the wrong vptr! So we need to write the correct vptr for that object, after which all works as expected7 .


This also applies to shared mapped memory. Extending the C++ object model to handle memory being modifiable by concurrently running C++ programs would be very hard, so I propose that we don’t do that. Rather we say that objects in mapped storage can only be reachable in exactly one or none running C++ programs at a time i.e. at exactly one or no address at any one time, otherwise it is undefined behaviour. Therefore, if process A wants to make available an object to process B, it unblesses it and tells process B that the object is now available for use. Process B blesses the object into reachable, and uses it. When done, it unblesses it once more. Now process A can bless it and use it again, if so desired.


Implied in this is that blessing and unblessing becomes able to rewrite the vptr (or whatever the C++ implementation uses to implement polymorphism). To be truthful, I am torn on this. On the one hand, if you wish to be able to bless and unbless polymorphic objects, rewriting the vptr is unavoidable, and the frequency of relocating polymorphic objects in current C++ is low. On the other hand, future C++’s may do a lot more remapping of memory during large array expansion in order to skip doing a memory copy, and in this circumstance every single polymorphic object in a few million item array would need their vptr’s needlessly rewritten, which is unnecessary work.


I’m going to err on the side of caution, and propose that the polymorphic editions of bless and unbless shall be:


// Effects: create an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation
//          apart from any necessary changes to make any polymorphic
//          objects within and including T ready for use.
template<typename T> T *std::revive(void *p);
 
// Effects: uncreate an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation
//          apart from any necessary changes to make any polymorphic
//          functions within the objects within and including T unusable.
template<typename T> void *std::stun(T *p);


Note that stun() and revive() not just change the vptrs of the type you pass them, but also any vptrs of any nested types within that type. They also perform blessing and unblessing, so you do not have to do that separately. Calling these functions on non-polymorphic types is permitted.


3.2 New attributes [[no_side_effects]] and [[no_visible_side_effects]], and new contract syntax for specifying lack of side effects

It is highly important for the future efficiency of any iostreams v2 that the low level i/o functions do not cause the compiler to dump and reload all state for every i/o, as they must do at present. The i/o functions proposed do not modify their handle on most platforms, and on those platforms we can guarantee to the compiler that there are either no side effects or no visible side effects except for those objects modified by parameters.


Reusing bless() and unbless(), I propose the following contracts syntax for the io_handle functions so we can tell the compiler what side effects the i/o functions have:


constexpr! void ensure_blessed(buffers_type buffers)
{
 
for(auto &buffer : buffers)
 
{
    bless
(buffer.data(), buffer.size());
 
}
}
 
// This function has no side effects visible to the caller except
// for the objects it creates in the scatter buffer list.
virtual buffers_type io_handle::read(io_request<buffers_type> reqs,
                                     deadline d
= deadline()) throws(file_io_error)
   
[[no_side_effects]]
   
[[ensures: ensure_blessed(reqs.buffers)]]
   
[[ensures: ensure_blessed(return)]];


The key thing to note here is if the caller never accesses any of the scatter buffers returned by the function, the read can be optimised out entirely due to the [[no_side_effects]] attribute. Obviously the compiler also does not dump and reload any state around this scatter read apart from buffers supplied and returned, despite it calling a kernel system call, because we are giving a hard guarantee to the compiler that it is safe to assume no side effects apart from modifying the scatter buffers.


Gather write is less exciting, but straightforward:


virtual const_buffers_type io_handle::write(io_request<const_buffers_type> reqs,
                                            deadline d
= deadline())
                           
throws(file_io_error) [[no_visible_side_effects]];


Here we are telling the compiler that there are side effects, just not ones visible to the caller. Hence the compiler cannot optimise out calling this function, but it can skip dumping and reloading state despite the kernel system call made by the function.


3.2.1 I/O write reordering barriers

C and C++ allow the programmer to constrain memory read and write operations, specifically to constrain the extent of reordering that the compiler and CPU are permitted to do. One can use atomics with a memory order specified, or one can call a fence function which applies a memory reordering constraint for the current thread of execution either at just the compiler level, or also at the CPU level whereby the CPU is told what ordering its symmetric multiprocessing cores needs to manifest in visible side effects.


File i/o is no different: concurrent users of the same file or directory or file system will experience races unless they take special measures to coordinate amongst themselves.


Given that I have proposed above that C++ standardises mapped storage duration into the language itself, it might seem self evident that one would also need to propose a standardised mechanism for indicating reordering constraints on i/o, which are separate to those for threads. My current advice is that we should not do this – yet.


The first reason why is that persistent memory is just around the corner, and I think it would be unwise to standardise without the industry gaining plenty of empirical experience first. So kick that decision down a few standard releases.

Secondly, the proposed library herein does expose whatever acquire-release semantics is implemented by the host operating system for file i/o, plus the advisory locking infrastructure implemented by the host, plus the ability to enable write-through caching semantics. It also provides barrier(), though one should not write code which relies upon it working, as it frequently does not.


Note that if the file is opened in non-volatile RAM storage mode, barrier() calls the appropriate architecture-specific assembler instructions to correctly barrier writes to main memory, so in this sense there is library, if not language, support for persistent memory. The obvious sticker is that barrier() is a virtual function with significant preamble and epilogue, so for flushing a single cache line it is extremely inefficient relative to direct language support for this.

Nevertheless, one can write standards conforming code with the proposed library which performs better on persistent memory than on traditional storage, and my advice is that this is good enough for the next few years. 

Daniel Gutson

unread,
Aug 1, 2018, 2:37:31 PM8/1/18
to std-proposals
Though the actual way of recognizing this work would be currently reading the proposal, and I will do my best during the weekend, I will do something that I think is not very common these days: to say an anonymous "thank you" for the (personal) time you took to contribute to the C++.
Same for all the people putting effort, time and/or money in the progress of the language.

Sorry for the spam to those offended.

    Daniel.

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/b214ce98-2aaa-4f7f-a04a-77687422111a%40isocpp.org.


--
Who’s got the sweetest disposition?
One guess, that’s who?
Who’d never, ever start an argument?
Who never shows a bit of temperament?
Who's never wrong but always right?
Who'd never dream of starting a fight?
Who get stuck with all the bad luck?

Jake Arkinstall

unread,
Aug 1, 2018, 5:28:05 PM8/1/18
to std-pr...@isocpp.org
This.


On Wed, 1 Aug 2018, 19:37 Daniel Gutson, <daniel...@gmail.com> wrote:
Though the actual way of recognizing this work would be currently reading the proposal, and I will do my best during the weekend, I will do something that I think is not very common these days: to say an anonymous "thank you" for the (personal) time you took to contribute to the C++.
Same for all the people putting effort, time and/or money in the progress of the language.

It's a long proposal and, for me at least, there's a lot of background reading involved. I think it's going to take a few days before you'll get some useful feedback, but it isn't being ignored - it is far too interesting for that.

Alexander Zaitsev

unread,
Aug 1, 2018, 6:38:36 PM8/1/18
to ISO C++ Standard - Future Proposals
вторник, 31 июля 2018 г., 22:08:27 UTC+3 пользователь Niall Douglas написал:
Great proposal. One suggestion - possibly will be better to divide your proposal into several proposals. My own wish - move contract [[no_side_effects]] into another proposal. Because [[no_side_effects]] can help in another places for code optimization. 

Nicol Bolas

unread,
Aug 1, 2018, 9:31:42 PM8/1/18
to ISO C++ Standard - Future Proposals

On Creation, Destruction, and Remapping:

So you have this operation you want to be able to do. You want to take objects in one address, make them disappear from that address, and then make them appear in a (potentially) different address. And you want to change the C++ object model to make this work.

Here's a question: do you need to change the object model in order to get this effect?

If we limit our usage of mappable types to P0593's implicit lifetime types, then I don't think we to affect the object model at all. Implicit lifetime types are objects which are nothing more than a set of bits, where their value representation is their logical functionality. Two objects of such types with the same value representation have the same value and are distinct only in that they have different addresses.

With such a limitation in place, you could define the mapped memory operation as a funny sort of memcpy. It doesn't move the object. Unmapping the memory range terminates the lifetime of all objects in that memory. The values are preserved, but not the objects. You then map the memory at potentially new addresses.

Now, you still need a way to create new objects in those addresses with their value representations intact (see below for why non-template `bless` can't do this). But such a feature does not require substantial changes to the object model. It's just a new way to create an object.

To me, the principle advantage of significant changes to the object model here would be to allow a larger variety of objects to work. So now we have the next question: exactly which types of objects are you trying to support? What is the minimum set of features you need to make this worthwhile? That will help you define what changes you need to the object model, if any.

For example, if you want to expand from Implicit Lifetime types to Trivially Relocatable types, then the object model need not be changed (save for the changes needed to make Trivially Relocatable work). You can still define it as destroying the old object and manifesting a new one.

However, because such types are more than just their values, this is of limited value. `unique_ptr<T>` is the quintessential Trivially Relocatable example. But would it be a good thing to stick in a memory mapped object? I don't think so. If the object it points to is not in the same memory map, then it is not reasonable to persist the object across executions, since that object may well be long dead.

And if `unique_ptr<T>` does point to an object in the same map, then its address will have to be updated before you can actually use it. That means you can't just cast and go; you have to do some work between mapping the memory and using it. Given that... what's the point of storing it in a memory map directly at all? Because it's part of some other object you do want to store there?

Somethign similar is true of other popular Trivially Relocatable types. `vector`, `fstream`, `any`, etc, all of them could be stored there, but the utility of doing so is... dubious.

Your proposal suggests allowing polymorphic types. Now with these, the destroy/recreate trick becomes tricky. The manifesting operation is based on the value being undisturbed. But you need the value to be disturbed, since the location of the vtable pointer may need to be fixed up.

Which brings me to my own personal experience. See, I've been a part of a team that actually implemented a "memcpy"-style serialization system that could handle polymorphic types. This system required two phases: serialization and deserialization.

Now admittedly, our system did the serialization part through copying (the host architecture and the destination architectures were not the same. And I mean not the same; in one remarkable case, the compiler for the deserialization part used a different order for the base classes than the serialization compiler). But part of what it did was to put something special in the vtable pointer slot: an index that represented which type it was. The deserialization code didn't copy anything; it just fixed up vtable pointers in-situ by reading the type index.

This is important because one of the things this allowed us to do was to deserialize without knowing at compile time what each type was. We didn't have to know the dynamic type from outside of the system. We were able to use the base class type, and the system would know how that the pointer actually pointed to a more derived class and therefore adjust the vtable accordingly.

That's really important, since most polymorphic type usage is based on base class types. If you have a polymorphic object in mapped memory, the code that wants to retrieve the address probably doesn't know what its dynamic type is; it knows the base class type it wants.

And that's where things get tricky. Because now, we're not talking about some kind of memory barrier. You're talking about having to perform a process on each object in a region of memory before the unmap process can be considered complete. Then you have to perform a counter-process on each object afterwards to fully map it again. And what's worse is that C++... makes this impossible to implement generally.

We could do it in our system only because our system had a limited, fixed set of types to work with. C++ does not. Loading DLL/SOs adds more type indices. And while type indices will be the same for an execution of the code, it will be able to change between executions. But you need to be able to store a value in the mapped memory that contains something that is persistent across executions.

C++ doesn't have a type identifier like that. So the only way to make this work is if you know the dynamic type of the object when you unpack it. And as someone who has used persistence systems of this sort, you generally don't know. And you generally don't want to know; if you wanted to know, you wouldn't be using a dynamic type ;)

So I don't think that polymorphic type support would be useful if it were done in an implementable fashion. However, even if it was implementable, it could still work based on having an explicit location in code where the lifetime of the old object ends (packing the polymorphic object; replacing the vtable pointer with an index) and the lifetime of the new begins (unpacking it; replacing the index with a valid vtable pointer). Obviously, we would need to create functions to explicitly do so on a per-object basis (as your proposal outlines), but this wouldn't need to affect the object model. Your "stun" function could be said to terminate the object's lifetime, while your "revive" function creates it.

So even in this case, the object model itself wouldn't need to be changed. So what use cases are you trying to support where you need to change the object model?

On Mapped Storage Duration:

I think "Mapped storage duration" is too on-point in terms of where the memory comes from, rather than how C++ thinks of it.

The four storage durations are based on how C++ interacts with them. Automatic storage duration objects have storage durations determined entirely by their objects' scopes. Static storage duration objects have their storage duration be the lifetime of the program. Thread-local storage duration objects have their storage duration be the lifetime of the thread. And dynamic duration objects have their storage duration determined dynamically: by direct manipulation of the code.

But I think there is a deeper misunderstanding here. You are confusing properties of "storage" with the properties of "storage duration". The former is a thing that an object needs to have in order to exist. The latter is a property of an object, determined by the means used to construct it, and it's purpose is to govern the (default) lifetime of that object.

For example, you can create an automatic variable with automatic storage duration. You can then reuse that variable's storage to create objects with dynamic storage duration. By reusing it, you will have ended the lifetime of the automatic variable. You would then have an object with dynamic storage duration whose storage happens to be on the stack.

Which is why "stack storage duration" would be a misnomer, much like "mapped storage duration".

Of course, C++ has a bunch of rules about what happens when you do this and then try to use that variable's name later on. As well as what happens when that variable goes out of scope, and what you need to do to make leaving scope not invoke UB (namely, create an object of that type back in that storage).

What you seem to want is simply a wider form of dynamic storage duration. You're not changing the meaning of the storage duration; you're simply adding ways to create/destroy such objects.

On Bless:

P0593 proposed these functions to mark regions as reachable:

Your notion of "reachability" has nothing to do with what P0593 is doing. `bless` does not make objects "reachable"; that proposal only uses the word "reachable" once, and that is merely a description about the relationship between a pointer and a size (which really ought to be a `std::span<std::byte>`).

And your definition of "reachable" here does not map to what P0593 is doing. `std::bless` and equivalent functions declare that a piece of memory creates implicit objects. When you call `bless` on a range of memory, objects will be created to make your code work. What your "reachable" behavior seems to want is for them to already exist.

This may seem like a trivial difference, but it isn't. P0593 points it out: when you create objects, the contents of the storage are rendered unspecified. And this is just as true for objects implicitly created by `std::bless`.

You explicitly don't want this. You want the objects to already exist, with their values intact. Non-template `bless` can't do that; `bless<T>` can, but only for a single complete object of a specified type. And even then, it is still creating the object, not making it "reachable". It wasn't there until `bless<T>` was called.

On Focus:

Your section 3.2 has nothing to do with the C++ object model or the needs of mapping memory. That's not to say that they aren't useful, but the presence of side effects is orthogonal to memory mapping or the object model.

Arthur O'Dwyer

unread,
Aug 1, 2018, 9:50:41 PM8/1/18
to ISO C++ Standard - Future Proposals
On Wednesday, August 1, 2018 at 11:01:23 AM UTC-7, Niall Douglas wrote:
So assuming people were put off by the length of the paper

That's a good assumption. :)

This is being dealt with in [P0593] Implicit creation of objects for low-level object manipulation. Amongst its many recommendations, it makes unconstructed memory unreachable by default. You can make it reachable by (i) constructing objects into it (ii) via the proposed std::bless() which tells the compiler that a region of unreachable memory (i.e. unconstructed) is now reachable (and contains alive trivial objects of some form).


Nit: for "trivial", read "implicit-lifetime", right?

[...]

Mapped storage has the most similarity to dynamic storage, but with the following differences:

  • It has allocation and alignment granularities with architecture specific coarse sizes (‘memory page’). 4Kb/2Mb/1Gb page sizes are common.
  • New allocations are guaranteed to be all bits zero on creation.
  • It has map on first read semantics. This means that the first read from a memory page can take hundreds of CPU cycles, and a TLB shootdown causes an interrupt for other CPUs.
  • It has allocate on first write semantics. This means that the first write to a memory page can take thousands or even hundreds of thousands of CPU cycles, plus a TLB shootdown.
  • Usually, but not always, mapped storage is a memory cache of equivalent storage a high latency storage device. Hence they can be individually pushed to storage, their contents thrown away, deallocated, given different access permission or caching strategies, and lots of other interesting (i.e. potentially game changing for large STL containers) operations. Individual pages can be:
    • Read-only, read-write, or copy-on-write. These are self describing, and these are hard characteristics: violating them means program failure.
    • Committed or uncommitted. This indicates whether the page counts towards the resources used by the C++ program. Uncommitted memory is inaccessible, and acts as a placeholder for later use (i.e. it is reserved address space, useful for expanding large arrays without content copying).
    • Dirty or clean. This indicates whether the page contains data not yet mirrored onto its backing storage.
    • Allocated or unallocated. This indicates whether storage backing the page has been allocated on the storage device. If not, the first write to a clean page may be very expensive as the page may need to be copied by the kernel and/or space allocated for it on the backing storage device.

The storage represented by a section_handle instance can be mapped into (i.e. made available to) a C++ program by creating a map_handle sourcing the storage from a section_handle instance. The storage mapped by the low level map_handle shall represent unconstructed and unreachable memory, and will require the use of std::bless() or map_view to make it reachable (alternatively, use the convenience class mapped on a section_handle instance which bundles the aforementioned low level operations on your behalf).


My hot take is that this is waaay too low-level and implementation-detaily for a WG21 proposal. C++ programs have to be able to run on hardware that might not even have the concept of a "TLB".  The stuff in this bulleted list is perfectly reasonable internal documentation for an implementation, but it seems like the sort of thing that ought to be abstracted away from everyday C++ programmers as much as possible.
I would want the trajectory to be:
- implement this stuff
- implement something higher-level and suitably abstract on top of this stuff
- consider how to standardize that abstract interface
- don't bother to mention the low-level details in that proposal (but have the reference implementation available for vendors to look at)

I'm sure I'm ill-informed and there will be apparently good reasons not to take this trajectory, but it's still what I would want to see as an end-user.
(And yes, I wish the Networking TS would have taken the same trajectory.)


3.1.3 A new lifetime stage: unreachable

I propose adding a new separate lifetime status for objects: unreachable. Under P0593, unconstructed objects are unreachable, but there is no possibility for a partially constructed, alive or partially destructed object to be unreachable. I propose that this new status ought to be added [...]


The paper may answer this question, but: Is this related at all to the existing garbage-collection hooks, http://eel.is/c++draft/util.dynamic.safety, which already define terminology such as "declare reachable"?

P0593 proposed these functions to mark regions as reachable:


// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly creates objects within the denoted region.
void std::bless(void *start, size_t length);
 
// Effects: create an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation.
template<typename T> T *std::bless(void *p);

Hooray!  For purposes of P1144 relocatability, btw, I am hoping that the words "implicit-lifetime" will be removed from the above comments.  My understanding of "implicit-lifetime" is that implicit-lifetime types are precisely those types which it would be safe to access even without a call to std::bless.  However, I am out of the loop.


Thus the obvious corrollary for marking regions as unreachable:


// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly uncreates objects within the denoted region.
void std::unbless(void *start, size_t length);
 
// Effects: uncreate an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation.
template<typename T> void *std::unbless(T *p); 

Nit: The template version should return `void` not `void*`, right?
I also don't fully understand the desired semantics of the non-template version of either `bless` or `unbless`.
 
It should be emphasised that similarly to blessing, unblessing of trivial types causes no code emission. It simply tells the compiler what is now unreachable.

+1.

[...]

I’m going to err on the side of caution, and propose that the polymorphic editions of bless and unbless shall be:


// Effects: create an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation
//          apart from any necessary changes to make any polymorphic
//          objects within and including T ready for use.
template<typename T> T *std::revive(void *p);
 
// Effects: uncreate an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation
//          apart from any necessary changes to make any polymorphic
//          functions within the objects within and including T unusable.
template<typename T> void *std::stun(T *p);

These names are clever (really! I like them!) but they don't convey the semantics at all. "Revive" looks like it should be concerned with resurrecting a previously killed object, not just blessing a non-object in a slightly different way.  This will of course get bikeshedded anyway, but it strikes me that it might at least be fun to look at other words with religious overtones.  Back in reality, though, what's wrong with "polymorphic_bless"?

And I don't understand why you need "stun / polymorphic_unbless" at all. What does it even mean to make a polymorphic object "unusable"? How is that different from just doing nothing at all?

And if "revive / polymorphic_bless" is not a no-op but actually does have an observable effect on memory, that's a big deal; you should emphasize that somehow.

Sorry, gotta run now; possibly more later.
–Arthur

Arthur O'Dwyer

unread,
Aug 1, 2018, 11:20:20 PM8/1/18
to ISO C++ Standard - Future Proposals
On Wednesday, August 1, 2018 at 6:31:42 PM UTC-7, Nicol Bolas wrote:

For example, if you want to expand from Implicit Lifetime types to Trivially Relocatable types, then the object model need not be changed (save for the changes needed to make Trivially Relocatable work). You can still define it as destroying the old object and manifesting a new one.

However, because such types are more than just their values, this is of limited value. `unique_ptr<T>` is the quintessential Trivially Relocatable example. But would it be a good thing to stick in a memory mapped object? I don't think so. If the object it points to is not in the same memory map, then it is not reasonable to persist the object across executions, since that object may well be long dead.

And if `unique_ptr<T>` does point to an object in the same map, then its address will have to be updated before you can actually use it. That means you can't just cast and go; you have to do some work between mapping the memory and using it. Given that... what's the point of storing it in a memory map directly at all? Because it's part of some other object you do want to store there?

+1 to all of the above.  "Relocatable" in the sense of "moveable-around within the same process" is almost entirely orthogonal to "position-independent" in the sense of "positionable-arbitrarily within the same process", which in turn is just a small part of "shareable" in the sense of "positionable-arbitrarily simultaneously in multiple processes with potentially different code segments."
Another nail in the coffin of this contrived unique_ptr example: unique_ptr<T> points to a thing that you're going to `delete` later, which depends on the state of the global new/delete heap in the current process — there's no way that's going to work if the current process isn't the one who new'ed the object, right?

I hope that `std::bless` (assuming it works for non-implicit-lifetime types) is good enough to solve the object-lifetime issues raised by "trivially relocatable." Nicol, you seem to have a better handle than I do on what exactly these issues are; could I trouble you (elsethread or via email) for feedback on P1144?

 
Somethign similar is true of other popular Trivially Relocatable types. `vector`, `fstream`, `any`, etc, all of them could be stored there, but the utility of doing so is... dubious.

Nit: Unless I am mistaken, `fstream` cannot possibly be trivially relocatable for several reasons. It contains a pointer-to-self (to its streambuf), and it has a virtual destructor (which could do just about anything, and therefore cannot be assumed to do the Right Thing w.r.t. coordinating with the move-constructor).  Please correct me if I'm wrong.

(Dissimilarly: `any` is not trivially relocatable on any mainstream implementation, but it can easily be made trivially relocatable. I make it so under an #ifdef in my libc++ fork.)


Your proposal suggests allowing polymorphic types. Now with these, the destroy/recreate trick becomes tricky. [...]

I would like to be able to use a single mental model to explain the behavior of both

    struct A {
        virtual void foo();
    };

and

    struct B {
        int (*f_)();
        int foo() { return (*f_)(); }
    };

What arguments can you provide to convince me that polymorphic `A` should be special-cased in some way that does not equally apply to non-polymorphic `B`?

–Arthur

Thiago Macieira

unread,
Aug 2, 2018, 1:06:06 AM8/2/18
to std-pr...@isocpp.org
On Wednesday, 1 August 2018 20:20:20 PDT Arthur O'Dwyer wrote:
> Another nail in the coffin of this contrived unique_ptr example:
> unique_ptr<T> points to a thing that you're going to `delete` later, which
> depends on the state of the global new/delete heap in the current process —
> there's *no way* that's going to work if the current process isn't the one
> who new'ed the object, right?

That depends on the deleter. For the default deleter, you're definitely right.

For a special deleter that operates on shared memory, unique_ptr might work.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center



Niall Douglas

unread,
Aug 2, 2018, 5:12:53 AM8/2/18
to ISO C++ Standard - Future Proposals

So you have this operation you want to be able to do. You want to take objects in one address, make them disappear from that address, and then make them appear in a (potentially) different address. And you want to change the C++ object model to make this work.

Here's a question: do you need to change the object model in order to get this effect?

So, here's what we want to be able to do without UB:
  1. Modify where allocated memory appears in the process (use cases: expanding arrays without memory copies, patching binaries, mapping page cached files etc)
  2. Share a region of memory with another running process (use cases: IPC, multiprocessing, etc)
  3. Reduce C++ causing unnecessary page faults/prevent use of memory with changed access permissions by making the compiler aware of what memory is not to be touched.
  4. Handle objects written out in a previous incarnation of a program being at a different address in a new incarnation of a program.
  5. Enforce a new read/write reordering constraint separate to compiler and thread barriers.
In hindsight, I probably ought to have specified this more succinctly in the paper. I will change it.

There are also multiple levels of persistability going on here:
  1. Trivially copyable types, which are just bits. You can use their in-memory representation directly, just need to constrain write reordering.
  2. Trivially relocatable types, which are moved with just bits, if we standardise those. You can use their in-memory representation directly, just need to constrain write reordering.
  3. Polymorphic types where if it weren't for the vptr, would be either of the first two categories. You can use their in-memory representation directly, bar the vptr restamp on load.
  4. Other types whose in-memory representation can be used directly after some trivial transformation e.g. replace all pointers with offset pointers.
So the common factor to all these is that their in-memory representation is sufficient for their persisted representation i.e. no memory copying needed, but there can be a "phase change" between "active" and "sleeping".

Serialisation is where you need to create new objects which represent a persisted edition of an in-memory form. That's a whole separate topic, to be addressed as iostreams v2 after we've nailed down the in-memory stuff which is a prerequisite as the serialised form of an object is clearly a persistable object meeting at least the criteria of the above.
 

If we limit our usage of mappable types to P0593's implicit lifetime types, then I don't think we to affect the object model at all. Implicit lifetime types are objects which are nothing more than a set of bits, where their value representation is their logical functionality. Two objects of such types with the same value representation have the same value and are distinct only in that they have different addresses.

With such a limitation in place, you could define the mapped memory operation as a funny sort of memcpy. It doesn't move the object. Unmapping the memory range terminates the lifetime of all objects in that memory. The values are preserved, but not the objects. You then map the memory at potentially new addresses.

Now, you still need a way to create new objects in those addresses with their value representations intact (see below for why non-template `bless` can't do this). But such a feature does not require substantial changes to the object model. It's just a new way to create an object.

Oh sure. Trivially copyable objects are almost easy. But I think it unwise to standardise support for those without considering the bigger picture first, so we know where we might be going eventually.
 

To me, the principle advantage of significant changes to the object model here would be to allow a larger variety of objects to work. So now we have the next question: exactly which types of objects are you trying to support? What is the minimum set of features you need to make this worthwhile? That will help you define what changes you need to the object model, if any.

Hopefully the above list answers you.
 

However, because such types are more than just their values, this is of limited value. `unique_ptr<T>` is the quintessential Trivially Relocatable example. But would it be a good thing to stick in a memory mapped object? I don't think so. If the object it points to is not in the same memory map, then it is not reasonable to persist the object across executions, since that object may well be long dead. 

And if `unique_ptr<T>` does point to an object in the same map, then its address will have to be updated before you can actually use it. That means you can't just cast and go; you have to do some work between mapping the memory and using it. Given that... what's the point of storing it in a memory map directly at all? Because it's part of some other object you do want to store there?

Where I'd like to get to is that there is a proposed framework for types which don't need full fat serialisation and whose in-memory representation can be used as-is for persisting, perhaps with some trivial manipulation.

I appreciate I haven't written any of that out yet, but it's hard until I know if WG21 likes my proposed bless/unbless approach.
 

Your proposal suggests allowing polymorphic types. Now with these, the destroy/recreate trick becomes tricky. The manifesting operation is based on the value being undisturbed. But you need the value to be disturbed, since the location of the vtable pointer may need to be fixed up.

Which brings me to my own personal experience. See, I've been a part of a team that actually implemented a "memcpy"-style serialization system that could handle polymorphic types. This system required two phases: serialization and deserialization.

Now admittedly, our system did the serialization part through copying (the host architecture and the destination architectures were not the same. And I mean not the same; in one remarkable case, the compiler for the deserialization part used a different order for the base classes than the serialization compiler). But part of what it did was to put something special in the vtable pointer slot: an index that represented which type it was. The deserialization code didn't copy anything; it just fixed up vtable pointers in-situ by reading the type index.

This is important because one of the things this allowed us to do was to deserialize without knowing at compile time what each type was. We didn't have to know the dynamic type from outside of the system. We were able to use the base class type, and the system would know how that the pointer actually pointed to a more derived class and therefore adjust the vtable accordingly.

That's really important, since most polymorphic type usage is based on base class types. If you have a polymorphic object in mapped memory, the code that wants to retrieve the address probably doesn't know what its dynamic type is; it knows the base class type it wants.

And that's where things get tricky. Because now, we're not talking about some kind of memory barrier. You're talking about having to perform a process on each object in a region of memory before the unmap process can be considered complete. Then you have to perform a counter-process on each object afterwards to fully map it again. And what's worse is that C++... makes this impossible to implement generally.

So, firstly I'm not proposing at this time any generalised type discovery mechanism. That's for a future serialisation layer. Under what I've proposed, the program by definition always knows the types of objects in mapped storage. So restoring any vptr is trivially easy.

Secondly, it would be UB under this limited scope of proposal for any program other than the constructing program to use any objects in mapped storage. So, if there is some shared memory, only instances of the constructing program can legally use it.

Down the line we'll come back to that, but for now, let's not go there yet.
 

We could do it in our system only because our system had a limited, fixed set of types to work with. C++ does not. Loading DLL/SOs adds more type indices. And while type indices will be the same for an execution of the code, it will be able to change between executions. But you need to be able to store a value in the mapped memory that contains something that is persistent across executions.

C++ doesn't have a type identifier like that. So the only way to make this work is if you know the dynamic type of the object when you unpack it. And as someone who has used persistence systems of this sort, you generally don't know. And you generally don't want to know; if you wanted to know, you wouldn't be using a dynamic type ;)

All grist for any full serialisation layer.

I'm coming at this from a much lower level. Get the basics in place first. Reduce the problem down to a manageable subset.
 

So I don't think that polymorphic type support would be useful if it were done in an implementable fashion. However, even if it was implementable, it could still work based on having an explicit location in code where the lifetime of the old object ends (packing the polymorphic object; replacing the vtable pointer with an index) and the lifetime of the new begins (unpacking it; replacing the index with a valid vtable pointer). Obviously, we would need to create functions to explicitly do so on a per-object basis (as your proposal outlines), but this wouldn't need to affect the object model. Your "stun" function could be said to terminate the object's lifetime, while your "revive" function creates it.

First I disagree that storing objects in storage of any form is not useful. What is the difference between mapped storage and dynamic storage? Merely duration.

I agree that the nomenclature is confusing. There seems to be fuzziness in the standard regarding object lifetime. I know Richard and SG12 are still in the process of fixing that. But I find it a bit confusing, and you can see that in my proposal text.
 

I think "Mapped storage duration" is too on-point in terms of where the memory comes from, rather than how C++ thinks of it.

The four storage durations are based on how C++ interacts with them. Automatic storage duration objects have storage durations determined entirely by their objects' scopes. Static storage duration objects have their storage duration be the lifetime of the program. Thread-local storage duration objects have their storage duration be the lifetime of the thread. And dynamic duration objects have their storage duration determined dynamically: by direct manipulation of the code.

Sure. And I propose that mapped storage duration has the lifetime of its backing filesystem entity. Same approach.
 

But I think there is a deeper misunderstanding here. You are confusing properties of "storage" with the properties of "storage duration". The former is a thing that an object needs to have in order to exist. The latter is a property of an object, determined by the means used to construct it, and it's purpose is to govern the (default) lifetime of that object.

For example, you can create an automatic variable with automatic storage duration. You can then reuse that variable's storage to create objects with dynamic storage duration. By reusing it, you will have ended the lifetime of the automatic variable. You would then have an object with dynamic storage duration whose storage happens to be on the stack.

Which is why "stack storage duration" would be a misnomer, much like "mapped storage duration".

Would "filesystem storage duration" be better?

I originally discounted that naming because there are mapped storage durations whose backing filesystem entity is the swap file/kernel, so I felt mentioning filesystem would be confusing.
 

Of course, C++ has a bunch of rules about what happens when you do this and then try to use that variable's name later on. As well as what happens when that variable goes out of scope, and what you need to do to make leaving scope not invoke UB (namely, create an object of that type back in that storage).

What you seem to want is simply a wider form of dynamic storage duration. You're not changing the meaning of the storage duration; you're simply adding ways to create/destroy such objects.

It's more I'm looking for a storage duration which exceeds the duration of the program, but still has a well defined duration. Dynamic storage can't do that.
 

On Bless:

P0593 proposed these functions to mark regions as reachable:

Your notion of "reachability" has nothing to do with what P0593 is doing. `bless` does not make objects "reachable"; that proposal only uses the word "reachable" once, and that is merely a description about the relationship between a pointer and a size (which really ought to be a `std::span<std::byte>`).

And your definition of "reachable" here does not map to what P0593 is doing. `std::bless` and equivalent functions declare that a piece of memory creates implicit objects. When you call `bless` on a range of memory, objects will be created to make your code work. What your "reachable" behavior seems to want is for them to already exist.

Eh ... you're right that I find blessing underspecified. This is that fuzziness about lifetime I mentioned earlier. I've spent a good few weeks pondering P0593, indeed I asked a ton of questions about it as the SG12 meeting at Rapperswil. And I'm confused about what it precisely does.

Personally, I find reachable vs unreachable a much less ambiguous explanation of what it does. And more helpful for compiler writers and formal verification. But I totally get that it may simply be my incompetence, and I need to be corrected. I am not a compiler writer after all.

(Richard Smith, feel free to jump in and correct me now!)
 

This may seem like a trivial difference, but it isn't. P0593 points it out: when you create objects, the contents of the storage are rendered unspecified. And this is just as true for objects implicitly created by `std::bless`.

Sure. They become reachable. It says nothing about their content.
 

You explicitly don't want this. You want the objects to already exist, with their values intact. Non-template `bless` can't do that; `bless<T>` can, but only for a single complete object of a specified type. And even then, it is still creating the object, not making it "reachable". It wasn't there until `bless<T>` was called.

Non template bless doesn't disturb any bytes already there. It is guaranteed not to do so. So I'm not getting the difference, sorry. Whether the compiler knows there is an explicit type at some address, or it's some unspecified array of something unknown but trivial, the common denominator here is that the specified memory range is now containing alive objects, where beforehand it might not have been.

This is the difference from std::launder. It requires the memory it operates upon to contain alive objects beforehand. bless does not require this.
 

On Focus:

Your section 3.2 has nothing to do with the C++ object model or the needs of mapping memory. That's not to say that they aren't useful, but the presence of side effects is orthogonal to memory mapping or the object model.

The paper is full of pieces which will be spun out into separate papers when they are mature enough.

Thanks for the detailed feedback. It was useful.

Niall

Niall Douglas

unread,
Aug 2, 2018, 5:15:13 AM8/2/18
to ISO C++ Standard - Future Proposals
My hot take is that this is waaay too low-level and implementation-detaily for a WG21 proposal. C++ programs have to be able to run on hardware that might not even have the concept of a "TLB".

There is a well defined subset of the proposal for Freestanding C++ and MMU-less CPUs. So I have it covered, just not explained in the paper yet.

I need to go to work where I have no access to external email or this forum, but I'll try to return to your other points tomorrow.

Niall

Nicol Bolas

unread,
Aug 2, 2018, 4:37:29 PM8/2/18
to ISO C++ Standard - Future Proposals
On Thursday, August 2, 2018 at 5:12:53 AM UTC-4, Niall Douglas wrote:

So you have this operation you want to be able to do. You want to take objects in one address, make them disappear from that address, and then make them appear in a (potentially) different address. And you want to change the C++ object model to make this work.

Here's a question: do you need to change the object model in order to get this effect?

So, here's what we want to be able to do without UB:
  1. Modify where allocated memory appears in the process (use cases: expanding arrays without memory copies, patching binaries, mapping page cached files etc)
  2. Share a region of memory with another running process (use cases: IPC, multiprocessing, etc)
  3. Reduce C++ causing unnecessary page faults/prevent use of memory with changed access permissions by making the compiler aware of what memory is not to be touched.
  4. Handle objects written out in a previous incarnation of a program being at a different address in a new incarnation of a program.
  5. Enforce a new read/write reordering constraint separate to compiler and thread barriers.
In hindsight, I probably ought to have specified this more succinctly in the paper. I will change it.

There are also multiple levels of persistability going on here:
  1. Trivially copyable types, which are just bits. You can use their in-memory representation directly, just need to constrain write reordering.
  2. Trivially relocatable types, which are moved with just bits, if we standardise those. You can use their in-memory representation directly, just need to constrain write reordering.
  3. Polymorphic types where if it weren't for the vptr, would be either of the first two categories. You can use their in-memory representation directly, bar the vptr restamp on load.
  4. Other types whose in-memory representation can be used directly after some trivial transformation e.g. replace all pointers with offset pointers.
So the common factor to all these is that their in-memory representation is sufficient for their persisted representation i.e. no memory copying needed, but there can be a "phase change" between "active" and "sleeping".

Serialisation is where you need to create new objects which represent a persisted edition of an in-memory form. That's a whole separate topic, to be addressed as iostreams v2 after we've nailed down the in-memory stuff which is a prerequisite as the serialised form of an object is clearly a persistable object meeting at least the criteria of the above.

There may be a common factor in these cases, but there is no common solution with them. Cases #1 and #2 are fundamentally different from #3 and #4. Why? Because it's impossible to do 3&4 on an arbitrary range of memory.

In cases 1&2, you can logically move these objects' values around and reconstitute them with no per-object code. This means you could just have the mapping/unmapping operation perform the "operation" needed to make this legal.

With 3&4, you're packing an object's values into a form from which it can be unpacked to reconstitute the original object's value. This operation has to be done on a per-object basis. That is, you can't just do "unmap range of memory, map it latter, do some casting, and go." You have to go to each object and perform some operation; code must be executed for each object before each unmap and after each map. Also, this operation has to cascade up and down the subobject hierarchy, applying that operation to each subobject in turn. Preferably in an order similar to how subobjects are created and destroyed.

If you're executing code at map/unmap time, you are doing serialization. It may not be the most complex form of serialization, but it is still serialization.

As such, I would say that what you're talking about is best handled in the C++ object model via what I said: destruction and construction. If you're unmapping memory, then any objects within it are destroyed, ending their lifetimes. When you map them, a new object is constructed in the storage whose values contain the old objects' values

Since this would be done via beginning and ending object lifetimes, that provides opportunities to inject user/compiler-code into the process via specialized constructors and destructors. For some objects (which may or may not equate to implicit lifetime or trivially relocatable types), these constructors/destructors would be trivial, and as such, we can give them special properties and effects.

If I were to give a name to this process, I would say that you are trivializing the value of an object. At its core, what you're wanting to do is to take an object, turn its values into data that is no longer considered to be "that object", but still is within the same storage. These bytes can be copied around verbatim, or the underlying storage can be persisted as you desire. You can later detrivialize the value back into an object of that exact type.

The trivialized form of an object is not that object anymore. It's just bytes of data. For some objects, its trivial form is exactly its regular form. For other objects, this is not the case. But the trivial form has the same size as the regular form.

A trivializing destructor takes the object and alters its values to make it trivial. As implied by this being invoked by a destructor, once you trivialize the object's data, the object's lifetime has ended. The storage behind a trivialized object is preserved after the object's lifetime has ended.

A detrivializing constructor takes the object representation of a trivialized object of that type and undoes the trivialization.

The one change to the object model that would be needed to do all of this would be a guarantee of the preservation of the values of a destroyed/created object. Currently, the C++ object model makes no guarantees of the value of the object's storage after its lifetime has ended. And when you begin the lifetime of an object, its storage before initialization has either zero values (if it's static) or unspecified values.

So you would need to say that trivializing destructors preserve the value of the memory, and detrivializing initialization/construction operations would maintain the storage of the memory at the start of the detrivializing constructor. And of course, objects with trivial trivializing operations (maybe "trivializing" wasn't such a good term...) can do these implicitly.

You will still need a placement-new replacement to be able to detrivialize objects with non-trivial detrivializing construction. The placement part obtains storage for the object, and that part is legally allowed to overwrite such storage. So you'd need an alternate version which isn't allowed to.

Note that a TriviallyCopyable type is not necessarily an object with trivial trivialization (wow, that's terrible naming ;). The reason being: pointers. An object with pointers is TriviallyCopyable, so you could theoretically map or unmap it. But if those pointers point to mapped memory, you need to manually trivialize and detrivialize them.

Indeed, I'm starting to wonder about that. It seems that the need for trivialization is more a property of the object than of the object's type. Trivialization ought to be distinct from trivial relocation and trivial copying. But it should still be something done via constructors/destructors.

I think "Mapped storage duration" is too on-point in terms of where the memory comes from, rather than how C++ thinks of it.

The four storage durations are based on how C++ interacts with them. Automatic storage duration objects have storage durations determined entirely by their objects' scopes. Static storage duration objects have their storage duration be the lifetime of the program. Thread-local storage duration objects have their storage duration be the lifetime of the thread. And dynamic duration objects have their storage duration determined dynamically: by direct manipulation of the code.

Sure. And I propose that mapped storage duration has the lifetime of its backing filesystem entity. Same approach.
 

But I think there is a deeper misunderstanding here. You are confusing properties of "storage" with the properties of "storage duration". The former is a thing that an object needs to have in order to exist. The latter is a property of an object, determined by the means used to construct it, and it's purpose is to govern the (default) lifetime of that object.

For example, you can create an automatic variable with automatic storage duration. You can then reuse that variable's storage to create objects with dynamic storage duration. By reusing it, you will have ended the lifetime of the automatic variable. You would then have an object with dynamic storage duration whose storage happens to be on the stack.

Which is why "stack storage duration" would be a misnomer, much like "mapped storage duration".

Would "filesystem storage duration" be better?

Let me try to explain again.

Automatic storage duration is named based on its high-level concepts: the storage duration of the object is handled "automatically" by the scope of the declaration. The memory for that storage may come from something we call the "stack", but we don't name it "stack storage duration" even though such objects are (nominally) on the "stack".

The same goes for your notion. Both "mapped" and "filesystem" are all named based on where the storage comes from. That is what is wrong with these terms. Terms should be based on the high-level concepts of the kind of storage duration you're imposing on the object, not based on where the memory is or how it's being interacted with.

I originally discounted that naming because there are mapped storage durations whose backing filesystem entity is the swap file/kernel, so I felt mentioning filesystem would be confusing.
 

Of course, C++ has a bunch of rules about what happens when you do this and then try to use that variable's name later on. As well as what happens when that variable goes out of scope, and what you need to do to make leaving scope not invoke UB (namely, create an object of that type back in that storage).

What you seem to want is simply a wider form of dynamic storage duration. You're not changing the meaning of the storage duration; you're simply adding ways to create/destroy such objects.

It's more I'm looking for a storage duration which exceeds the duration of the program, but still has a well defined duration. Dynamic storage can't do that.

You're misunderstanding something critical here. "Storage duration" is a misnomer; it has nothing to do with the duration of a piece of memory (yes, really). Storage duration defines how the lifetime of an object works, based on how it is created. Dynamic storage duration means that the lifetime of the object being created is governed explicitly by runtime-executed code.

For example:

int i; //i has automatic storage duration.
auto pf = new(&i) float;

The storage for `*pf` comes from an automatic variable, but the storage duration of the object is dynamic. It has dynamic storage duration because it is created by `operator new`, rather than a declaration or somesuch.

Therefore, the only way you would need your hypothetical "mapped storage duration" would be if you are proposing a new way to create objects. If you can create objects in mapped storage with `operator new`, then those objects have dynamic storage duration, period.

On Bless:

P0593 proposed these functions to mark regions as reachable:

Your notion of "reachability" has nothing to do with what P0593 is doing. `bless` does not make objects "reachable"; that proposal only uses the word "reachable" once, and that is merely a description about the relationship between a pointer and a size (which really ought to be a `std::span<std::byte>`).

And your definition of "reachable" here does not map to what P0593 is doing. `std::bless` and equivalent functions declare that a piece of memory creates implicit objects. When you call `bless` on a range of memory, objects will be created to make your code work. What your "reachable" behavior seems to want is for them to already exist.

Eh ... you're right that I find blessing underspecified. This is that fuzziness about lifetime I mentioned earlier. I've spent a good few weeks pondering P0593, indeed I asked a ton of questions about it as the SG12 meeting at Rapperswil. And I'm confused about what it precisely does.

Personally, I find reachable vs unreachable a much less ambiguous explanation of what it does.

If you're confused by "what it precisely does", then I'm not sure how good an idea it is to impose your own terminology on it. You need to understand what it currently means before you can say that your terminology describes that meaning better or worse. After all, if you don't understand it, your terminology could be imposing different behavior than what it's trying to do.

Which is what's happening here. What you say reachability means and what P0593 says `bless` means are two different things.

`launder` says "there is an object, at this address, of this type. Get me a pointer to it."

`bless` says "create objects of implicit lifetime types within this storage, such that my following code will be legal."

What you want your "make reachable" operation to say is "there already are some objects of various types in this storage; I want to access them." Your "reachable" concept isn't creating objects, but `std::bless` is.

You're thinking of this from a low-level "what is going on in memory" perspective. When dealing with the C++ object model, you have to think from a high-level "what are objects" perspective. This is what leads us to the following:

And more helpful for compiler writers and formal verification. But I totally get that it may simply be my incompetence, and I need to be corrected. I am not a compiler writer after all.

(Richard Smith, feel free to jump in and correct me now!)
 
This may seem like a trivial difference, but it isn't. P0593 points it out: when you create objects, the contents of the storage are rendered unspecified. And this is just as true for objects implicitly created by `std::bless`.

Sure. They become reachable. It says nothing about their content.

From the section I linked to (emphasis added):

Specifically, the value held by an object is only stable throughout its lifetime. When the lifetime of the int object in line #1 ends (when its storage is reused by the float object in line #2), its value is gone. Symmetrically, when the float object is created, the object has an indeterminate value ([dcl.init]p12), and therefore any attempt to load its value results in undefined behavior.

The thing about P0593 is that it's actually a bit inconsistent about this. Since we don't have actual standards wording, we have to do the best we can with the details we've been given. The area of inconsistency is that Section 4.1 of the same document says:

In some cases it is desirable to change the dynamic type of existing storage while maintaining the object representation. If the destination type is an implicit lifetime type, this can be accomplished by usage of std::bless to change the type, followed by std::launder to acquire a pointer to the newly-created object.

That section can only be true if the previously quoted section is false. If there's a newly created object in that storage, the only way to access the value from the recently-destroyed object that used to live there is if it's value is not "gone".

However, section 4.1 is the odd man out in P0593. Throughout that paper, it refers to the operation which `bless` performs as "creating objects". If you create an object in storage that already contains an object, then you are reusing that object's storage. Reusing an object's storage ends that object's lifetime.

And the C++ object model is very clear: the value of an object whose lifetime has ended is not preserved. The contents of an object's storage after its lifetime has ended is not defined by the standard.

So my reading of P0593 is that if you `bless` a range of memory, any objects already residing there have their lifetimes ended. The objects which have been created by this process therefore have unspecified values, since all previous objects in that storage have been reused.

And once an object's lifetime ends, the value of that object is gone.

Basically, consider this:

int i = 45;
int *pi = new(&i) int;
std
::cout << i;

The standard says that this is not guaranteed to print 45. My reading of P0593 is that the following is the equivalent of the above:

int i = 45;
std
::bless(&i, sizeof(int)); //"Creates objects" in `i`.
std
::cout << i; //Newly created `int` object, due to `bless` and access

So this is not guaranteed to print 45 either. And neither `reinterpret_cast` nor `launder` would change that.

Now, I could be misinterpreting P0593. Like I said, it doesn't have formal standards wording. But the frequent use of the phrase "create objects", coupled with the explicit prohibition of type punning, suggests that I'm probably right.

Richard Hodges

unread,
Aug 3, 2018, 3:19:30 AM8/3/18
to std-pr...@isocpp.org
Picking up on the subject of storage durations, we have:

T i = x;  // automatic storage duration

T* pi = new int(T);  // dynamic storage duration

[thread_local] static T si = x;   // static storage duration

do we merely need to be able to create new objects marked as persistent?

auto ppi = new (persistence_policy(...args...)) T ();

where persistence_policy is some policy class, derived from std::persistence_policy,  which could take care of deserialisation/serialisation, fixing vtables, remapping, etc.

Of course the keyword new would need a small modification, in order to only actually call the constructor if the policy says it should.


--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.

Niall Douglas

unread,
Aug 3, 2018, 4:47:10 AM8/3/18
to ISO C++ Standard - Future Proposals
[...]

Mapped storage has the most similarity to dynamic storage, but with the following differences:

  • It has allocation and alignment granularities with architecture specific coarse sizes (‘memory page’). 4Kb/2Mb/1Gb page sizes are common.
  • New allocations are guaranteed to be all bits zero on creation.
  • It has map on first read semantics. This means that the first read from a memory page can take hundreds of CPU cycles, and a TLB shootdown causes an interrupt for other CPUs.
  • It has allocate on first write semantics. This means that the first write to a memory page can take thousands or even hundreds of thousands of CPU cycles, plus a TLB shootdown.
  • Usually, but not always, mapped storage is a memory cache of equivalent storage a high latency storage device. Hence they can be individually pushed to storage, their contents thrown away, deallocated, given different access permission or caching strategies, and lots of other interesting (i.e. potentially game changing for large STL containers) operations. Individual pages can be:
    • Read-only, read-write, or copy-on-write. These are self describing, and these are hard characteristics: violating them means program failure.
    • Committed or uncommitted. This indicates whether the page counts towards the resources used by the C++ program. Uncommitted memory is inaccessible, and acts as a placeholder for later use (i.e. it is reserved address space, useful for expanding large arrays without content copying).
    • Dirty or clean. This indicates whether the page contains data not yet mirrored onto its backing storage.
    • Allocated or unallocated. This indicates whether storage backing the page has been allocated on the storage device. If not, the first write to a clean page may be very expensive as the page may need to be copied by the kernel and/or space allocated for it on the backing storage device.

The storage represented by a section_handle instance can be mapped into (i.e. made available to) a C++ program by creating a map_handle sourcing the storage from a section_handle instance. The storage mapped by the low level map_handle shall represent unconstructed and unreachable memory, and will require the use of std::bless() or map_view to make it reachable (alternatively, use the convenience class mapped on a section_handle instance which bundles the aforementioned low level operations on your behalf).


My hot take is that this is waaay too low-level and implementation-detaily for a WG21 proposal. C++ programs have to be able to run on hardware that might not even have the concept of a "TLB".  The stuff in this bulleted list is perfectly reasonable internal documentation for an implementation, but it seems like the sort of thing that ought to be abstracted away from everyday C++ programmers as much as possible.

Ok, back to replies to feedback. Sorry I had to cut off my reply yesterday, I hadn't realised the late time.

I think that if C++ is to reach a low latency useful use case i.e. we are going to get closer to the metal, you need to start building in an awareness of memory not all being the same. As P1027 up before SG14 next telecon shows in pretty graphs, memory on modern CPUs has significant latency variation. Simply ignoring the reality on the ground does not bring a language ecosystem closer to the metal!

Now all that said, I totally agree that the average C++ programmer should never see this stuff. But it should be there, if they go looking for it, in my opinion.
 
I would want the trajectory to be:
- implement this stuff
- implement something higher-level and suitably abstract on top of this stuff
- consider how to standardize that abstract interface
- don't bother to mention the low-level details in that proposal (but have the reference implementation available for vendors to look at)

That's an approach. But probably impractical.

My approach is to implement the simple stuff, like wrapping POSIX.2008 into standard C++ like this proposal does, and the minimum necessary amount of language support. Eric already has ideas for plugging this stuff into Ranges.

Then leave the ecosystem figure out a high level abstract interface, because that one is hard. I'd personally like to see a Boost library on this before anybody moves on standardising something.

I very strongly disagree that the committee is the right place to decide on the higher level interface. I say that as somebody having worked in this specialisation for six years now, and knowing and having spoken to lots of Serialisation folk about correct Serialisation. Everybody agrees on the common direction. But the details are hard.
 

I'm sure I'm ill-informed and there will be apparently good reasons not to take this trajectory, but it's still what I would want to see as an end-user.
(And yes, I wish the Networking TS would have taken the same trajectory.)

The Networking TS did go via the ecosystem route. Though, what will be standardised as Networking now looks increasingly very far away from ASIO a few years ago. I worry.
 

3.1.3 A new lifetime stage: unreachable

I propose adding a new separate lifetime status for objects: unreachable. Under P0593, unconstructed objects are unreachable, but there is no possibility for a partially constructed, alive or partially destructed object to be unreachable. I propose that this new status ought to be added [...]


The paper may answer this question, but: Is this related at all to the existing garbage-collection hooks, http://eel.is/c++draft/util.dynamic.safety, which already define terminology such as "declare reachable"?

Good point about confusing terminology. No.
 

P0593 proposed these functions to mark regions as reachable:


// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly creates objects within the denoted region.
void std::bless(void *start, size_t length);
 
// Effects: create an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation.
template<typename T> T *std::bless(void *p);

Hooray!  For purposes of P1144 relocatability, btw, I am hoping that the words "implicit-lifetime" will be removed from the above comments.  My understanding of "implicit-lifetime" is that implicit-lifetime types are precisely those types which it would be safe to access even without a call to std::bless.  However, I am out of the loop.

Those words were directly copy and pasted from Richard's paper.
 

Thus the obvious corrollary for marking regions as unreachable:


// Requires: [start, (char*)start + length) denotes a region of allocated
// storage that is a subset of the region of storage reachable through start.
// Effects: implicitly uncreates objects within the denoted region.
void std::unbless(void *start, size_t length);
 
// Effects: uncreate an object of implicit lifetype type T in the storage
//          pointed to by T, while preserving the object representation.
template<typename T> void *std::unbless(T *p); 

Nit: The template version should return `void` not `void*`, right?

I believe the above is correct. It's the reverse operation of bless, which consumes a void * for the typed edition.
 
I also don't fully understand the desired semantics of the non-template version of either `bless` or `unbless`.

Same as non-template bless.

Richard's paper infers you would be able to non-template bless by calling the destructor of some trivial type e.g. std::byte on a range of memory. Proposed unbless() just does the same thing in a more convenient function form.
 

And I don't understand why you need "stun / polymorphic_unbless" at all. What does it even mean to make a polymorphic object "unusable"? How is that different from just doing nothing at all?

The C++ standard doesn't say how to implement polymorphism. You are correct that on a vptr based implementation, one doesn't have to reset the vptrs. But for a non-vptr implementation, well who knows?
 

And if "revive / polymorphic_bless" is not a no-op but actually does have an observable effect on memory, that's a big deal; you should emphasize that somehow.

That's also an interesting question. I am unsure if the polymorphic implementation is considered to have any observable effect on memory under the strict language of the standard. In other words, vptrs "don't count" if that makes sense. This is why I don't claim that polymorphic blessing as you put it has any observable effects on memory, because perhaps with some polymorphic implementations it might not?

Niall

Ben Craig

unread,
Aug 3, 2018, 8:12:25 AM8/3/18
to ISO C++ Standard - Future Proposals
I haven't read all the replies here, so apologies if I cover old territory.  Also, this is only feedback on the "high priority" section that Niall posted.

I think I am fine with the idea of mapped storage.  I like the ability to decouple reachability and lifetime.  I will offer some bikeshed fodder, perhaps "persistent storage"... though that doesn't work great for non-filesystem backed memory mappings.

I think that a large portion of 3.1.2. should be marked as background information, or perhaps as forward references, but I think a lot of this section is at the wrong level of detail for the standard.  You should focus on the effects that mapped storage will have on the abstract machine here.  You can mention that section_handle and map_view are ways to acquire / manipulate mapped storage, but you shouldn't talk about them in detail here.

"New allocations are guaranteed to be all bits zero on creation."
This is almost always going to be the case in hosted environments for security reasons.  On freestanding implementations, the zero'ing isn't a sunk cost like it is on hosted implementations.  I think that you should say that the contents are unspecified on creation.

3.1.4
I'm not super interested in mapped polymorphic objects.  I am interested in having objects reachable from multiple mappings simultaneously.  Inter-process communication with shared memory is a real thing, and I think mapped storage in combination with regular C++11 atomics makes it work.

I expect resistance from the implementers when it comes to modifying the vptr.  Optimizers can currently assume it won't change, except when going through launder barriers.

I think I would prefer only allowing implicit lifetime types into mapped memory.

3.2
I generally like it.  You might want to see if there is any further inspiration to draw from gcc assembly clobber lists.

Niall Douglas

unread,
Aug 3, 2018, 12:19:52 PM8/3/18
to ISO C++ Standard - Future Proposals

There are also multiple levels of persistability going on here:
  1. Trivially copyable types, which are just bits. You can use their in-memory representation directly, just need to constrain write reordering.
  2. Trivially relocatable types, which are moved with just bits, if we standardise those. You can use their in-memory representation directly, just need to constrain write reordering.
  3. Polymorphic types where if it weren't for the vptr, would be either of the first two categories. You can use their in-memory representation directly, bar the vptr restamp on load.
  4. Other types whose in-memory representation can be used directly after some trivial transformation e.g. replace all pointers with offset pointers.

There may be a common factor in these cases, but there is no common solution with them. Cases #1 and #2 are fundamentally different from #3 and #4. Why? Because it's impossible to do 3&4 on an arbitrary range of memory.

Arbitrary untyped range of memory, yes.
 

In cases 1&2, you can logically move these objects' values around and reconstitute them with no per-object code. This means you could just have the mapping/unmapping operation perform the "operation" needed to make this legal.

With 3&4, you're packing an object's values into a form from which it can be unpacked to reconstitute the original object's value. This operation has to be done on a per-object basis. That is, you can't just do "unmap range of memory, map it latter, do some casting, and go." You have to go to each object and perform some operation; code must be executed for each object before each unmap and after each map. Also, this operation has to cascade up and down the subobject hierarchy, applying that operation to each subobject in turn. Preferably in an order similar to how subobjects are created and destroyed.

I do see that having Reflection to hand could turn this from a compiler generated vptr restamp operation into a library based one.

The level of UB needed to hack a library implementation of vptr restamping is hideous, but internal UB by the standard library is explicitly defined as not UB.
 

If you're executing code at map/unmap time, you are doing serialization. It may not be the most complex form of serialization, but it is still serialization.

Maybe it's just me, but serialisation always involves transforming representation via copy in my head. Nobody calls manipulating data in a file directly serialisation. You're just working with data in a file directly. Allocating memory in a file is no different to allocating it anywhere else in the system.
 

As such, I would say that what you're talking about is best handled in the C++ object model via what I said: destruction and construction. If you're unmapping memory, then any objects within it are destroyed, ending their lifetimes. When you map them, a new object is constructed in the storage whose values contain the old objects' values

Since this would be done via beginning and ending object lifetimes, that provides opportunities to inject user/compiler-code into the process via specialized constructors and destructors. For some objects (which may or may not equate to implicit lifetime or trivially relocatable types), these constructors/destructors would be trivial, and as such, we can give them special properties and effects.

I can get onboard with the idea of ending lifetime and starting lifetime if it makes things easier.

I disagree in the strongest possible terms that constructors and destructors ought to be used, even if special ones. That would be severely anti-social to the user base through being extremely confusing, as we are overloading a feature already far too overloaded with multiple use cases, meanings and semantics. If anything, we should be purging possible special constructors and destructors.

I could get onboard some sort of brand new operator. So you apply this special operator to an object - let's call it @<< - it gets put into an asleep state, and its lifetime is ended as far as the compiler is concerned. One can then apply the reverse form of that operator @>> to waken the object from sleep, and its lifetime is new as far as the compiler is concerned.

But adding operators to C++ has not gone well, historically. I don't believe anybody has succeeded since operator new and delete in fact. Not even operator<<< for rotation, which seems a slam dunk to me at least.
 

If I were to give a name to this process, I would say that you are trivializing the value of an object. At its core, what you're wanting to do is to take an object, turn its values into data that is no longer considered to be "that object", but still is within the same storage. These bytes can be copied around verbatim, or the underlying storage can be persisted as you desire. You can later detrivialize the value back into an object of that exact type.

The trivialized form of an object is not that object anymore. It's just bytes of data. For some objects, its trivial form is exactly its regular form. For other objects, this is not the case. But the trivial form has the same size as the regular form.

I can get aboard with this approach.
 

A trivializing destructor takes the object and alters its values to make it trivial. As implied by this being invoked by a destructor, once you trivialize the object's data, the object's lifetime has ended. The storage behind a trivialized object is preserved after the object's lifetime has ended.

A detrivializing constructor takes the object representation of a trivialized object of that type and undoes the trivialization.

The one change to the object model that would be needed to do all of this would be a guarantee of the preservation of the values of a destroyed/created object. Currently, the C++ object model makes no guarantees of the value of the object's storage after its lifetime has ended. And when you begin the lifetime of an object, its storage before initialization has either zero values (if it's static) or unspecified values.

Agreed. This is why I really don't think constructors and destructors should be used for this. They have a specific meaning in people's minds. Which is a birth/death meaning, not a birth/revive vs death/sleep meaning.
 

So you would need to say that trivializing destructors preserve the value of the memory, and detrivializing initialization/construction operations would maintain the storage of the memory at the start of the detrivializing constructor. And of course, objects with trivial trivializing operations (maybe "trivializing" wasn't such a good term...) can do these implicitly.

You will still need a placement-new replacement to be able to detrivialize objects with non-trivial detrivializing construction. The placement part obtains storage for the object, and that part is legally allowed to overwrite such storage. So you'd need an alternate version which isn't allowed to.

Operator new also has far too many overloaded meanings and use cases. I appreciate WG21's reticence to add ever more operators, but sometimes a new operator really is the right thing to do.
 

Note that a TriviallyCopyable type is not necessarily an object with trivial trivialization (wow, that's terrible naming ;). The reason being: pointers. An object with pointers is TriviallyCopyable, so you could theoretically map or unmap it. But if those pointers point to mapped memory, you need to manually trivialize and detrivialize them.

One has two options here. One can either add a new offset pointer to the language/library. They're pretty low overhead in fact, probably statistically unmeasurable in real world code. One then makes it a compile time error to "trivialise"/sleep an object containing ordinary pointers.

The other is some sort of pointer colouring, so your pointer via a non-deterministic piece of code probably can be deduced into which map_handle it points into, and if it's the same mapping it is converted into an offset pointer and permitted, otherwise a runtime error occurs. I'm not so keen on this approach. I'd prefer to leave it to users to choose if they want to implement that themselves.
 

Indeed, I'm starting to wonder about that. It seems that the need for trivialization is more a property of the object than of the object's type. Trivialization ought to be distinct from trivial relocation and trivial copying. But it should still be something done via constructors/destructors.

Agreed, except for the point about constructors/destructors.
 

Would "filesystem storage duration" be better?

Let me try to explain again.

Automatic storage duration is named based on its high-level concepts: the storage duration of the object is handled "automatically" by the scope of the declaration. The memory for that storage may come from something we call the "stack", but we don't name it "stack storage duration" even though such objects are (nominally) on the "stack".

The same goes for your notion. Both "mapped" and "filesystem" are all named based on where the storage comes from. That is what is wrong with these terms. Terms should be based on the high-level concepts of the kind of storage duration you're imposing on the object, not based on where the memory is or how it's being interacted with.

Let's assume you're persuaded me of an operator based approach with lifetime ending and beginning, so a mapped storage duration would no longer be necessary.
 
Eh ... you're right that I find blessing underspecified. This is that fuzziness about lifetime I mentioned earlier. I've spent a good few weeks pondering P0593, indeed I asked a ton of questions about it as the SG12 meeting at Rapperswil. And I'm confused about what it precisely does.

Personally, I find reachable vs unreachable a much less ambiguous explanation of what it does.

If you're confused by "what it precisely does", then I'm not sure how good an idea it is to impose your own terminology on it. You need to understand what it currently means before you can say that your terminology describes that meaning better or worse. After all, if you don't understand it, your terminology could be imposing different behavior than what it's trying to do.

I'd like to think of it as feedback on P0593's choice of wording from somebody who studied the paper for some weeks :)
 

From the section I linked to (emphasis added):

Specifically, the value held by an object is only stable throughout its lifetime. When the lifetime of the int object in line #1 ends (when its storage is reused by the float object in line #2), its value is gone. Symmetrically, when the float object is created, the object has an indeterminate value ([dcl.init]p12), and therefore any attempt to load its value results in undefined behavior.

The thing about P0593 is that it's actually a bit inconsistent about this. Since we don't have actual standards wording, we have to do the best we can with the details we've been given. The area of inconsistency is that Section 4.1 of the same document says:

In some cases it is desirable to change the dynamic type of existing storage while maintaining the object representation. If the destination type is an implicit lifetime type, this can be accomplished by usage of std::bless to change the type, followed by std::launder to acquire a pointer to the newly-created object.

That section can only be true if the previously quoted section is false. If there's a newly created object in that storage, the only way to access the value from the recently-destroyed object that used to live there is if it's value is not "gone".

However, section 4.1 is the odd man out in P0593. Throughout that paper, it refers to the operation which `bless` performs as "creating objects". If you create an object in storage that already contains an object, then you are reusing that object's storage. Reusing an object's storage ends that object's lifetime.

And the C++ object model is very clear: the value of an object whose lifetime has ended is not preserved. The contents of an object's storage after its lifetime has ended is not defined by the standard.

So my reading of P0593 is that if you `bless` a range of memory, any objects already residing there have their lifetimes ended. The objects which have been created by this process therefore have unspecified values, since all previous objects in that storage have been reused.

And once an object's lifetime ends, the value of that object is gone.

That is a valid reading of Richard's paper. Though, if one blesses a region holding a type T, and thereafter uses it as a type T, I think that is also valid reading of Richard's paper. As far as I can tell, blessing in this situation simply causes the compiler to reload state from that region.
 

Basically, consider this:

int i = 45;
int *pi = new(&i) int;
std
::cout << i;

The standard says that this is not guaranteed to print 45. My reading of P0593 is that the following is the equivalent of the above:

int i = 45;
std
::bless(&i, sizeof(int)); //"Creates objects" in `i`.
std
::cout << i; //Newly created `int` object, due to `bless` and access

So this is not guaranteed to print 45 either. And neither `reinterpret_cast` nor `launder` would change that.

I don't see any evidence of this interpretation in Richard's paper. Blessing is when you tell the compiler "I am telling you that whatever resides in this range of bytes contains valid objects". As far as I can tell, if you then access those bytes as a type T, the compiler just takes your word on it.

Now, equally, Richard's paper does say that blessing can't be used to type pun any more than reinterpret cast can. But if storage contains some valid T, and you reinterpret that storage as something else, not modifying it, and then reinterpret it back into T, that's guaranteed valid in today's standard, and as far as I understand Richard's paper, that still remains the case with blessing as it is explicitly guaranteed to not modify what you bless.
 

Now, I could be misinterpreting P0593. Like I said, it doesn't have formal standards wording. But the frequent use of the phrase "create objects", coupled with the explicit prohibition of type punning, suggests that I'm probably right.

I don't find any fault in your reading of P0593. But I don't think it's the only valid reading of that paper. As I mentioned, I find that paper a bit confusing. I think speaking in terms of reachability and non-reachability would greatly improve the clarity of P0593. And the ability of the compiler to reason about the code. But I am not a compiler writer, and Richard is. Maybe he'll comment at some point.

Niall

Niall Douglas

unread,
Aug 3, 2018, 12:34:41 PM8/3/18
to ISO C++ Standard - Future Proposals

"New allocations are guaranteed to be all bits zero on creation."
This is almost always going to be the case in hosted environments for security reasons.  On freestanding implementations, the zero'ing isn't a sunk cost like it is on hosted implementations.  I think that you should say that the contents are unspecified on creation.

I haven't written the documentation for section_handle yet, but you'll find that the unbacked constructor will say exactly this. The backed constructor says nothing, because it's the backing file_handle which does the zeroing when you extend the file. And zeroing here isn't writing zeros to memory, it's the kernel zero page, on first write the page fault then allocates storage on the drive which is only then zeroed on non-TRIM devices.

Historically some embedded systems with filing systems did not zero file extensions, but I am unaware of any major embedded systems with filing systems in the past decade which don't. For example Windows CE didn't originally, but then started to because it was a giant security hole.
 

3.1.4
I'm not super interested in mapped polymorphic objects.  I am interested in having objects reachable from multiple mappings simultaneously.  Inter-process communication with shared memory is a real thing, and I think mapped storage in combination with regular C++11 atomics makes it work.

Technically speaking, if you read the standard wording, it currently provides no guarantees that atomics work outside of the threads running in the current process.

I am all for your help in changing that, but I would assume that your plate is fairly full right now.
 

I expect resistance from the implementers when it comes to modifying the vptr.  Optimizers can currently assume it won't change, except when going through launder barriers.

Maybe Nicol is right. If we end and start lifetime, that enables vptr restamping within the current object lifetime framework. But you are right, the compiler vendors are between cold and freezing on this proposal.
 
3.2
I generally like it.  You might want to see if there is any further inspiration to draw from gcc assembly clobber lists.

It's probably not widely known yet, but there is a new effort to deprecate volatile in C++ which may or may not become a big push soon. So we may actually see a std::clobber(std::byte *, std::size_t) which tells the compiler to reload any bytes within that range.

Niall

Nicol Bolas

unread,
Aug 3, 2018, 1:33:25 PM8/3/18
to ISO C++ Standard - Future Proposals
I understand your point on the whole parallel constructor/destructor thing. The reason I suggested to use them is that it fits within the existing paradigm, which means you don't have to do something like create a new series of special member functions. But making new functions may be the cleaner alternative.

I'm going to invent some new strawman terminology because "trivialization" just isn't working out. Let's call it "reduction" and "regeneration". So every type has "reducers", which work like destructors in that they end the object's lifetime, but they ensure that the object's values remain present. And every type has "regenerators", which work like constructors in that they begin the object's lifetime but can access the reduced value of the objects in their current space.

Both reduction and regeneration functions need to be able to take arbitrary parameters. And you need special syntax to invoke regeneration, for a specific type, on a piece of memory.

Reduction and regeneration can be trivial operations. For implicit lifetime and TriviallyRelocatable types, reduction can be done trivially. But you can also perform non-trivial reduction/regeneration of specific objects of those types.

This is actually a lot like the relationship between such types and their constructors. implicit lifetime types can have non-trivial constructors, but there are still trivial ways of interacting with them.

Some operations invoke "trivial reduction" on all complete objects in a piece of storage. These would be things like functions that unmap objects, but much like your original `unbless`, we can have a function to directly invoke this process. If that storage contains objects which do not have trivial reduction, undefined behavior results. So if you want to do non-trivial reduction, you have to do it before performing such reduction.

Similarly, some operations invoke "trivial regeneration", like the memory mapping process. And this is where we have to start getting into `bless`-like wording, where objects of trivially regeneratable types can "magically" appear based on usage. But you can also explicitly invoke non-trivial regeneration for specific objects, which causes those objects to pop into being.

So let's explore some rules. Trivial reduction requires:

* There is no user-provided default reducer overload.
* All subobjects must have trivial reduction.
* The type must be TriviallyRelocatable or Implicit Lifetime (which requires a trivial destructor).

And similarly, trivial regeneration requires:

* There is no user-provided default regeration overload.
* All subobjects must have trivial regeneration.
* The type must be TriviallyRelocatable or Implicit Lifetime.

So, here's what we need:

1. Syntax for declaring reducers/regenerator member functions. It needs to not conflict with existing names.
    * Regenerators probably need similar abilities that constructors have. Things like member initializer lists, inheriting regenerators, forwarding regenerators, etc.

2. Changes to the object model to allow for reducers/regenerators to destroy/create objects, but preserving the stored bitpattern of the results of the reduction and providing it to the regenerator on regeneration.

    * There needs to be some idea of how exceptions work with regeneration. That is, when exactly each subobject is considered live, so that exceptions thrown from later subobject regeneration can destroy them. These rules could work much like the rules of constructors, but we need to spell them out all the same.

3. Syntax for invoking regeneration on a piece of memory (which is required to contain the data from a previous reduction operation for an object of that type).

4. Syntax for invoking reduction on an object. It would probably look like an explicit destructor call.

Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.

Now, I'm not saying we should go changing Arthur's proposal, since it is much farther along than this. But it would allow us to have a second way to declare that a type is TriviallyRelocatable.

Niall Douglas

unread,
Aug 3, 2018, 1:59:43 PM8/3/18
to ISO C++ Standard - Future Proposals
On Friday, August 3, 2018 at 6:33:25 PM UTC+1, Nicol Bolas wrote:
I understand your point on the whole parallel constructor/destructor thing. The reason I suggested to use them is that it fits within the existing paradigm, which means you don't have to do something like create a new series of special member functions. But making new functions may be the cleaner alternative.

I'm going to invent some new strawman terminology because "trivialization" just isn't working out. Let's call it "reduction" and "regeneration". So every type has "reducers", which work like destructors in that they end the object's lifetime, but they ensure that the object's values remain present. And every type has "regenerators", which work like constructors in that they begin the object's lifetime but can access the reduced value of the objects in their current space.

Both reduction and regeneration functions need to be able to take arbitrary parameters. And you need special syntax to invoke regeneration, for a specific type, on a piece of memory.

Reduction and regeneration can be trivial operations. For implicit lifetime and TriviallyRelocatable types, reduction can be done trivially. But you can also perform non-trivial reduction/regeneration of specific objects of those types.

This is actually a lot like the relationship between such types and their constructors. implicit lifetime types can have non-trivial constructors, but there are still trivial ways of interacting with them.

Some operations invoke "trivial reduction" on all complete objects in a piece of storage. These would be things like functions that unmap objects, but much like your original `unbless`, we can have a function to directly invoke this process. If that storage contains objects which do not have trivial reduction, undefined behavior results. So if you want to do non-trivial reduction, you have to do it before performing such reduction.

Similarly, some operations invoke "trivial regeneration", like the memory mapping process. And this is where we have to start getting into `bless`-like wording, where objects of trivially regeneratable types can "magically" appear based on usage. But you can also explicitly invoke non-trivial regeneration for specific objects, which causes those objects to pop into being.

So let's explore some rules. Trivial reduction requires:

* There is no user-provided default reducer overload.
* All subobjects must have trivial reduction.
* The type must be TriviallyRelocatable or Implicit Lifetime (which requires a trivial destructor).

And similarly, trivial regeneration requires:

* There is no user-provided default regeration overload.
* All subobjects must have trivial regeneration.
* The type must be TriviallyRelocatable or Implicit Lifetime.

So, here's what we need:

1. Syntax for declaring reducers/regenerator member functions. It needs to not conflict with existing names.
    * Regenerators probably need similar abilities that constructors have. Things like member initializer lists, inheriting regenerators, forwarding regenerators, etc.

2. Changes to the object model to allow for reducers/regenerators to destroy/create objects, but preserving the stored bitpattern of the results of the reduction and providing it to the regenerator on regeneration.

    * There needs to be some idea of how exceptions work with regeneration. That is, when exactly each subobject is considered live, so that exceptions thrown from later subobject regeneration can destroy them. These rules could work much like the rules of constructors, but we need to spell them out all the same.

3. Syntax for invoking regeneration on a piece of memory (which is required to contain the data from a previous reduction operation for an object of that type).

4. Syntax for invoking reduction on an object. It would probably look like an explicit destructor call.

This is one of those very few occasions in standards work where I can get onboard with everything you just stated above. Well, apart from the naming of reduction/regeneration.

Tony van Eerd, I summon thee! Any thoughts on naming? You have a knack for thinking of naming acceptable to a majority.
 

Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.

Now, I'm not saying we should go changing Arthur's proposal, since it is much farther along than this. But it would allow us to have a second way to declare that a type is TriviallyRelocatable.

Still though, that's exciting because this gives Arthur the ability to relocate polymorphic objects. I agree that the current revision of his proposal shouldn't propose this, but a future addendum paper certainly could extend his work via this mechanism.

Thanks Nicol. That was a very productive discussion.

Now, I'll be frank here, I don't think I have the spare time between now and SD mailing deadline to write this up. Getting a lot of pressure to issue new editions of my other papers, especially on the deterministic exceptions front where WG14 are super keen for detail, as is Herb. But I'll certainly aim for Kona.

(BTW this is to not shut down any further feedback on my draft paper. I certainly would be very interested in what people think of Nicol's proposal. Oh, and should these operations be functions, or operators?)

Niall

Arthur O'Dwyer

unread,
Aug 3, 2018, 2:00:28 PM8/3/18
to ISO C++ Standard - Future Proposals
On Fri, Aug 3, 2018 at 10:33 AM, Nicol Bolas <jmck...@gmail.com> wrote:
I understand your point on the whole parallel constructor/destructor thing. The reason I suggested to use them is that it fits within the existing paradigm, which means you don't have to do something like create a new series of special member functions. But making new functions may be the cleaner alternative.

I'm going to invent some new strawman terminology because "trivialization" just isn't working out. Let's call it "reduction" and "regeneration". So every type has "reducers", which work like destructors in that they end the object's lifetime, but they ensure that the object's values remain present. And every type has "regenerators", which work like constructors in that they begin the object's lifetime but can access the reduced value of the objects in their current space.

I like each new iteration of your terminology less and less. :P  What was wrong with "serialization" and "deserialization"?


So let's explore some rules. Trivial reduction requires:

* There is no user-provided default reducer overload.
* All subobjects must have trivial reduction.
* The type must be TriviallyRelocatable or Implicit Lifetime (which requires a trivial destructor).

For Niall's purposes, a reducible/trivializable/serializable (persistable) object does NOT need to be either TriviallyRelocatable or ImplicitLifetime. For example, as far as I can tell, boost::interprocess::offset_ptr<T> should be the canonical building block for "persistable, shareable" objects, and it is definitely not TriviallyRelocatable.


Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.

That conclusion is incorrect. The problem is that your "reduction/regeneration" operations are not context-free: they require some knowledge of the surrounding context. "Reducing" an object in preparation for serializing it to disk can be trivial in some contexts but not others.

    struct Poly {
        virtual void foo();
    };

Suppose we have an object of type `Poly` in memory, and we want to write it out and then read it back in later, in a different process.
If that process is exactly the same as the current process, then `Poly` is trivially reducible/serializable: we just write out the bytes of its vptr. Later, when we read it back in, we reinterpret those bytes as a pointer, and it points to the same place in the new process as in our current process, and we're good to go.
But if that new process is different from our current process — either because it's running on a different architecture, or because it's a different program, or because it's a new compilation of the same program, or because it's exactly the same program but our loader does ASLR — then when we reinterpret those serialized bytes as a vptr, they'll point to garbage, and our code won't work.

In one scenario, our code works. In the other scenario, our code breaks. We cannot say for sure whether our code will work unless we know which scenario we're in.

So, if you are proposing a way that C++ can guarantee that certain code works, you must (at least implicitly) be proposing a way for the C++ compiler to know which scenario we're in.

And I don't see that mechanism here — not in Nicol's terminology as presented in this thread, and also not in Niall's paper.

P1144 "trivially relocatable" is easier, because the situation is nailed down: everything happens within a single process. This lets us say for sure that `struct Poly` is trivially relocatable. Whereas `struct Poly` may or may not be safe to serialize to disk, depending on how far you're going to transport it. Even just transporting it to a different process might easily break it.

–Arthur

Arthur O'Dwyer

unread,
Aug 3, 2018, 2:11:44 PM8/3/18
to ISO C++ Standard - Future Proposals, Niall Douglas
On Fri, Aug 3, 2018 at 10:59 AM, Niall Douglas <nialldo...@gmail.com> wrote:
On Friday, August 3, 2018 at 6:33:25 PM UTC+1, Nicol Bolas wrote:

Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.

Now, I'm not saying we should go changing Arthur's proposal, since it is much farther along than this. But it would allow us to have a second way to declare that a type is TriviallyRelocatable.

Still though, that's exciting because this gives Arthur the ability to relocate polymorphic objects. I agree that the current revision of his proposal shouldn't propose this, but a future addendum paper certainly could extend his work via this mechanism.

P1144 already (since draft revision 1 when I put the wording in) explains that having a vptr doesn't stop an object from being trivially relocatable.
I'm attaching the current draft for reference.

Polymorphism is not a problem for the existing C++ abstract machine.  It's only a problem when you start extending the abstract machine to permit sharing of data between processes that do not share the same code.

Likewise, pointers are not a problem for the existing C++ abstract machine.  They are only a problem when you start extending the abstract machine to permit sharing of data between processes that do not share the same address space.

–Arthur
object-relocation-in-terms-of-move-plus-destroy-draft-10.html

Nicol Bolas

unread,
Aug 3, 2018, 2:37:57 PM8/3/18
to ISO C++ Standard - Future Proposals
By my above definition, they would not have trivial relocation abilities, since reduction/regeneration would have to execute code. And as Arthur pointed out, there are differences between relocation and reduction/regeneration. Indeed, based on his post, TriviallyRelocatable is not something we can use to allow trivial reduction/regeneration. Or rather, it would have to be TriviallyRelocatable & non-polymorphic.

I need to think on this some more.

Tom Honermann

unread,
Aug 3, 2018, 3:19:20 PM8/3/18
to std-pr...@isocpp.org, Niall Douglas
Bike shedding is probably a bit premature here, but...
- hibernate & awaken
- dismiss & summon
- disavow & reclaim
- drop & retrieve/recover

Tom.

 

Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.

Now, I'm not saying we should go changing Arthur's proposal, since it is much farther along than this. But it would allow us to have a second way to declare that a type is TriviallyRelocatable.

Still though, that's exciting because this gives Arthur the ability to relocate polymorphic objects. I agree that the current revision of his proposal shouldn't propose this, but a future addendum paper certainly could extend his work via this mechanism.

Thanks Nicol. That was a very productive discussion.

Now, I'll be frank here, I don't think I have the spare time between now and SD mailing deadline to write this up. Getting a lot of pressure to issue new editions of my other papers, especially on the deterministic exceptions front where WG14 are super keen for detail, as is Herb. But I'll certainly aim for Kona.

(BTW this is to not shut down any further feedback on my draft paper. I certainly would be very interested in what people think of Nicol's proposal. Oh, and should these operations be functions, or operators?)

Niall

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.

Nicol Bolas

unread,
Aug 3, 2018, 3:57:51 PM8/3/18
to ISO C++ Standard - Future Proposals
I get your point on contexts. Originally, I imagined that "context" was solely for user purposes. That relocator and regenerator functions could be given parameters that represent their context. I hadn't realized that compiler-generated code would need context info too.

From the compiler's perspective, there are really only two contexts: reduction for the purposes of regenerating the object "here" (in the currently executing process) and reduction for the purposes of regenerating the object "elsewhere" (in a compatible process). Only in the latter case do you need vtable fixup.

Nicol Bolas

unread,
Aug 3, 2018, 4:11:55 PM8/3/18
to ISO C++ Standard - Future Proposals, nialldo...@gmail.com
Suspension! That's the term. It gives the right impression, that the object goes away yet is in a form that can return.

When you suspend an object, you end its lifetime but you leave the values of its storage alone. You can later unsuspend the values of that memory, beginning the lifetime of a new object.

Ville Voutilainen

unread,
Aug 3, 2018, 4:17:19 PM8/3/18
to ISO C++ Standard - Future Proposals, Niall Douglas
On 3 August 2018 at 23:11, Nicol Bolas <jmck...@gmail.com> wrote:
>> Bike shedding is probably a bit premature here, but...
>> - hibernate & awaken
>> - dismiss & summon
>> - disavow & reclaim
>> - drop & retrieve/recover
>>
>> Tom.
>
>
> Suspension! That's the term. It gives the right impression, that the object
> goes away yet is in a form that can return.
>
> When you suspend an object, you end its lifetime but you leave the values of
> its storage alone. You can later unsuspend the values of that memory,
> beginning the lifetime of a new object.

I don't think so, suspension is an overloaded term. When a suspended
coroutine resumes,
the objects that were alive during suspension time are still alive.
The same objects, not different
ones.

Nicol Bolas

unread,
Aug 3, 2018, 4:41:44 PM8/3/18
to ISO C++ Standard - Future Proposals, nialldo...@gmail.com
Well, we could just... not have coroutines ;)

Matthew Woehlke

unread,
Aug 3, 2018, 4:41:52 PM8/3/18
to std-pr...@isocpp.org
On 2018-08-03 16:17, Ville Voutilainen wrote:
> On 3 August 2018 at 23:11, Nicol Bolas <jmck...@gmail.com> wrote:
>>> Bike shedding is probably a bit premature here, but...
>>> - hibernate & awaken
>>> - dismiss & summon
>>> - disavow & reclaim
>>> - drop & retrieve/recover
>>
>> Suspension! That's the term. It gives the right impression, that the object
>> goes away yet is in a form that can return.
>>
>> When you suspend an object, you end its lifetime but you leave the values of
>> its storage alone. You can later unsuspend the values of that memory,
>> beginning the lifetime of a new object.
>
> I don't think so, suspension is an overloaded term. When a suspended
> coroutine resumes, the objects that were alive during suspension time
> are still alive. The same objects, not different ones.
Yeah...

If not for its other baggage (in particular, that we might want it for
real serialization), I personally would lean toward [un]pickle...

(I guess freeze/thaw already got rejected?)

Hmm, what else?

- reduce / revive
- chill / warm
- dissolve / reform
- disjoin / rejoin (good symmetry but otherwise lame)
- adjourn / assemble (or adjourn/convene)
- [de]mobilize
- mothball / ??
- retain / restore
- dehydrate / rehydrate

...and now a few "just for fun" ;-)

- parch / soak
- desiccate / moisten
- mummify / unwrap

--
Matthew

Ville Voutilainen

unread,
Aug 3, 2018, 5:03:21 PM8/3/18
to ISO C++ Standard - Future Proposals
dematerialize / reify
dematerialize / manifest
dematerialize / materialize

My vocabulary is not good enough to find anything better than
dematerialize on the left hand side, so materialize
would be a decent counterpart for it. I do like reify and manifest
slightly better, because the facility reifies/manifests an
object from raw bits.

Nicol Bolas

unread,
Aug 3, 2018, 5:49:44 PM8/3/18
to ISO C++ Standard - Future Proposals
Materialize is already taken by prvalues and temporaries. So "dematerialize" would be confusing.

For the time being, I've settled on "activate/deactivate".

Ville Voutilainen

unread,
Aug 3, 2018, 6:02:12 PM8/3/18
to ISO C++ Standard - Future Proposals
On 4 August 2018 at 00:49, Nicol Bolas <jmck...@gmail.com> wrote:
>> dematerialize / reify
>> dematerialize / manifest
>> dematerialize / materialize
>>
>> My vocabulary is not good enough to find anything better than
>> dematerialize on the left hand side, so materialize
>> would be a decent counterpart for it. I do like reify and manifest
>> slightly better, because the facility reifies/manifests an
>> object from raw bits.
>
>
> Materialize is already taken by prvalues and temporaries. So "dematerialize"
> would be confusing.
>
> For the time being, I've settled on "activate/deactivate".

Fine. dissolve / reify, then.

Arthur O'Dwyer

unread,
Aug 3, 2018, 6:09:57 PM8/3/18
to ISO C++ Standard - Future Proposals
On Fri, Aug 3, 2018 at 3:02 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:
On 4 August 2018 at 00:49, Nicol Bolas <jmck...@gmail.com> wrote:
>> dematerialize / reify
>> dematerialize / manifest
>> dematerialize / materialize
>[...]

> Materialize is already taken by prvalues and temporaries. So "dematerialize"
> would be confusing.
>
> For the time being, I've settled on "activate/deactivate".

Fine. dissolve / reify, then.

Before anyone can decide on a name for the thing, step 1 is to decide on what the thing actually is.

Niall's original conception was that the thing is not related to beginning or ending object lifetime at all — object lifetime just kind of persists "through" this operation.
(If the operation is related to beginning and ending object lifetime, then I propose "construct" and "destruct". ;))

My own conception is that I want to see how this functionality would actually be used in a real-world high-level library; I imagine that we don't need any special names or semantics at all. For example, we could just state that if you use "libPersistence", then

    auto p = libpersistence::open("somefile.bin");
    std::vector<int> foo = p.readobject("foo");
    p.writeobject("foo") = foo;

will just work, and we don't necessarily need to describe the mechanism by which it works — not right away, anyway. Step one is to focus on that implementation: can it be implemented? is it efficient enough for real-world uses? is it robust enough for real-world uses?

But certainly, if there is any reason to subtly prefer "reify" over "manifest" or vice versa, it will strongly depend on the subtle semantics of the operation. And we do not have any semantics at all, yet. Heck, I haven't seen a use-case yet.

–Arthur

Tom Honermann

unread,
Aug 3, 2018, 6:42:01 PM8/3/18
to std-pr...@isocpp.org, Ville Voutilainen
More fine.  detach / attach.  These correlate with existing memory
mapping terminology as exhibited by shmdt() and shmat(). Detach is also
used by std::thread though.

Tom.

Nicol Bolas

unread,
Aug 3, 2018, 10:08:27 PM8/3/18
to ISO C++ Standard - Future Proposals
On Friday, August 3, 2018 at 6:09:57 PM UTC-4, Arthur O'Dwyer wrote:
On Fri, Aug 3, 2018 at 3:02 PM, Ville Voutilainen <ville.vo...@gmail.com> wrote:
On 4 August 2018 at 00:49, Nicol Bolas <jmck...@gmail.com> wrote:
>> dematerialize / reify
>> dematerialize / manifest
>> dematerialize / materialize
>[...]
> Materialize is already taken by prvalues and temporaries. So "dematerialize"
> would be confusing.
>
> For the time being, I've settled on "activate/deactivate".

Fine. dissolve / reify, then.

Before anyone can decide on a name for the thing, step 1 is to decide on what the thing actually is.

Niall's original conception was that the thing is not related to beginning or ending object lifetime at all — object lifetime just kind of persists "through" this operation.
(If the operation is related to beginning and ending object lifetime, then I propose "construct" and "destruct". ;))

My own conception is that I want to see how this functionality would actually be used in a real-world high-level library; I imagine that we don't need any special names or semantics at all. For example, we could just state that if you use "libPersistence", then

    auto p = libpersistence::open("somefile.bin");
    std::vector<int> foo = p.readobject("foo");
    p.writeobject("foo") = foo;

will just work, and we don't necessarily need to describe the mechanism by which it works — not right away, anyway. Step one is to focus on that implementation: can it be implemented? is it efficient enough for real-world uses? is it robust enough for real-world uses?

The kind of persistence that Niall is talking about is persistence at its lowest level: what happens to a C++ object if it is in mapped memory that gets unmapped? Can it survive? Can another process receive and manipulate that object? How can we make transmission of objects work at that level?

Defining this is the point of this exercise. The high level details you're talking about for some hypothetical library are beyond this exercise. We're talking about a low-level tool for low-level needs. How high-level code deals with it is something that can be looked at once we allow the object model to be able to consider these possibilities.

Think about all of the issues that went into defining the C++11 memory model that included the possibility of data races. Those weren't based on high-level constructs and the like; it was solely about pinning down how memory works between threads. That's the kind of thing this is dealing with: how objects work in a world where storage for them can exist beyond the lifespan of the program.

Ross Smith

unread,
Aug 5, 2018, 4:45:36 PM8/5/18
to std-pr...@isocpp.org
deconstruct/reconstruct

Ross Smith


Niall Douglas

unread,
Aug 6, 2018, 1:13:53 PM8/6/18
to ISO C++ Standard - Future Proposals

Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.


By my above definition, they would not have trivial relocation abilities, since reduction/regeneration would have to execute code. And as Arthur pointed out, there are differences between relocation and reduction/regeneration. Indeed, based on his post, TriviallyRelocatable is not something we can use to allow trivial reduction/regeneration. Or rather, it would have to be TriviallyRelocatable & non-polymorphic.

I need to think on this some more.

Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality. 

If the compiler can trivially reduce, copy and regenerate on its own, that's still trivial by definition. It's also optimisable, so if the compiler knows that nothing will modify the bytes between the reduction and regeneration, it can optimise out say vptr modification and just use straight memcpy.

So, in principle, this is fine. Same thing as guaranteed copy elision.

However, I am unsure if it is wise to - especially right now - muddy the waters about relocation. We sorely need relocation sooner rather than later. A good enough stopgap implementation even.

I appreciate, Nicol, that you feel that deterministic exceptions ought to be static exceptions, however as I write up the C _Fails paper, if we want C compatibility, I think we need be a bit more flexible and impure on that point.

You may not have noticed, but D1031R1 draft 1's draft TS wording is specified in P0709 deterministic exceptions. It throws file_io_error, not error.

Niall

Niall Douglas

unread,
Aug 6, 2018, 1:35:17 PM8/6/18
to ISO C++ Standard - Future Proposals

Niall's original conception was that the thing is not related to beginning or ending object lifetime at all — object lifetime just kind of persists "through" this operation.
(If the operation is related to beginning and ending object lifetime, then I propose "construct" and "destruct". ;))

My own conception is that I want to see how this functionality would actually be used in a real-world high-level library; I imagine that we don't need any special names or semantics at all. For example, we could just state that if you use "libPersistence", then

    auto p = libpersistence::open("somefile.bin");
    std::vector<int> foo = p.readobject("foo");
    p.writeobject("foo") = foo;

will just work, and we don't necessarily need to describe the mechanism by which it works — not right away, anyway. Step one is to focus on that implementation: can it be implemented? is it efficient enough for real-world uses? is it robust enough for real-world uses?

Nobody is in any doubt that a serialisation library can be written which works well on the major compilers. There are dozens to choose from. P1026 A call for a Data Persistence (iostream v2) study group lists and benchmarks some of them.

Intel's PMDK https://github.com/pmem/pmdk goes a step further, and stores STL objects directly in persistent memory. It can use mapped kernel cache memory in lieu of proper persistent memory. So again, nobody is in any doubt that these can be written and they work well on the major compilers.

What we are doing here is figuring out how to standardise support for these sorts of library into the C++ standard. Because right now, this stuff is far into UB land. It relies on the major compilers interpreting UB a certain way, because if you don't rely heavily on UB you get terrible performance.

If we are to build an iostreams v2, we need to improve the C++ object and lifetime model such that resorting to UB tricks is no longer necessary. My proposition is that these improvements would also benefit codegen and static analysis in any case, because they help the compiler reason more strongly and more accurately about code. So they're worth doing in any case.

They are also hard to do. There is a reason nobody has standardised mapped memory support into C++ yet. It's a deep well and a long road.
 

But certainly, if there is any reason to subtly prefer "reify" over "manifest" or vice versa, it will strongly depend on the subtle semantics of the operation. And we do not have any semantics at all, yet. Heck, I haven't seen a use-case yet.


Any of the examples in P1031's long list of example use cases which use mapped memory become UB as soon you unmap and remap any memory you placed an object into. They're use cases.

More deeply, we cannot currently implement malloc() in C++ without relying on UB. I'd like to fix that. There's another use case.

Niall

Niall Douglas

unread,
Aug 6, 2018, 1:59:04 PM8/6/18
to ISO C++ Standard - Future Proposals
The kind of persistence that Niall is talking about is persistence at its lowest level: what happens to a C++ object if it is in mapped memory that gets unmapped? Can it survive? Can another process receive and manipulate that object? How can we make transmission of objects work at that level?

Defining this is the point of this exercise. The high level details you're talking about for some hypothetical library are beyond this exercise. We're talking about a low-level tool for low-level needs. How high-level code deals with it is something that can be looked at once we allow the object model to be able to consider these possibilities.

Think about all of the issues that went into defining the C++11 memory model that included the possibility of data races. Those weren't based on high-level constructs and the like; it was solely about pinning down how memory works between threads. That's the kind of thing this is dealing with: how objects work in a world where storage for them can exist beyond the lifespan of the program.

The analogy to the changes to the memory model to support threads is apt.

Even if we limited ourselves to solely trivially copyable types, there is a whole new world of read and write reordering barriers to those of atomics and threads, which are like those for threads, but are wholly independent.

We have this problem with persistence because we don't control when the kernel/CPU flushes modifications to an object to storage - either or both may completely reorder modifications at will, which is a race. Therefore we need to build an awareness into the C++ object model that persisting an object is a reordering barrier at the compiler, current CPU and storage level. Note this does not include thread level. In this way, persistent storage is different to shared memory, even if we use the same library primitive functions for both.

I'm personally minded that the next layer on top of mapped memory ought to be records a la VMS. In other words, you can define a given file to be an array of objects with a known type and size, and the manipulation of each which is guaranteed to be atomic. Support for these is partial amongst the major operating systems, POSIX treats it as an extension. But their availability certainly simplifies a lot of code, and if they don't suit your use case, fall back to the byte layer underneath (the one we are defining now).

Anyway, that's later, and I suspect I can't return to this until the Kona mailing date anyway.

Niall

Arthur O'Dwyer

unread,
Aug 6, 2018, 3:10:34 PM8/6/18
to ISO C++ Standard - Future Proposals
On Mon, Aug 6, 2018 at 10:13 AM, Niall Douglas <nialldo...@gmail.com> wrote:

Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.


By my above definition, they would not have trivial relocation abilities, since reduction/regeneration would have to execute code. And as Arthur pointed out, there are differences between relocation and reduction/regeneration. Indeed, based on his post, TriviallyRelocatable is not something we can use to allow trivial reduction/regeneration. Or rather, it would have to be TriviallyRelocatable & non-polymorphic.

I need to think on this some more.

Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality. 

I should point out that that is not my definition of triviality.  My definition of triviality is "as-if by memfoo." That is, for any given operation on an object-in-memory (e.g. copying, e.g. relocation, e.g. comparison, e.g. destruction, e.g. swapping) there is exactly one "trivial" implementation of that operation, which relies on absolutely nothing about the object's type except maybe its size.

This is subtly different from the current Standard's notion of "triviality," which is closer to Niall's (but still not identical). The Standard's notion is that any operation is "trivial" iff it runs no user-provided functions.

E.g. copying a `volatile int` is "trivial" by the Standard's definition but not by mine (since it cannot be done by memcpy) nor Niall's (since it cannot be memoized).
E.g. constructing a `float` from an `int` is "trivial" by the Standard's definition (is_trivially_constructible_v<float,int>) and by Niall's, but not by mine (since it cannot be done by memcpy).
E.g. comparing two `float`s is "trivial" by the Standard's definition and by Niall's, but not by mine (since it cannot be done by memcmp).



If the compiler can trivially reduce, copy and regenerate on its own, that's still trivial by definition. It's also optimisable, so if the compiler knows that nothing will modify the bytes between the reduction and regeneration, it can optimise out say vptr modification and just use straight memcpy.

I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.

    struct Vec { int *p; };  // cannot be written out and "regenerated" later
    struct MemFn { T *p; void (*f)(T); };  // cannot be written out and "regenerated" later
    struct Poly { virtual void foo(); };  // cannot be written out and "regenerated" later

I definitely wouldn't worry about the third example until you've got something working-in-practice for the first two, simpler, examples.


> nobody is in any doubt that these can be written and they work well on the major compilers.
> What we are doing here is figuring out how to standardise support for these sorts of library into the C++ standard. [...]
> we need to improve the C++ object and lifetime model such that resorting to UB tricks is no longer necessary.

I think that's simply "std::bless should work for non-implicit-lifetime types," as suggested in the "Further Work" section of P0593,
a combined operation to create an object of implicit lifetime type in-place while preserving the object representation may be useful ... we could imagine extending its semantics to also permit conversions where each subobject of non-implicit-lifetime type in the destination corresponds to an object of the same type (ignoring cv-qualifications) in the source ...

All you need is a way to say "here are some bytes, please treat them as an object."  Right now, everyone (including STL vendors) does that via reinterpret_cast, and it works great, and it isn't going to break anytime soon (because all STL implementations depend on it).  Post-P0593-further-work, some people may switch to using reinterpret_cast plus std::bless, which will work equally great (modulo an initial crop of compiler bugs, I'm sure).  I don't think the UB-ness of reinterpret_cast is really the biggest blocker to getting persistent data structures into C++.

–Arthur

Arthur O'Dwyer

unread,
Aug 6, 2018, 3:42:39 PM8/6/18
to ISO C++ Standard - Future Proposals
On Mon, Aug 6, 2018 at 10:35 AM, Niall Douglas <nialldo...@gmail.com> wrote:

Niall's original conception was that the thing is not related to beginning or ending object lifetime at all — object lifetime just kind of persists "through" this operation.
(If the operation is related to beginning and ending object lifetime, then I propose "construct" and "destruct". ;))

My own conception is that I want to see how this functionality would actually be used in a real-world high-level library; I imagine that we don't need any special names or semantics at all. For example, we could just state that if you use "libPersistence", then

    auto p = libpersistence::open("somefile.bin");
    std::vector<int> foo = p.readobject("foo");
    p.writeobject("foo") = foo;

will just work, and we don't necessarily need to describe the mechanism by which it works — not right away, anyway. Step one is to focus on that implementation: can it be implemented? is it efficient enough for real-world uses? is it robust enough for real-world uses?

Nobody is in any doubt that a serialisation library can be written which works well on the major compilers. There are dozens to choose from. P1026 A call for a Data Persistence (iostream v2) study group lists and benchmarks some of them.

- memcpy
- yas
- zpp
- Boost.Serialization
- std::stringstream

These are all what I would call serialization libraries, yes. They're ways of taking an object of type T as input, and producing some kind of formatted string as output (maybe JSON, maybe XML, maybe some proprietary binary format), in a neatly reversible way. Notably they do not involve the filesystem at all, and they do not involve the object model at all. Serialization is a completely value-space operation.

    T someobj;
    std::string s = serialize(someobj, FMT_JSON);  // or FMT_BINARY, same difference
    T otherobj = deserialize(s, FMT_JSON);
    assert(otherobj == someobj);  // more or less

I say "more or less" because of course if `T` is `std::shared_ptr<U>`, then these libraries aren't going to make `otherobj == someobj` in the C++ sense; they're going to allocate a new object for `otherobj` to point to. That's because that's their use-case. Their use-case is things like save-games — things where the original objects don't need to persist in any sense.  These libraries are all about serialization (marshalling, pickling, stringification...).  They don't have anything to do with persistence.  They also do not rely on UB.

 
Intel's PMDK https://github.com/pmem/pmdk goes a step further, and stores STL objects directly in persistent memory. It can use mapped kernel cache memory in lieu of proper persistent memory. So again, nobody is in any doubt that these can be written and they work well on the major compilers.

PMDK is what I'm talking about when I say "persistence."  I contend that PMDK is still fairly experimental, though; much more experimental than any of the serialization libraries you listed above.  Its API doesn't look anything like a JSON-serialization library, nor does it look anything like what's in your P1031R0.

I observe that
- Serialization is fundamentally value-semantic.
- Persistence is fundamentally object-semantic.

–Arthur

Arthur O'Dwyer

unread,
Aug 6, 2018, 4:40:13 PM8/6/18
to ISO C++ Standard - Future Proposals
On Tuesday, July 31, 2018 at 12:08:27 PM UTC-7, Niall Douglas wrote:
So after many weeks of pondering and writing this outside of work each day, please find attached an early draft of revision 1 of P1031 destined for San Diego. Changes since R0:
  • Wrote partial draft TS wording for deadline, handle, io_handle, mapped, mapped_view, native_handle_type and file_io_error.
  • Added impact on the standard regarding potential changes to the C++ object model.
  • Added impact on the standard regarding the proposed [[no_side_effects]] et al contracts attributes.

`class mapped` is referred to as `mapped_span` in two places.

`class handle`'s move-constructor is marked [[move_relocates]] even though `class handle` has a virtual destructor.
Your P1029R0 proposes that this should mean the attribute is silently ignored.
My D1144 proposes that if you were to mark `class handle` as [[trivially_relocatable]], the compiler would trust you; but that wouldn't change the fact that you'd be lying to it, and you'd get UB at runtime when the compiler incorrectly assumed it could replace some arbitrary derived class's destructor with a `memcpy`.

Should we open a side thread to talk about under what circumstances it might be okay to memcpy a thing with a virtual destructor?  (My D1144 has a well-formed opinion on the topic, but I admit I don't do much mixing of classical polymorphism with move-semantics in my own code.)

–Arthur

Nicol Bolas

unread,
Aug 6, 2018, 4:43:09 PM8/6/18
to ISO C++ Standard - Future Proposals
On Monday, August 6, 2018 at 3:10:34 PM UTC-4, Arthur O'Dwyer wrote:
On Mon, Aug 6, 2018 at 10:13 AM, Niall Douglas <nialldo...@gmail.com> wrote:

Thinking about this further, "trivial relocation" could be redefined as trivial reduction, memcpy, and trivial regeneration. And thus, a type is trivially relocatable if it has no-argument reducer and regenerators that are trivial, which can be declared with `= default` syntax.


By my above definition, they would not have trivial relocation abilities, since reduction/regeneration would have to execute code. And as Arthur pointed out, there are differences between relocation and reduction/regeneration. Indeed, based on his post, TriviallyRelocatable is not something we can use to allow trivial reduction/regeneration. Or rather, it would have to be TriviallyRelocatable & non-polymorphic.

I need to think on this some more.

Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality. 

I should point out that that is not my definition of triviality.  My definition of triviality is "as-if by memfoo." That is, for any given operation on an object-in-memory (e.g. copying, e.g. relocation, e.g. comparison, e.g. destruction, e.g. swapping) there is exactly one "trivial" implementation of that operation, which relies on absolutely nothing about the object's type except maybe its size.

This, exactly. This is what I think of when dealing with triviality.

And note: types with virtuals are not trivially default constructible, nor are they Trivially Copyable. So the standard is at least partially in agreement with this notion.

This is subtly different from the current Standard's notion of "triviality," which is closer to Niall's (but still not identical). The Standard's notion is that any operation is "trivial" iff it runs no user-provided functions.

E.g. copying a `volatile int` is "trivial" by the Standard's definition but not by mine (since it cannot be done by memcpy) nor Niall's (since it cannot be memoized).
E.g. constructing a `float` from an `int` is "trivial" by the Standard's definition (is_trivially_constructible_v<float,int>) and by Niall's, but not by mine (since it cannot be done by memcpy).
E.g. comparing two `float`s is "trivial" by the Standard's definition and by Niall's, but not by mine (since it cannot be done by memcmp).



If the compiler can trivially reduce, copy and regenerate on its own, that's still trivial by definition. It's also optimisable, so if the compiler knows that nothing will modify the bytes between the reduction and regeneration, it can optimise out say vptr modification and just use straight memcpy.

I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.

That's true. But unlike pointer members of the object, you can't modify the vtable pointer. By "you", I mean "the user". Such things are the domain of the compiler.

So if you want to be able to persist polymorphic types, you have to have compiler involvement.

That's why I like thinking of persistence as a form of construction/destruction. When you construct a polymorphic type, it puts the vtable pointer in place. So when you unpack/whatever the value representation of a polymorphic type, that operation can likewise put the vtable pointer in place. And part of that operation should, just like object construction, allow you to execute whatever code you need for manual fixup of any pointers.

And thus, there are types for which persistence can be trivial (or "implicit" using my current wording) and types which cannot. Pointer types can be persisted, and defaultly persistent, but they are not implicitly so.

    struct Vec { int *p; };  // cannot be written out and "regenerated" later

Whether it can be done without code execution or not depends entirely on what `p` points to and how long the object will be persistent. If `p` points to a static object and the particular persistent instance of `Vec` will not be persistent across program executions, then it's fine as is.
 
    struct MemFn { T *p; void (*f)(T); };  // cannot be written out and "regenerated" later
    struct Poly { virtual void foo(); };  // cannot be written out and "regenerated" later

I definitely wouldn't worry about the third example until you've got something working-in-practice for the first two, simpler, examples.

But the third example is the easiest and the only one the compiler can actually do for you. And most important of all, the only one you cannot do for yourself.

> nobody is in any doubt that these can be written and they work well on the major compilers.
> What we are doing here is figuring out how to standardise support for these sorts of library into the C++ standard. [...]
> we need to improve the C++ object and lifetime model such that resorting to UB tricks is no longer necessary.

I think that's simply "std::bless should work for non-implicit-lifetime types," as suggested in the "Further Work" section of P0593,
a combined operation to create an object of implicit lifetime type in-place while preserving the object representation may be useful ... we could imagine extending its semantics to also permit conversions where each subobject of non-implicit-lifetime type in the destination corresponds to an object of the same type (ignoring cv-qualifications) in the source ...

All you need is a way to say "here are some bytes, please treat them as an object."  Right now, everyone (including STL vendors) does that via reinterpret_cast, and it works great, and it isn't going to break anytime soon (because all STL implementations depend on it).  Post-P0593-further-work, some people may switch to using reinterpret_cast plus std::bless, which will work equally great (modulo an initial crop of compiler bugs, I'm sure).  I don't think the UB-ness of reinterpret_cast is really the biggest blocker to getting persistent data structures into C++.

You cannot standardize something if the object model says that its undefined behavior. You can't just say, "this has behavior X and it's undefined behavior." That's a contradiction.

Arthur O'Dwyer

unread,
Aug 6, 2018, 6:15:42 PM8/6/18
to ISO C++ Standard - Future Proposals
On Mon, Aug 6, 2018 at 1:43 PM, Nicol Bolas <jmck...@gmail.com> wrote:
On Monday, August 6, 2018 at 3:10:34 PM UTC-4, Arthur O'Dwyer wrote:
On Mon, Aug 6, 2018 at 10:13 AM, Niall Douglas <nialldo...@gmail.com> wrote:

Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality. 

I should point out that that is not my definition of triviality.  My definition of triviality is "as-if by memfoo." That is, for any given operation on an object-in-memory (e.g. copying, e.g. relocation, e.g. comparison, e.g. destruction, e.g. swapping) there is exactly one "trivial" implementation of that operation, which relies on absolutely nothing about the object's type except maybe its size.

This, exactly. This is what I think of when dealing with triviality.

And note: types with virtuals are not trivially default constructible, nor are they Trivially Copyable. So the standard is at least partially in agreement with this notion.

Once you're talking about types with vptrs, you're definitely shading into the area where "the Standard's notion of triviality" starts bifurcating into many subtly different definitions that all happen to share the same word. :/  I'm not a fan of the Standard's terminology in this area.

Default-constructing an object-with-a-vptr can be done without running any user code, but is non-trivial by my definition and also non-trivial by the Standard's (and I'm not sure about Niall's).
Copy-constructing an object-with-a-vptr is can be done without running any user code, and is "trivial" by my definition, and by Niall's, but not by the Standard's.
Destructing an object-with-a-vptr is "trivial" by all three definitions.
Destructing an object-with-a-virtual-destructor is "non-trivial" by all three definitions.



I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.

That's true. But unlike pointer members of the object, you can't modify the vtable pointer. By "you", I mean "the user". Such things are the domain of the compiler.
So if you want to be able to persist polymorphic types, you have to have compiler involvement.

I still don't get it — and now I'm even more confused as to what we're talking about, after seeing all those serialization libraries that Niall thought were related to persistence.

If you want to serialize a polymorphic object: awesome, just do it.
If you want to deserialize a polymorphic object: you'll be constructing a new object on the fly, so you don't have to overwrite any vptrs or anything. No UB here.

If you want to store a polymorphic object in persistent memory: awesome, just do it. The library (e.g. PMDK) will take care of the persistence for you.
If you want to access a polymorphic object in existing memory, without the aid of any library to bless the pointer for you: adopt the "Further Work" of P0593r2 and call std::bless<T> on that memory.

If you want to construct a polymorphic object in existing memory, some of whose bytes have the correct values and some of which have garbage values, and you do not know the correct values for those bytes: well, that's currently impossible, but if you think that's what you want to do, I'd really like to see your use-case.


That's why I like thinking of persistence as a form of construction/destruction. When you construct a polymorphic type, it puts the vtable pointer in place. So when you unpack/whatever the value representation of a polymorphic type, that operation can likewise put the vtable pointer in place. And part of that operation should, just like object construction, allow you to execute whatever code you need for manual fixup of any pointers.

This sounds like you're talking about serialization, not persistence. And deserialization will construct new objects anyway (via placement new, or regular new, or whatever). It never needs to rewrite vptrs. (Or, if I'm wrong, please point to an existing line of code in a serialization library that rewrites vptrs.)



    struct Vec { int *p; };  // cannot be written out and "regenerated" later

Whether it can be done without code execution or not depends entirely on what `p` points to and how long the object will be persistent. If `p` points to a static object and the particular persistent instance of `Vec` will not be persistent across program executions, then it's fine as is.

Right.  And there are "less strongly typed" types, such as `std::pair<int, int*>`, where you can't even hope to tell from the type of the object what its runtime behavior will be like.  But for a serialization library (like, one that reads and writes save-game files), this is fine.  The programmer just never tries to serialize a `std::pair<int, int*>` per se.

All you need is a way to say "here are some bytes, please treat them as an object."  Right now, everyone (including STL vendors) does that via reinterpret_cast, and it works great, and it isn't going to break anytime soon (because all STL implementations depend on it).  Post-P0593-further-work, some people may switch to using reinterpret_cast plus std::bless, which will work equally great (modulo an initial crop of compiler bugs, I'm sure).  I don't think the UB-ness of reinterpret_cast is really the biggest blocker to getting persistent data structures into C++.

You cannot standardize something if the object model says that its undefined behavior. You can't just say, "this has behavior X and it's undefined behavior." That's a contradiction.

Sure, philosophically speaking it might seem that way. But this model has worked great for C++ for more than 20 years. People never tire of pointing out that std::vector is unimplementable... that malloc() is unimplementable... that std::async is unimplementable... that std::optional is unimplementable... And yet, they're part of the standard library so that working programmers can use them every single day without worrying about how unimplementable they are!

All I'm saying is that step 1 should be to pick some "unimplementable, but useful" semantics that would improve programmers' lives, and implement it. And then step 2 should be to standardize that API into the library. And if standardizing that API turns out to require some additional groundwork, okay, cool... But this proposal seems to be trying to standardize only groundwork in service of no actual library API.

–Arthur

Nicol Bolas

unread,
Aug 6, 2018, 9:33:42 PM8/6/18
to ISO C++ Standard - Future Proposals
On Monday, August 6, 2018 at 6:15:42 PM UTC-4, Arthur O'Dwyer wrote:
On Mon, Aug 6, 2018 at 1:43 PM, Nicol Bolas <jmck...@gmail.com> wrote:
On Monday, August 6, 2018 at 3:10:34 PM UTC-4, Arthur O'Dwyer wrote:
On Mon, Aug 6, 2018 at 10:13 AM, Niall Douglas <nialldo...@gmail.com> wrote:

Trivial operation X simply means that the compiler can perform operation X entirely on its own, as frequently and infrequently as it so chooses, including ignoring the programmer if it would be as-if the same. That's the meaning of triviality. 

I should point out that that is not my definition of triviality.  My definition of triviality is "as-if by memfoo." That is, for any given operation on an object-in-memory (e.g. copying, e.g. relocation, e.g. comparison, e.g. destruction, e.g. swapping) there is exactly one "trivial" implementation of that operation, which relies on absolutely nothing about the object's type except maybe its size.

This, exactly. This is what I think of when dealing with triviality.

And note: types with virtuals are not trivially default constructible, nor are they Trivially Copyable. So the standard is at least partially in agreement with this notion.

Once you're talking about types with vptrs, you're definitely shading into the area where "the Standard's notion of triviality" starts bifurcating into many subtly different definitions that all happen to share the same word. :/  I'm not a fan of the Standard's terminology in this area.

Default-constructing an object-with-a-vptr can be done without running any user code, but is non-trivial by my definition and also non-trivial by the Standard's (and I'm not sure about Niall's).
Copy-constructing an object-with-a-vptr is can be done without running any user code, and is "trivial" by my definition, and by Niall's, but not by the Standard's.
Destructing an object-with-a-vptr is "trivial" by all three definitions.
Destructing an object-with-a-virtual-destructor is "non-trivial" by all three definitions.



I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.

That's true. But unlike pointer members of the object, you can't modify the vtable pointer. By "you", I mean "the user". Such things are the domain of the compiler.
So if you want to be able to persist polymorphic types, you have to have compiler involvement.

I still don't get it — and now I'm even more confused as to what we're talking about, after seeing all those serialization libraries that Niall thought were related to persistence.

If you want to serialize a polymorphic object: awesome, just do it.
If you want to deserialize a polymorphic object: you'll be constructing a new object on the fly, so you don't have to overwrite any vptrs or anything. No UB here.

If you want to store a polymorphic object in persistent memory: awesome, just do it. The library (e.g. PMDK) will take care of the persistence for you.
If you want to access a polymorphic object in existing memory, without the aid of any library to bless the pointer for you: adopt the "Further Work" of P0593r2 and call std::bless<T> on that memory.

But that doesn't work. Even if `bless<T>` could take non-implicit lifetime objects, it will not do vtable pointer fixup. And you need vtable fixup because you might be doing that persistent access on a different process with different addresses for its vtables and virtual functions.

Furthermore, even if `bless<T>` for polymorphic types worked, you would have created a very limited idea. You're starting an object's lifetime in some storage, and you're invoking code to do it. But that code cannot include any user-defined code.

It'd be like having only default/copy/move constructors in C++, with any user-defined code having to happen via two-phase construction. How is that a good thing?

Things like that are why a top-down approach to memory model stuff is not as good as a bottom-up one. With bottom-up, you have the ability to design a real foundation for building higher-level functionality, rather than building just enough low-level stuff for your arbitrary high-level functionality.

We want low-level tools for users to build what works for them, not a monolithic high-level tool with only enough low-level functionality to make the specific high-level stuff we picked out. The latter kind of design tends to lead to Stroustrup's "Remember the Vasa!" issues.

Holistic design starts at the bottom.

If you want to construct a polymorphic object in existing memory, some of whose bytes have the correct values and some of which have garbage values, and you do not know the correct values for those bytes: well, that's currently impossible, but if you think that's what you want to do, I'd really like to see your use-case.


That's why I like thinking of persistence as a form of construction/destruction. When you construct a polymorphic type, it puts the vtable pointer in place. So when you unpack/whatever the value representation of a polymorphic type, that operation can likewise put the vtable pointer in place. And part of that operation should, just like object construction, allow you to execute whatever code you need for manual fixup of any pointers.

This sounds like you're talking about serialization, not persistence.

That is a matter of semantics and is essentially irrelevant. Persistence is a form of serialization, so all persistence is serialization.

If you're going to define persistence as working only on objects whose values are completely unmodified from the last time they were valid C++ objects to the next time they are (that is, no code execution of any kind), then you can only deal with aggregates of non-pointer scalar types, period. That's not the definition Niall wants to work with here, and I see no reason why we should be so limited.

Aspects of that limitation will happen naturally (because such types do have the logical advantage of not having to run code in order to persist them), but by having a foundation to deal with persistence of objects of types that require some in-situ work at persistence boundaries, you also gain advantages when dealing which such "natural" types.

Here's what I mean. A struct holding an `int` could be implicitly persisted. But what if that `int` happens to be a POSIX file descriptor? Then persisting such an object implicitly needs to invoke UB, which means you need to have persistence be part of the object model. You need to know when you're persisting something, so that you can know that you've invoked UB when you do it to objects that can't handle it. You also need a way to determine when an object can't handle implicit persistence.

That all needs to be part of the object model to make even implicit persistence work. Extending that slightly to allow customized persistence on a per-object basis not only allows persistence of virtual types, but also allows persistence of types with pointers in them. It allows users to pass arbitrary context information so that they can choose how to package their pointers. And so forth.

And deserialization will construct new objects anyway (via placement new, or regular new, or whatever). It never needs to rewrite vptrs. (Or, if I'm wrong, please point to an existing line of code in a serialization library that rewrites vptrs.)

I've been party to proprietary serialization systems that have done exactly that. I described it earlier in the thread.

    struct Vec { int *p; };  // cannot be written out and "regenerated" later

Whether it can be done without code execution or not depends entirely on what `p` points to and how long the object will be persistent. If `p` points to a static object and the particular persistent instance of `Vec` will not be persistent across program executions, then it's fine as is.

Right.  And there are "less strongly typed" types, such as `std::pair<int, int*>`, where you can't even hope to tell from the type of the object what its runtime behavior will be like.  But for a serialization library (like, one that reads and writes save-game files), this is fine.  The programmer just never tries to serialize a `std::pair<int, int*>` per se.

All you need is a way to say "here are some bytes, please treat them as an object."  Right now, everyone (including STL vendors) does that via reinterpret_cast, and it works great, and it isn't going to break anytime soon (because all STL implementations depend on it).  Post-P0593-further-work, some people may switch to using reinterpret_cast plus std::bless, which will work equally great (modulo an initial crop of compiler bugs, I'm sure).  I don't think the UB-ness of reinterpret_cast is really the biggest blocker to getting persistent data structures into C++.

You cannot standardize something if the object model says that its undefined behavior. You can't just say, "this has behavior X and it's undefined behavior." That's a contradiction.

Sure, philosophically speaking it might seem that way. But this model has worked great for C++ for more than 20 years. People never tire of pointing out that std::vector is unimplementable... that malloc() is unimplementable... that std::async is unimplementable... that std::optional is unimplementable... And yet, they're part of the standard library so that working programmers can use them every single day without worrying about how unimplementable they are!

That basically makes the C++ standard a misnomer: the actual standard is merely what compilers agree on. That's antithetical to the entire purpose in having a standard.

I fail to see how that can be considered "working great".

Also, the point in raiding the issue of `std::vector` being unimplementable is not really about `std::vector`. Implementations of the standard library, since they're part of the C++ implementation, can do whatever they want so long as they provide the required behavior. The problem is that users cannot implement a type equivalent to `std::vector` without explicitly relying on UB.

All I'm saying is that step 1 should be to pick some "unimplementable, but useful" semantics that would improve programmers' lives, and implement it.

That's not possible without doing it at the compiler level, because those semantics cannot exist without the compiler's help.

Niall Douglas

unread,
Aug 7, 2018, 4:41:17 AM8/7/18
to ISO C++ Standard - Future Proposals
Nobody is in any doubt that a serialisation library can be written which works well on the major compilers. There are dozens to choose from. P1026 A call for a Data Persistence (iostream v2) study group lists and benchmarks some of them.

- memcpy
- yas
- zpp
- Boost.Serialization
- std::stringstream

These are all what I would call serialization libraries, yes. They're ways of taking an object of type T as input, and producing some kind of formatted string as output (maybe JSON, maybe XML, maybe some proprietary binary format), in a neatly reversible way.

For some of those e.g. Boost.Serialisation, you are correct.

For some others e.g. Flatbuffers, you are incorrect.

Those in the first category always make a copy. So, for some given C++ object X, a serialised edition of that object Y is created and that is what is sent over the wire. As Nicol put it before, object Y is trivially serialisable.

Those in the second category do not make a copy. So, for some given C++ object X, its serialised edition is the same object X, and that is what is sent over the wire. One could call these "intrusively serialisable". This approach almost always involves a reinterpret cast, plus relies on the compiler dumping and reloading state around syscalls, which is both UB and inefficient.

What I want to do at standards level sets aside everything in category one for now. I also want to set aside most of what is in category two, for now. I just want to get to being able to take a reasonably large subset of possible C++ objects, and apply a transformation to them which:
  1. Involves no memory copying.
  2. Puts them into a state from which a live C++ object can be reconstituted later.
  3. Is sufficiently unambitious that it might actually be possible to get this through the committee for C++ 23 without the compiler vendors vetoing it.
 
Notably they do not involve the filesystem at all, and they do not involve the object model at all. Serialization is a completely value-space operation.

As a nitpick, all memory on a virtual memory kernel architecture involves the filesystem. Memory is the filesystem. If you are not mapping a file, you are mapping the swap file.

But assuming you meant something else, when we speak of filesystem, we really mean "remote memory" which implies a DMA to remote storage. That can be a network card, a NUMA node, or a storage device. All of which are conceptually the same thing.

Let me put this another way: you don't bother serialising something unless you need to serialise it. That non sequiter means that if your C++ object can't be used as-is, you need to make a copy of it, or otherwise transform it, into a form which can be used. The filesystem, like a network card, has semantics incompatible with using some C++ objects without such a transformation.
 

    T someobj;
    std::string s = serialize(someobj, FMT_JSON);  // or FMT_BINARY, same difference
    T otherobj = deserialize(s, FMT_JSON);
    assert(otherobj == someobj);  // more or less

I say "more or less" because of course if `T` is `std::shared_ptr<U>`, then these libraries aren't going to make `otherobj == someobj` in the C++ sense; they're going to allocate a new object for `otherobj` to point to. That's because that's their use-case. Their use-case is things like save-games — things where the original objects don't need to persist in any sense.  These libraries are all about serialization (marshalling, pickling, stringification...).  They don't have anything to do with persistence.  They also do not rely on UB.

I am not including any of what you describe in what I am talking about here. I am solely focusing on the zero-copy, zero-allocation, zero-synchronisation subset. We need to get those sorted out before we can usefully discuss an iostreams v2. 

 
Intel's PMDK https://github.com/pmem/pmdk goes a step further, and stores STL objects directly in persistent memory. It can use mapped kernel cache memory in lieu of proper persistent memory. So again, nobody is in any doubt that these can be written and they work well on the major compilers.

PMDK is what I'm talking about when I say "persistence."  I contend that PMDK is still fairly experimental, though; much more experimental than any of the serialization libraries you listed above.  Its API doesn't look anything like a JSON-serialization library, nor does it look anything like what's in your P1031R0.

Intel's PMDK is a reasonable first generation attempt at solving this problem space. If C++ had non-UB support along the lines I described earlier, and if it followed an intrusive design, I think it could be greatly improved upon.
 

I observe that
- Serialization is fundamentally value-semantic.
- Persistence is fundamentally object-semantic.
 
I still prefer my original phrasing that serialisation involves copies and metadata above and beyond the actual objects themselves. And all that is to be excluded from scope, for now.

Niall

Niall Douglas

unread,
Aug 7, 2018, 4:48:47 AM8/7/18
to ISO C++ Standard - Future Proposals

`class mapped` is referred to as `mapped_span` in two places.

Fixed. Thanks.
 

`class handle`'s move-constructor is marked [[move_relocates]] even though `class handle` has a virtual destructor.
Your P1029R0 proposes that this should mean the attribute is silently ignored.

R1 permits virtual destructors. If the programmer uses the attribute, they're giving a guarantee to the compiler. The compiler trusts the programmer.
 
My D1144 proposes that if you were to mark `class handle` as [[trivially_relocatable]], the compiler would trust you; but that wouldn't change the fact that you'd be lying to it, and you'd get UB at runtime when the compiler incorrectly assumed it could replace some arbitrary derived class's destructor with a `memcpy`.

Should we open a side thread to talk about under what circumstances it might be okay to memcpy a thing with a virtual destructor?  (My D1144 has a well-formed opinion on the topic, but I admit I don't do much mixing of classical polymorphism with move-semantics in my own code.)
 
I'm not sure how much there is to discuss. The real opposition on committee will be to the memcpy of any polymorphic type at all.

Niall

Niall Douglas

unread,
Aug 7, 2018, 4:58:42 AM8/7/18
to ISO C++ Standard - Future Proposals

And note: types with virtuals are not trivially default constructible, nor are they Trivially Copyable. So the standard is at least partially in agreement with this notion.

Fair enough. What we really need is a new standards term for operations which are not expensive and the compiler can do on its own. I'll see what I can do on that.
 

If the compiler can trivially reduce, copy and regenerate on its own, that's still trivial by definition. It's also optimisable, so if the compiler knows that nothing will modify the bytes between the reduction and regeneration, it can optimise out say vptr modification and just use straight memcpy.

I still don't see what you hope to achieve with "vptr modification." If a persisted object contains native pointers, they will not point to the correct place when reloaded, unless <a global property of the program is carefully maintained by the programmer>. This applies to all kinds of structs, not just vptrs.

That's true. But unlike pointer members of the object, you can't modify the vtable pointer. By "you", I mean "the user". Such things are the domain of the compiler.

So if you want to be able to persist polymorphic types, you have to have compiler involvement.

It's not like this is hard for the compiler if the type is known to it. Every time you construct an object, the compiler statically writes its vptr. I've certainly written vptr restamping code in the past which simply pokes the vptr with its new "correct" value (since C++ 11, you need to use a seq_cst atomic, otherwise the compiler unhelpfully reorders the restamp with respect to other operations on the object).
 

That's why I like thinking of persistence as a form of construction/destruction. When you construct a polymorphic type, it puts the vtable pointer in place. So when you unpack/whatever the value representation of a polymorphic type, that operation can likewise put the vtable pointer in place. And part of that operation should, just like object construction, allow you to execute whatever code you need for manual fixup of any pointers.

I'm almost tempted, actually, to split object construction/destruction into two parts: the compiler defined part, and the user defined part. We'd be effectively opening up the compiler defined part to user customisation which isn't possible right now. This could also give a universal solution to bless().

If you like the idea, what syntax would be best?

Niall

floria...@gmail.com

unread,
Aug 7, 2018, 5:09:11 AM8/7/18
to ISO C++ Standard - Future Proposals
I'm not sure to fully understand, but I have the impression that persisting polymorphic objects in the unbounded case without adding any extra information is impossible.

What do you mean when say persisting a polymorphic object?

struct Base { virtual void foo(); };
struct Derived { void foo() override; };

Base* derived = /* allocate on persistent storage as Derived */;

/* persist derived object (as Base?) */

Do you want to be able to "unpersist" the derived object as a Base, or directly as a Derived because you know somehow it was actually Derived (and not a class derived from Derived)?

If you mean the latter, then there is no problem, but it might not be that useful.
If you mean the former, then I don't see how it can be done without any extra information.

If you mean something else, please correct me.

Niall Douglas

unread,
Aug 7, 2018, 1:35:24 PM8/7/18
to ISO C++ Standard - Future Proposals, floria...@gmail.com
On Tuesday, August 7, 2018 at 10:09:11 AM UTC+1, floria...@gmail.com wrote:
I'm not sure to fully understand, but I have the impression that persisting polymorphic objects in the unbounded case without adding any extra information is impossible.

What do you mean when say persisting a polymorphic object?

Final types only.

Niall 

Vinnie Falco

unread,
Aug 7, 2018, 7:09:32 PM8/7/18
to ISO C++ Standard - Future Proposals, nialldo...@gmail.com
On Friday, August 3, 2018 at 1:41:44 PM UTC-7, Nicol Bolas wrote:
Well, we could just... not have coroutines ;)

Somewhat related, I need someone who is an expert at working on Clang and LLVM to help me implement a new form of suspend-down coroutines (doesn't have to be in time for San Diego). Please reach out to me if you are able to help or know someone who can.

Thanks

Magnus Fromreide

unread,
Sep 24, 2018, 4:18:51 PM9/24/18
to std-pr...@isocpp.org, Ville Voutilainen
On Fri, Aug 03, 2018 at 06:41:59PM -0400, Tom Honermann wrote:
>
> More fine.  detach / attach.  These correlate with existing memory mapping
> terminology as exhibited by shmdt() and shmat().

If this proposal will allow only one user of an object at any time with extra
baggage beeing required in order to shift the use to another concurrently
running process then I think the similarity to shm* is bad.

One use case for shared meory is as a read-only preloaded cache of things
and that might very well be readonly so it could be used by multiple proceeses
simultaneously.

/MF
Reply all
Reply to author
Forward
0 new messages