Relocating moves proposal paper

314 views
Skip to first unread message

Niall Douglas

unread,
Apr 16, 2018, 10:38:46 AM4/16/18
to ISO C++ Standard - Future Proposals
Please find attached my proposal for the forthcoming SG14 paper proposing relocatable objects. I should stress that there are competing proposals at SG14 on this topic, and this one may not be the one selected by SG14.

There is a fair bit of (failed) prior work in this area, and this proposal aims to be utterly limited, short and simple. It is hoped, that due to its brevity and simplicity, that it may stand some chance at WG21.

Feedback welcome.

Niall
DEEEER0 SG14 move_relocates draft 3.pdf

Nicol Bolas

unread,
Apr 16, 2018, 11:46:19 AM4/16/18
to ISO C++ Standard - Future Proposals
This attribute shall be silently ignored if the type does not have a public, non-deleted, constexpr, in-class defined default constructor and if there is not a public, non-deleted, non-virtual move constructor.

I don't like the idea of silently ignoring this attribute. Here, you've specified a specific list of conditions, and if those conditions are not met, then the compiler must ignore the attribute. Since those conditions are well-defined, users can reasonably be expected to know what they are. And therefore if they use the attribute on a type that doesn't fit these conditions, then they've probably made a mistake. And that shouldn't be ignored.

To me, it should be a hard-error to specify such types with the attribute.

Oh, and constructors can't be `virtual` ;)

----

If a type T has attribute [[move_relocates]], instead of calling the defined move constructor, the compiler will implement move construction as-if by memcpy(dest, src, sizeof(T)), followed by as-if memcpy(src, &T{}, sizeof(T)).

I find myself concerned with the "will implement" bit, which is also echoed by this statement:

It is considered good practice that the move constructor be defaulted with an explanatory comment mentioning the [[move_relocates]], as the move constructor is never called on types with non ignored [[move_relocates]].

The issue with the "will implement" goes back to the nature of attributes in C++.

The [[no_unique_address]] proposal tried to weasel-word its way around the fact that it was expanding the nature of attributes. It did so by making user-visible code behave the same way regardless of the attribute being present or not.

This attribute goes even farther. A defaulted move constructor is not a correct move constructor, so this "good practice" is encouraging people to write code that won't work if the attribute is not implemented. At least with [[no_unqiue_address]], code that is correct with the attribute is still correct without it. But that's not the case here.

I don't feel it is appropriate for attributes to impose such a requirement on code generation. [[no_unique_address]] weasels out of this by making all of its behavior implementation-dependent. Which admittedly is rather silly, when we could have laid down rules for when it's required.

But that's the price you pay when you use an attribute for something that should by all rights be a keyword.

----

If STL containers see that for their type T that std::is_relocatable<T> is true, that
std::has_virtual_destructor<T> is false, and if the Allocator they are configured with has
a defaulted construct() and destroy(), they will relocate the storage for type T in the move
construction + destruction cycle by a method equivalent to copying bits, but without calling
the move constructor, nor the destructor on the moved-from storage.

Your previous discussion of [[move_relocatable]] states that such types are allowed/required to replace "move construction + destruction" with "copy + copy-default + destruction". So user-code would be restricted to exactly and only that replacement.

But here, you bless "STL containers" with something more: the ability to replace "move construction + destruction" with "copy + drop". Why are only "STL containers" allowed to do that replacement? Shouldn't any applicable user code be allowed to make this substitution?

Also, why "STL containers" in general? I don't see any real benefit for most `std::list` or `std::forward_list` implementations to do this. I don't think `std::deque` benefits from it either, or any of the other node-based containers. Even `basic_string<T>` wouldn't need it, since it already has a restriction that its `T` must be TriviallyCopyable (though the requirement that it must be a "char-like object", which must be a POD, which includes TriviallyCopyable).

This is really just about `vector`.

----

Overall, I think this a big improvement over the deduction approach. I'd much prefer that this proposal merge with Arthur's: using his standard library infrastructure for doing library-based relocation, coupled with your attribute for declaring types that allow the compiler the freedom to do special things.

That is, I would say that you should focus on the attribute and compiler behavior, and let Arthur focus on the library and memory model behavior.

Barry Revzin

unread,
Apr 16, 2018, 1:56:28 PM4/16/18
to ISO C++ Standard - Future Proposals
The paper has an example for unique_ptr:

template<class T>
class [[move_relocates]] unique_ptr { ... };

How would this work for the real unique_ptr which is also templated on its deleter? In that case, we'd want unique_ptr to be move_relocates if D is relocatable. Would that be something like this?

template<class T, class Deleter=std::default_delete<T>>
class [[move_relocates(std::is_relocatable_v<Deleter>)]] unique_ptr { ... };

vector and friends would have to work the same way. 

I'm skeptical about the defaulted move constructor. You're asking the presence of the attribute change what the default move constructor/assignment would be, but the point of the attribute is to be able to avoid invoking the move constructor to begin with and just use memcpy. 

Nicol Bolas

unread,
Apr 16, 2018, 2:33:57 PM4/16/18
to ISO C++ Standard - Future Proposals
On Monday, April 16, 2018 at 1:56:28 PM UTC-4, Barry Revzin wrote:
I'm skeptical about the defaulted move constructor. You're asking the presence of the attribute change what the default move constructor/assignment would be, but the point of the attribute is to be able to avoid invoking the move constructor to begin with and just use memcpy. 

I don't think the proposal is saying that `= default` will generate a different constructor. It's saying that, because the move constructor will never be called, you could use any implementation of it. So using `= default` is a sign that you're not doing something unexpected in that move constructor.

Arthur O'Dwyer

unread,
Apr 16, 2018, 6:49:28 PM4/16/18
to ISO C++ Standard - Future Proposals
.

Nicol Bolas

unread,
Apr 17, 2018, 3:43:01 AM4/17/18
to ISO C++ Standard - Future Proposals
On Monday, April 16, 2018 at 11:46:19 AM UTC-4, Nicol Bolas wrote:
This attribute shall be silently ignored if the type does not have a public, non-deleted, constexpr, in-class defined default constructor and if there is not a public, non-deleted, non-virtual move constructor.

I don't like the idea of silently ignoring this attribute. Here, you've specified a specific list of conditions, and if those conditions are not met, then the compiler must ignore the attribute. Since those conditions are well-defined, users can reasonably be expected to know what they are. And therefore if they use the attribute on a type that doesn't fit these conditions, then they've probably made a mistake. And that shouldn't be ignored.

To me, it should be a hard-error to specify such types with the attribute.

Oh, and constructors can't be `virtual` ;)

I would also say that for a type to be applicable for this attribute, the copy constructor must either be public or deleted (and probably should be defaulted if it is not deleted).

Alberto Barbati

unread,
Apr 17, 2018, 3:56:35 AM4/17/18
to ISO C++ Standard - Future Proposals
Il giorno lunedì 16 aprile 2018 19:56:28 UTC+2, Barry Revzin ha scritto:

I'm skeptical about the defaulted move constructor. You're asking the presence of the attribute change what the default move constructor/assignment would be, but the point of the attribute is to be able to avoid invoking the move constructor to begin with and just use memcpy. 

This bit worries me, too. Please always bear in mind that attributes, at least in principle, should be designed so that the compiler is allowed to ignore them, while still producing the same observable behaviour. If putting your attribute means that I have to change the definition of the move constructor, the attribute is no longer ignorable. In your the unique_ptr example, for instance, if the attribute is ignored, you will probably get UB due to multiple deletes on the same pointer.

BTW, since all this is about the move constructor, wouldn't it be better to put the attribute on the move constructor itself? For example:

  type& operator(type&&) [[can_relocate]] { /* definition in case the compiler doesn't relocate */ }

if the condition to apply relocation are met (these conditions includes all considerations about the other constructors), the body of the move constructor is disregarded, the move is performed as-if by memcpy and the move source is not destroyed. If the conditions for relocation are not met or if the compiler decides to ignore the attribute, a valid implemenation of the move constructor is still available and can be used to provide the correct observable behaviour.

Just my 0.02€

Avi Kivity

unread,
Apr 17, 2018, 4:26:33 AM4/17/18
to std-pr...@isocpp.org, Alberto Barbati
Why not tell the compiler to relocate? With a context keyword.

 type(type&&) = relocate;  // instead of "= default"

Alberto Barbati

unread,
Apr 17, 2018, 4:35:00 AM4/17/18
to ISO C++ Standard - Future Proposals, alberto...@gmail.com
Il giorno martedì 17 aprile 2018 10:26:33 UTC+2, Avi Kivity ha scritto:

Why not tell the compiler to relocate? With a context keyword.

 type(type&&) = relocate;  // instead of "= default"

Because I'm still not convinced that the compiler is always able to relocate even in presence of a strong request to do that. Consider this code:

void g(std::unique_ptr<int>);
void h(int*); // or std::observer_ptr<int>

void f()
{
    std
::unique_ptr<int> p { new int{42} };
   
if (/* condition */)
   
{
        g
(std::move(p)); // should it relocate?
   
}
   
else
   
{
        h
(p.get());
   
}
   
// should p be destroyed or not, here?
}

A.

Niall Douglas

unread,
Apr 17, 2018, 6:28:48 AM4/17/18
to ISO C++ Standard - Future Proposals
On Monday, April 16, 2018 at 4:46:19 PM UTC+1, Nicol Bolas wrote:
This attribute shall be silently ignored if the type does not have a public, non-deleted, constexpr, in-class defined default constructor and if there is not a public, non-deleted, non-virtual move constructor.

I don't like the idea of silently ignoring this attribute. Here, you've specified a specific list of conditions, and if those conditions are not met, then the compiler must ignore the attribute. Since those conditions are well-defined, users can reasonably be expected to know what they are. And therefore if they use the attribute on a type that doesn't fit these conditions, then they've probably made a mistake. And that shouldn't be ignored.

Benefits of being at two conferences full of standards folk and compiler writers!

The general advice I was given is that attributes must never change semantics. The compiler should always be able to ignore them, and the program works.

Hence it's not just okay to ignore them, it is best practice to have them be ignored under various constraints. I have been told with other attributes that this is common, and certainly with the proprietary attributes this is very common.
 

To me, it should be a hard-error to specify such types with the attribute.

I've definitely been persuaded that the move constructor ought to not be defaulted, and should be written so it would work, whether the attribute is ignored or not.
 

Oh, and constructors can't be `virtual` ;)

Good catch. I am tired. Thank you.
 

If a type T has attribute [[move_relocates]], instead of calling the defined move constructor, the compiler will implement move construction as-if by memcpy(dest, src, sizeof(T)), followed by as-if memcpy(src, &T{}, sizeof(T)).

I find myself concerned with the "will implement" bit, which is also echoed by this statement:

It is considered good practice that the move constructor be defaulted with an explanatory comment mentioning the [[move_relocates]], as the move constructor is never called on types with non ignored [[move_relocates]].

The issue with the "will implement" goes back to the nature of attributes in C++.

The [[no_unique_address]] proposal tried to weasel-word its way around the fact that it was expanding the nature of attributes. It did so by making user-visible code behave the same way regardless of the attribute being present or not.

This attribute goes even farther. A defaulted move constructor is not a correct move constructor, so this "good practice" is encouraging people to write code that won't work if the attribute is not implemented. At least with [[no_unqiue_address]], code that is correct with the attribute is still correct without it. But that's not the case here.

I don't feel it is appropriate for attributes to impose such a requirement on code generation. [[no_unique_address]] weasels out of this by making all of its behavior implementation-dependent. Which admittedly is rather silly, when we could have laid down rules for when it's required.

But that's the price you pay when you use an attribute for something that should by all rights be a keyword.

Yes, persuasive. We will say that the move constructor needs to be implemented correctly, even though likely never called.
 

If STL containers see that for their type T that std::is_relocatable<T> is true, that
std::has_virtual_destructor<T> is false, and if the Allocator they are configured with has
a defaulted construct() and destroy(), they will relocate the storage for type T in the move
construction + destruction cycle by a method equivalent to copying bits, but without calling
the move constructor, nor the destructor on the moved-from storage.

Your previous discussion of [[move_relocatable]] states that such types are allowed/required to replace "move construction + destruction" with "copy + copy-default + destruction". So user-code would be restricted to exactly and only that replacement.

But here, you bless "STL containers" with something more: the ability to replace "move construction + destruction" with "copy + drop". Why are only "STL containers" allowed to do that replacement? Shouldn't any applicable user code be allowed to make this substitution?

The reason is for backwards link compatibility with older C++. We need for relocating moves to be ABI compatible with older compilers, hence why we don't fiddle with move semantics and don't implement destructive move.

We can be stronger with the STL as surely the maintainer there can maintain their own ABI.
 

Also, why "STL containers" in general? I don't see any real benefit for most `std::list` or `std::forward_list` implementations to do this. I don't think `std::deque` benefits from it either, or any of the other node-based containers. Even `basic_string<T>` wouldn't need it, since it already has a restriction that its `T` must be TriviallyCopyable (though the requirement that it must be a "char-like object", which must be a POD, which includes TriviallyCopyable).

This is really just about `vector`.

I think "it depends".

I particularly think that there is a discipline to keeping all your types TriviallyCopyable, and that same discipline would extend to move_relocates, and that discipline aggregates accumulating benefits across a whole process.


Overall, I think this a big improvement over the deduction approach. I'd much prefer that this proposal merge with Arthur's: using his standard library infrastructure for doing library-based relocation, coupled with your attribute for declaring types that allow the compiler the freedom to do special things.

That is, I would say that you should focus on the attribute and compiler behavior, and let Arthur focus on the library and memory model behavior.

Oh for sure. But EWG is a different destination to LEWG. So two separate papers have the best chance, particularly if the language changes are minimum.

Niall 

Niall Douglas

unread,
Apr 17, 2018, 6:30:41 AM4/17/18
to ISO C++ Standard - Future Proposals
How would this work for the real unique_ptr which is also templated on its deleter? In that case, we'd want unique_ptr to be move_relocates if D is relocatable. Would that be something like this?

Good point. We would need to require that all base classes are also marked [[move_relocates]], otherwise [[move_relocates]] is ignored. Thank you.

Niall

Niall Douglas

unread,
Apr 17, 2018, 6:32:17 AM4/17/18
to ISO C++ Standard - Future Proposals

I would also say that for a type to be applicable for this attribute, the copy constructor must either be public or deleted (and probably should be defaulted if it is not deleted).

I'm not sure I see why the copy constructor has anything involved here. Can you explain?

Niall 

Niall Douglas

unread,
Apr 17, 2018, 6:37:49 AM4/17/18
to ISO C++ Standard - Future Proposals, alberto...@gmail.com
On Tuesday, April 17, 2018 at 9:35:00 AM UTC+1, Alberto Barbati wrote:
Il giorno martedì 17 aprile 2018 10:26:33 UTC+2, Avi Kivity ha scritto:

Why not tell the compiler to relocate? With a context keyword.

 type(type&&) = relocate;  // instead of "= default"

Yes, that was debated here at these conferences. Such syntax is nice and clean and obvious.
 

Because I'm still not convinced that the compiler is always able to relocate even in presence of a strong request to do that. Consider this code:

Indeed. We primarily decided against it because of backwards ABI compatibility where new C++ code calls into a binary compiled by a previous C++, but it was felt - without a counterexample - that there could be cases where relocation cannot occur. In your example, because move occurred, relocation occurred, so p is made null exactly as move would do it. So that is not an example of where relocation cannot happen. Where moves occur, relocation can occur, as they are the exact same operation.

Niall

Barry Revzin

unread,
Apr 17, 2018, 8:11:14 AM4/17/18
to std-pr...@isocpp.org
You'd need, at least, base classes and members to be marked [[move_relocates]]. unique_ptr<T, D> doesn't inherit from D, it just has a member D. Likewise, vector<T, A> just has a member A. But then, the libstdc++ implementation of unique_ptr<T, D> has a base class which has a member of type tuple<T, D>. So in order for that to work, tuple and all of its glorious base classes would all have to be marked [[move_relocates]]... just in case? 

Avi Kivity

unread,
Apr 17, 2018, 8:47:44 AM4/17/18
to std-pr...@isocpp.org, Alberto Barbati



On 2018-04-17 11:35, Alberto Barbati wrote:
Il giorno martedì 17 aprile 2018 10:26:33 UTC+2, Avi Kivity ha scritto:

Why not tell the compiler to relocate? With a context keyword.

 type(type&&) = relocate;  // instead of "= default"

Because I'm still not convinced that the compiler is always able to relocate even in presence of a strong request to do that. Consider this code:

void g(std::unique_ptr<int>);
void h(int*); // or std::observer_ptr<int>

void f()
{
    std
::unique_ptr<int> p { new int{42} };
   
if (/* condition */)
   
{
        g
(std::move(p)); // should it relocate?


Yes. std::unique_ptr's move constructor was defined as = relocate.


    }
   
else
   
{
        h
(p.get());
   
}
   
// should p be destroyed or not, here?
}


Yes. Relocation doesn't destroy the object, but instead reinitializes it using the memory image of a default-constructed object.


(going by Niall's proposal)

A.
--
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/556a287c-505a-4ead-a752-244df90de614%40isocpp.org.

Niall Douglas

unread,
Apr 17, 2018, 9:09:38 AM4/17/18
to ISO C++ Standard - Future Proposals
You'd need, at least, base classes and members to be marked [[move_relocates]]. unique_ptr<T, D> doesn't inherit from D, it just has a member D. Likewise, vector<T, A> just has a member A.

Yes, this is what I added to the current draft of the paper.
 
But then, the libstdc++ implementation of unique_ptr<T, D> has a base class which has a member of type tuple<T, D>. So in order for that to work, tuple and all of its glorious base classes would all have to be marked [[move_relocates]]... just in case? 

Currently, it is on the implementation to make variants of each inherited class, one with [[move_relocates]], one without, and to choose at compile time appropriately.

I appreciate that isn't pretty, but fixing that is a separate paper. This paper needs to be utterly, utterly, simple if it stands a chance at EWG.

Niall

Barry Revzin

unread,
Apr 17, 2018, 10:55:06 AM4/17/18
to ISO C++ Standard - Future Proposals


On Tuesday, April 17, 2018 at 8:09:38 AM UTC-5, Niall Douglas wrote:
You'd need, at least, base classes and members to be marked [[move_relocates]]. unique_ptr<T, D> doesn't inherit from D, it just has a member D. Likewise, vector<T, A> just has a member A.

Yes, this is what I added to the current draft of the paper.
 
But then, the libstdc++ implementation of unique_ptr<T, D> has a base class which has a member of type tuple<T, D>. So in order for that to work, tuple and all of its glorious base classes would all have to be marked [[move_relocates]]... just in case? 

Currently, it is on the implementation to make variants of each inherited class, one with [[move_relocates]], one without, and to choose at compile time appropriately.

So the options for making a class template conditionally relocatable are:
(1) Just mark it [[move_relocates]] unconditionally. Hopefully the rules we have don't end up relocating an unrelocatable type - in C++ there's always some edge case. 
(2) Duplicate the entire class, constrained on is_relocatable, and just mark the constrained one. 

The former means we have this annotation everywhere and it loses its ability to draw attention, and the latter, while easier to do in a post-concepts world, is still a lot of code duplication. 

Arthur O'Dwyer

unread,
Apr 17, 2018, 1:31:09 PM4/17/18
to ISO C++ Standard - Future Proposals
This example comes up in my C++Now talk; I actually use "reallocating a vector<unique_ptr<int>>" as my performance benchmark.
My tentative solution (slide 23) is to make std::unique_ptr conditionally trivially relocatable:

template<class T, class D = std::default_delete<T>>
 class unique_ptr {
 public:
     using deleter_type = D;
     using pointer = std::remove_reference_t<D>::pointer;

     static constexpr bool is_trivially_relocatable =
         std::is_trivially_relocatable_v<pointer> &&
         std::is_trivially_relocatable_v<deleter_type>;

     // ...
 };



It is important to remember that the "relocation operation" is the combination of one "move-construct" operation and one "destroy" operation (on the source of the move).  Relocation is not an optimization of move-construct alone.  If you start thinking in terms of "replacing move with relocate" you will rapidly confuse yourself.  You must think in terms of "replacing move+destroy with relocate."  (Slide 25 shows how std::vector uses uninitialized_relocate() as a substitute for uninitialized_move()+destroy() when it detects that the replacement is safe. Slide 18 shows how uninitialized_relocate(), in turn, detects when it is dealing with a contiguous range of trivially relocatable objects, and optimizes into a simple memcpy.)


The compiler can help us identify the base cases (slides 21+22), but there is no general way to recursively infer whether a class is intended to be trivially relocatable or not (because you cannot reliably detect semantics by examining syntax; this is the Concepts problem, and the reason for forward_iterator_tag). Since there is no general way for the compiler to infer trivial relocatability, we must have the programmer opt-in in specific cases.
Niall wants to use a novel attribute to opt-in to trivial-relocatability; my approach uses a pure-library trait modeled after is_transparent. (That is, the trait std::is_trivially_relocatable_v<T> starts out by checking for T::is_trivially_relocatable::value; otherwise it takes on the value of (std::is_trivially_move_constructible_v<T> && std::is_trivially_destructible_v<T>).)

The attribute's main problem is that it is semantically meaningful to the library. I don't personally care about that (I love [[trivial_abi]] and [[section(".sdata")]] and so on), but it is still true that (by design) we have no way for the library to detect the presence of an attribute and act upon it!  We'd have to add a mechanism to query this attribute of a class. The trait doesn't have that problem.

The trait's main problem is that it obeys all the existing rules of C++, including that a derived class can inherit an inappropriate setting for the member typedef from one of its parent classes. (We could also fix that by adding a new handwavey magic type-trait that could somehow express is_inherited<> vs. is_declared_directly_inside<>. We could easier fix that by making the opt-in mechanism a specialization, like std::hash, instead of a member typedef like is_transparent. I have not much investigated that line because it seems like it adds a lot of boilerplate to the opting-in process.  I owe a blog post on this topic.)
Another problem for the trait is that I am very worried that people are going to try to make the trait relevant to the core language in ways that require the compiler to know the magic identifier is_trivially_relocatable (in the same way that the compiler knows the magic identifiers begin, end, get, tuple_size, and tuple_element).  The attribute doesn't have this problem; it is not surprising or problematic for attributes' names to be known to the compiler.

–Arthur

Nicol Bolas

unread,
Apr 17, 2018, 4:05:55 PM4/17/18
to ISO C++ Standard - Future Proposals
On Tuesday, April 17, 2018 at 1:31:09 PM UTC-4, Arthur O'Dwyer wrote:
On Tue, Apr 17, 2018 at 5:11 AM, Barry Revzin <barry....@gmail.com> wrote:
On Tue, Apr 17, 2018 at 5:30 AM Niall Douglas <nialldo...@gmail.com> wrote:
How would this work for the real unique_ptr which is also templated on its deleter? In that case, we'd want unique_ptr to be move_relocates if D is relocatable. Would that be something like this?

Good point. We would need to require that all base classes are also marked [[move_relocates]], otherwise [[move_relocates]] is ignored. Thank you.

Niall

You'd need, at least, base classes and members to be marked [[move_relocates]]. unique_ptr<T, D> doesn't inherit from D, it just has a member D. Likewise, vector<T, A> just has a member A. But then, the libstdc++ implementation of unique_ptr<T, D> has a base class which has a member of type tuple<T, D>. So in order for that to work, tuple and all of its glorious base classes would all have to be marked [[move_relocates]]... just in case? 

This example comes up in my C++Now talk; I actually use "reallocating a vector<unique_ptr<int>>" as my performance benchmark.
My tentative solution (slide 23) is to make std::unique_ptr conditionally trivially relocatable:

template<class T, class D = std::default_delete<T>>
 class unique_ptr {
 public:
     using deleter_type = D;
     using pointer = std::remove_reference_t<D>::pointer;

     static constexpr bool is_trivially_relocatable =
         std::is_trivially_relocatable_v<pointer> &&
         std::is_trivially_relocatable_v<deleter_type>;

     // ...
 };



It is important to remember that the "relocation operation" is the combination of one "move-construct" operation and one "destroy" operation (on the source of the move).  Relocation is not an optimization of move-construct alone.  If you start thinking in terms of "replacing move with relocate" you will rapidly confuse yourself.  You must think in terms of "replacing move+destroy with relocate."  (Slide 25 shows how std::vector uses uninitialized_relocate() as a substitute for uninitialized_move()+destroy() when it detects that the replacement is safe. Slide 18 shows how uninitialized_relocate(), in turn, detects when it is dealing with a contiguous range of trivially relocatable objects, and optimizes into a simple memcpy.)


The compiler can help us identify the base cases (slides 21+22), but there is no general way to recursively infer whether a class is intended to be trivially relocatable or not (because you cannot reliably detect semantics by examining syntax; this is the Concepts problem, and the reason for forward_iterator_tag). Since there is no general way for the compiler to infer trivial relocatability, we must have the programmer opt-in in specific cases.
Niall wants to use a novel attribute to opt-in to trivial-relocatability; my approach uses a pure-library trait modeled after is_transparent.

I don't think that's the best way to look at the relationship between Niall's proposal and yours.

In Niall's proposal, the attribute is not just about "trivial relocatability" as your proposal defines it. The attribute allows for more than just memcpy+drop (indeed technically, it doesn't even allow for that). The use of the attribute requires that the compiler will never call the move constructor. Anytime move construction happens, the compiler will replace the constructor call with a pair of `memcpy`-equivalent operations. Coupled with the statement that calling the destructor on a default-constructed value is a no-op, this ensures that user code is never involved in move+destroy operations.

ABIs have the freedom to store TriviallyCopyable types in registers because no user code gets called when they get copied/moved/destroyed. As such, no user code can detect that an object is being copied when the standard says that it isn't to be copied, or not copied when the standard says it must be copied, or destroyed when the standard says it still exists, and so forth. This freedom allows TrivialCopyable types to live in registers if the compiler/ABI so chooses rather than in actual storage.

Niall's "move relocatable" property permits the same thing, just on a broader range of types. By eliminating all user code from a sequence of move+destruct calls, the ABI is now free to store such types in registers. No user code can detect that this has happened, so long as move construction and destruction-of-moved-from objects are the only operations that take place in that process.

As I understand it, enabling this is the primary goal of Niall's proposal.

Relocation as a library construct (that is, memcpy+drop) is a separate-but-related thing. That is, types that use the attribute are clearly appropriate for library relocation, but types which do not use the attribute can explicitly opt-in to library relocation as well (which is why he has a customization point for it). The library part of Niall's proposal is basically a version of your idea, just without as much stuff in it.

The two of you are taking different approaches because you're solving different problems. The problem you're trying to solve is all about library code; you don't care what the compiler is doing with those types. Niall's problem is all about compiler-generated code; while his proposal has a library component, it seems to be there because it's a convenient addition, a thing that could be solved along the way, not because it's absolutely essential.

And that's why I think that both proposals are necessary. Solving the problem of allowing more objects in registers is important. But solving the problem of standard library inefficiencies with regard to movable types is also important. And while the solutions are related (if a type can fit in registers, it certainly can do the library relocation thing), they're ultimately different.

I'd love to see a pure-language version of Niall's proposal (with appropriate solutions for inheritance, members, conditional move-relocatability, and the like) which automatically works with the trivially relocation hooks in your proposal.

Arthur O'Dwyer

unread,
Apr 17, 2018, 5:01:46 PM4/17/18
to ISO C++ Standard - Future Proposals
On Tue, Apr 17, 2018 at 1:05 PM, Nicol Bolas <jmck...@gmail.com> wrote:

In Niall's proposal, the attribute is not just about "trivial relocatability" as your proposal defines it. The attribute allows for more than just memcpy+drop (indeed technically, it doesn't even allow for that). The use of the attribute requires that the compiler will never call the move constructor. Anytime move construction happens, the compiler will replace the constructor call with a pair of `memcpy`-equivalent operations. Coupled with the statement that calling the destructor on a default-constructed value is a no-op, this ensures that user code is never involved in move+destroy operations.

It is physically possible for a type to be "trivially relocatable" (that is, move+destroy be equivalent to memcpy+drop) without having the property "destroying a default-constructed object is a no-op."  I admit I'm not sure how likely that is in idiomatic C++ code.
A simple example is nn_unique_ptr<T>, which is trivially relocatable but is not default-constructible at all. (There is a sense in which this type has a "default-constructed empty state", but that state is not literally produced by the default constructor.)
The other examples I can think of off the top of my head are fairly contrived.

ABIs have the freedom to store TriviallyCopyable types in registers because no user code gets called when they get copied/moved/destroyed. As such, no user code can detect that an object is being copied when the standard says that it isn't to be copied, or not copied when the standard says it must be copied, or destroyed when the standard says it still exists, and so forth. This freedom allows TrivialCopyable types to live in registers if the compiler/ABI so chooses rather than in actual storage.

I don't think the "no user code gets called" part is actually what allows TriviallyCopyable types to live in registers. The compiler can make other types live in registers too, if it wants. Here is an example of GCC placing a unique_ptr in a register.

int *p(int *q)
{
    std::unique_ptr<int> u(q);
    return u.release();
}

Objects can "live" in registers whenever the compiler feels like it, with one major exception: At cross-module call boundaries, both sides must agree on where the function parameter object is expected to live!  In practice, this means that the location of the parameter must be determined only by the properties of its type, and not by more local properties (such as the escape analysis that permitted GCC to place the local variable u in a register).
"Both sides must agree" is just another way of saying "there must be a standard calling convention."
The standard calling convention for Linux is defined by the Itanium C++ ABI. The Itanium C++ ABI defines where parameters are passed based on the properties of their types (a wise, but not inevitable, strategy). In particular, Itanium looks at the trivial copyability of the parameter type to decide where it's passed. (And, in more recent/future revisions, the Itanium C++ ABI will also look at whether the type has __attribute__((trivial_abi)), to decide where it's passed.)

Nicol, what in your view is the relationship between Niall's attribute proposal and __attribute__((trivial_abi))?  Would you describe Niall's proposal as just an ISO-adoption of the Itanium-status-quo, or do you see other important components as well in Niall's current proposal?

[...]
I think that both proposals are necessary. Solving the problem of allowing more objects in registers is important. But solving the problem of standard library inefficiencies with regard to movable types is also important. And while the solutions are related (if a type can fit in registers, it certainly can do the library relocation thing), they're ultimately different.

I do not believe that "if a type can fit in registers, it certainly can do the library relocation thing."  For example, its move-constructor (by itself) might have side effects that the user is not willing to discard.  Here is a compilable example:
Notice that the parameter is passed in %rdi and the result is returned in %rax.
Notice that there are no load/store operations happening. We never touch memory.
Notice that the compiler quietly eliminates the dead store and puts() from the destructor.
Finally, very importantly, notice that the compiler correctly preserves the side-effecting puts() in our type's non-trivial move-constructor.

Triviality-of-special-member-functions and ability-to-be-stored-in-registers are not inextricably related.  They are linked only by a historical (pre-2017) quirk of the Itanium C++ ABI's calling convention.
(I know MSVC does not use the Itanium calling convention. I don't know whether their calling convention currently permits passing unique_ptr-esque types in registers. Anyone here know?)

–Arthur

Nicol Bolas

unread,
Apr 17, 2018, 9:52:20 PM4/17/18
to ISO C++ Standard - Future Proposals
On Tuesday, April 17, 2018 at 5:01:46 PM UTC-4, Arthur O'Dwyer wrote:
On Tue, Apr 17, 2018 at 1:05 PM, Nicol Bolas <jmck...@gmail.com> wrote:
In Niall's proposal, the attribute is not just about "trivial relocatability" as your proposal defines it. The attribute allows for more than just memcpy+drop (indeed technically, it doesn't even allow for that). The use of the attribute requires that the compiler will never call the move constructor. Anytime move construction happens, the compiler will replace the constructor call with a pair of `memcpy`-equivalent operations. Coupled with the statement that calling the destructor on a default-constructed value is a no-op, this ensures that user code is never involved in move+destroy operations.

It is physically possible for a type to be "trivially relocatable" (that is, move+destroy be equivalent to memcpy+drop) without having the property "destroying a default-constructed object is a no-op."  I admit I'm not sure how likely that is in idiomatic C++ code.
A simple example is nn_unique_ptr<T>, which is trivially relocatable but is not default-constructible at all. (There is a sense in which this type has a "default-constructed empty state", but that state is not literally produced by the default constructor.)
The other examples I can think of off the top of my head are fairly contrived.
 

Such a type may be relocatable for library purposes, but it is not one for which you can apply the [[move_relocatable]] attribute.

TriviallyCopyable has an object model foundation for why it's OK for you to be able to do byte-copies on such objects. The compiler can see that a copy operation is just copying each value, which is equivalent to doing a byte-copy. And the trivial destructor does not actually do anything. As such, there is no object model harm to allowing such types to be copied without invoking an explicit constructor.

The [[move_relocatable]] attribute is trying to establish a similar foundation. Instead of being based on how the code looks, its foundation is based on a user promise coupled with implementation behavior. The nature of these promises is what gives the compiler the freedom to convert a move operation into a pair of `memcpy`s. And that transformation is based on the promise that you provide a default constructor and the relationship between that constructor and the destructor.

That being said, I think Niall's definition for this promise is overly restrictive. There's no reason why an `nn_unique_ptr` type should be forbidden from being [[move_relocatable]]. A different set of promises can allow it to be move-relocatable.

Niall's promise list is:

1. The type shall have a publicly-accessible (and inline) default constructor.
2. The type shall have a publicly-accessible (and inline) move constructor.
3. The type shall have a publicly-accessible (and inline) destructor.
4. The writer of the type warrants that the default constructor puts the object in a state such that calling the destructor on it has no side effects and does nothing.
5. The writer of the type warrants that the move constructor gives the newly constructed object a value equivalent to performing a `memcpy` from the original state of the moved-from object.
6. The writer of the type warrants that the move constructor puts the moved-from object in a state equivalent to a default-constructed value.

My promise list would be:

1. The type shall have a publicly-accessible move constructor.
2. The type shall have a publicly-accessible destructor.
3. The writer of the type warrants that the move constructor gives the newly constructed object a value equivalent to performing a `memcpy` from the original state of the moved-from object.
4. The writer of the type warrants that the move constructor puts the moved-from object in a state such that calling the destructor on it has no side effects and does nothing.

Niall's definition takes "move+destroy", turns it into "memcpy+memcpy+destroy", which the compiler is expected to optimize into "memcpy+drop". My way simply turns "move+destroy" into "memcpy+drop" directly; if the compiler only detects "move" with no "destroy" being visible, then that's what gets called.

The upsides of this to me are:

* expanding the number of types that can be [[move_relocatable]]
* since the move constructor can still be called, you don't have the luxury of `= default`ing it, and thus cannot create inconsistent behavior.
* compilers don't have to have special code to turn `memcpy+memcpy+destroy" into "memcpy+drop"; they can just go straight there.

The only downside I can see is that this is identical to destructive move. And that's terrible... somehow.

ABIs have the freedom to store TriviallyCopyable types in registers because no user code gets called when they get copied/moved/destroyed. As such, no user code can detect that an object is being copied when the standard says that it isn't to be copied, or not copied when the standard says it must be copied, or destroyed when the standard says it still exists, and so forth. This freedom allows TrivialCopyable types to live in registers if the compiler/ABI so chooses rather than in actual storage.

I don't think the "no user code gets called" part is actually what allows TriviallyCopyable types to live in registers. The compiler can make other types live in registers too, if it wants. Here is an example of GCC placing a unique_ptr in a register.

int *p(int *q)
{
    std::unique_ptr<int> u(q);
    return u.release();
}

Objects can "live" in registers whenever the compiler feels like it, with one major exception: At cross-module call boundaries, both sides must agree on where the function parameter object is expected to live!  In practice, this means that the location of the parameter must be determined only by the properties of its type, and not by more local properties (such as the escape analysis that permitted GCC to place the local variable u in a register).
"Both sides must agree" is just another way of saying "there must be a standard calling convention."
The standard calling convention for Linux is defined by the Itanium C++ ABI. The Itanium C++ ABI defines where parameters are passed based on the properties of their types (a wise, but not inevitable, strategy). In particular, Itanium looks at the trivial copyability of the parameter type to decide where it's passed. (And, in more recent/future revisions, the Itanium C++ ABI will also look at whether the type has __attribute__((trivial_abi)), to decide where it's passed.)

Nicol, what in your view is the relationship between Niall's attribute proposal and __attribute__((trivial_abi))?  Would you describe Niall's proposal as just an ISO-adoption of the Itanium-status-quo, or do you see other important components as well in Niall's current proposal?

Now, I haven't read the entire thread from the link you provided, but it seems to be that [[trivial_abi]] has the substitutability of one type for another as a main feature. That if you have a function that returns a `T*`, you could turn it into an `observer_ptr<T>` without the ABI of the function being considered different. That seems a primary goal of the feature.

[[move_relocatable]] doesn't care about that.

[[trivial_abi]] says that a move+destroy operation can be converted into a memcpy+drop (or even less), but it doesn't require it in all cases. [[move_relocatable]] requires that move operations of any kind be implemented as memcpy+memcpy, with the expectation that, if the destroy happens soon thereafter, the compiler will optimize it to memcpy+drop.

So both have aspects of complexity that are not available in the other, but they do have some overlap.

That being said, I think the substitutability part is not something that can reasonably be standardized. It's ABI stuff; it's not something that's a valid part of the C++ abstract machine. And indeed, I don't think it would be good if these two functions were considered the same declaration:

void func(observer_ptr<T> pT);
void func(T *pT);

But that is the effective equivalent of what some in that thread want [[trivial_abi]] to say. I think.

Another difference is that [[trivial_abi]] only really matters when the type is used in function signatures. By contrast, [[move_relocatable]] affects the nature of the type in all situations, which includes the prospective lightweight exception mechanism.

Now granted, [[move_relocatable]] is in an actual proposal with a description of how it works with the C++ object model. While [[trivial_abi]] is defined by compilers and some discussion threads. So it's hard to really compare the two without a more formal proposal for the latter.

I think they are working in the same area, but they're also working in different areas. Even so, I'd really hate it if we have a lot of types that use both attributes; it seems to me that one could be a subset of the other. [[move_relocatable]] could handle the object model stuff, while [[trivial_abi]] is a strict subset that imposes other ABI-related requirements.

[...]
I think that both proposals are necessary. Solving the problem of allowing more objects in registers is important. But solving the problem of standard library inefficiencies with regard to movable types is also important. And while the solutions are related (if a type can fit in registers, it certainly can do the library relocation thing), they're ultimately different.

I do not believe that "if a type can fit in registers, it certainly can do the library relocation thing."  For example, its move-constructor (by itself) might have side effects that the user is not willing to discard.

Then you can't declare it [[move_relocatable]]. Or [[trivial_abi]] for that matter.

The idea with [[move_relocatable]] is not that it represents all of the objects that could potentially be stuck inside registers at ABI boundaries. They represent a very specific class of them: those where move+destruct is equivalent to memcpy+drop, and therefore it's OK to not call move constructors and destructors even if the specification allows it.

If I have a copy constructor that has side effects that I don't want to discard, then I can't make the type TriviallyCopyable. Or [[trivial_abi]], for that matter, since it won't always call your copy/move constructors even when the standard says that it would. The same holds true here: if you can't live without the side effects of a move constructor, then your type cannot be [[move_relocatable]].

The only difference is that to get TrivialCopyability, you have to not write those constructors. Here, you write constructors and destructors that don't get called.

  Here is a compilable example:
Notice that the parameter is passed in %rdi and the result is returned in %rax.
Notice that there are no load/store operations happening. We never touch memory.
Notice that the compiler quietly eliminates the dead store and puts() from the destructor.
Finally, very importantly, notice that the compiler correctly preserves the side-effecting puts() in our type's non-trivial move-constructor.

What happens if you put the elements of that code in different translation units?

Arthur O'Dwyer

unread,
Apr 17, 2018, 10:41:52 PM4/17/18
to ISO C++ Standard - Future Proposals
On Tue, Apr 17, 2018 at 6:52 PM, Nicol Bolas <jmck...@gmail.com> wrote:
On Tuesday, April 17, 2018 at 5:01:46 PM UTC-4, Arthur O'Dwyer wrote:
On Tue, Apr 17, 2018 at 1:05 PM, Nicol Bolas <jmck...@gmail.com> wrote:
In Niall's proposal, the attribute is not just about "trivial relocatability" as your proposal defines it. The attribute allows for more than just memcpy+drop (indeed technically, it doesn't even allow for that). The use of the attribute requires that the compiler will never call the move constructor. Anytime move construction happens, the compiler will replace the constructor call with a pair of `memcpy`-equivalent operations. Coupled with the statement that calling the destructor on a default-constructed value is a no-op, this ensures that user code is never involved in move+destroy operations.

It is physically possible for a type to be "trivially relocatable" (that is, move+destroy be equivalent to memcpy+drop) without having the property "destroying a default-constructed object is a no-op."  I admit I'm not sure how likely that is in idiomatic C++ code.
A simple example is nn_unique_ptr<T>, which is trivially relocatable but is not default-constructible at all. (There is a sense in which this type has a "default-constructed empty state", but that state is not literally produced by the default constructor.)
The other examples I can think of off the top of my head are fairly contrived.

Such a type may be relocatable for library purposes, but it is not one for which you can apply the [[move_relocatable]] attribute.

Right. Niall's attribute doesn't work for nn_unique_ptr<T>, but...

That being said, I think Niall's definition for this promise is overly restrictive. There's no reason why an `nn_unique_ptr` type should be forbidden from being [[move_relocatable]]. A different set of promises can allow it to be move-relocatable.

Right.

My promise list would be:

1. The type shall have a publicly-accessible move constructor.
2. The type shall have a publicly-accessible destructor.
3. The writer of the type warrants that the move constructor gives the newly constructed object a value equivalent to performing a `memcpy` from the original state of the moved-from object.
4. The writer of the type warrants that the move constructor puts the moved-from object in a state such that calling the destructor on it has no side effects and does nothing.

My promise list, as to-be-presented on May 8, is even simpler:
1. The type shall have an accessible move constructor. (Not necessarily public, but that's a super nitpick.)
2. The type shall have an accessible destructor.
3. The writer of the type warrants that the move constructor followed by the destructor (of the source) produces results semantically equivalent to "memcpy+drop".

I don't bother to add silly restrictions about the allowable state of the moved-from object in the instant between move-from and destruction.
Adding those arbitrary restrictions gains you nothing, and costs you the ability to relocate types such as std::list<T> (assuming one of those old implementations where default-constructing a std::list<T> allocates a sentinel node).


The upsides of this to me are:

* expanding the number of types that can be [[move_relocatable]]
* since the move constructor can still be called, you don't have the luxury of `= default`ing it, and thus cannot create inconsistent behavior.
* compilers don't have to have special code to turn `memcpy+memcpy+destroy" into "memcpy+drop"; they can just go straight there.

The only downside I can see is that this is identical to destructive move. And that's terrible... somehow.

All correct. :)  Except that these ideas are significant different from the original old-school "destructive move" ideas; the major point in favor of the library approach I propose is that it is not only implementable but implemented (in Qt, EASTL, BSL, etc).


ABIs have the freedom to store TriviallyCopyable types in registers [...]

The compiler can make other types live in registers too, if it wants. Here is an example of GCC placing a unique_ptr in a register.

int *p(int *q)
{
    std::unique_ptr<int> u(q);
    return u.release();
}

Objects can "live" in registers whenever the compiler feels like it, with one major exception: At cross-module call boundaries, both sides must agree on where the function parameter object is expected to live!  In practice, this means that the location of the parameter must be determined only by the properties of its type, and not by more local properties (such as the escape analysis that permitted GCC to place the local variable u in a register).
"Both sides must agree" is just another way of saying "there must be a standard calling convention."
The standard calling convention for Linux is defined by the Itanium C++ ABI. The Itanium C++ ABI defines where parameters are passed based on the properties of their types (a wise, but not inevitable, strategy). In particular, Itanium looks at the trivial copyability of the parameter type to decide where it's passed. (And, in more recent/future revisions, the Itanium C++ ABI will also look at whether the type has __attribute__((trivial_abi)), to decide where it's passed.)

Nicol, what in your view is the relationship between Niall's attribute proposal and __attribute__((trivial_abi))?  Would you describe Niall's proposal as just an ISO-adoption of the Itanium-status-quo, or do you see other important components as well in Niall's current proposal?

Now, I haven't read the entire thread from the link you provided, but it seems to be that [[trivial_abi]] has the substitutability of one type for another as a main feature. That if you have a function that returns a `T*`, you could turn it into an `observer_ptr<T>` without the ABI of the function being considered different. That seems a primary goal of the feature.

No, that's incorrect.
This is not abstract — this is literally implemented in Clang. You can go play with it on Godbolt and see what's true and what's false.


[[trivial_abi]] says that a move+destroy operation can be converted into a memcpy+drop (or even less), but it doesn't require it in all cases.

That's incorrect. Click through to the Godbolt example from my previous message:
Having done so:
Notice that the parameter is passed in %rdi and the result is returned in %rax.
Notice that there are no load/store operations happening. We never touch memory.
Notice that the compiler quietly eliminates the dead store and puts() from the destructor.
Finally, very importantly, notice that the compiler correctly preserves the side-effecting puts() in our type's non-trivial move-constructor.


 
What happens if you put the elements of that code in different translation units?

Which "elements" are you referring to, here?  You mean what happens if the move-constructor and destructor are declared-but-not-defined, like this?
In that case, the compiler cannot inline them, so it must generate call instructions.
Notice that the first call it generates is to the function prototyped as InstrumentedUniquePtr(InstrumentedUniquePtr&& b). This means it needs to produce an rvalue reference (i.e., a memory address) for the object p. On lines 3 and 4 of the assembly, you can observe it spilling %rdi to the stack in order to give it a memory address suitable to pass to InstrumentedUniquePtr(InstrumentedUniquePtr&& b).

[[trivial_abi]] doesn't magically change pass-by-reference into pass-by-value. It does change inefficient-pass-by-value (by indirection) into efficient-pass-by-value (in registers).  Remember, it's an ABI option (it affects calling convention). The semantics of C++ references remain unchanged.

I think there's a lot of confusion in these threads re: the difference between the core language, the standard library, and the Itanium ABI.  I hope I'm doing a little bit to clear it up, even if it's bailing with a teaspoon.

–Arthur

P.S. By the way, if you don't have access to Godbolt (e.g. if you can't run Javascript), please let me know. I can paste assembly dumps in here if it helps. I just figure that sending Godbolt links is easier for everyone involved AFAIK, because it gives you a place to answer questions like "What happens if you change X to Y" via direct experimentation.

Niall Douglas

unread,
Apr 18, 2018, 4:31:49 AM4/18/18
to ISO C++ Standard - Future Proposals

Niall's definition takes "move+destroy", turns it into "memcpy+memcpy+destroy", which the compiler is expected to optimize into "memcpy+drop". My way simply turns "move+destroy" into "memcpy+drop" directly; if the compiler only detects "move" with no "destroy" being visible, then that's what gets called.

The upsides of this to me are:

* expanding the number of types that can be [[move_relocatable]]
* since the move constructor can still be called, you don't have the luxury of `= default`ing it, and thus cannot create inconsistent behavior.
* compilers don't have to have special code to turn `memcpy+memcpy+destroy" into "memcpy+drop"; they can just go straight there.

The only downside I can see is that this is identical to destructive move. And that's terrible... somehow.

I am now returned home from the ACCU + LLVM conferences in Bristol.

You should be aware that a good chunk of Core, Direction, Libraries and clang were there.

I straw polled all of them before coming up with my proposal in the form it has. It's "doable". It is clearly too simple and doesn't cover all the types possible. But it can be discussed, specified and implemented easily.

Nobody is saying it is anything but a stopgap measure. In particular, not a single person at the conferences could remember exactly what the problem with destructive moves was or is, only that "it's bad" for some reason. And we've got very senior folk who I talked to about this unable to remember the precise details of why it's a bad thing, Roger Orr, Alasdair Meridith, Richard Smith and so on.

So, I see that two things could have happened here. The first is that everybody is remembering the pain of discussing it last time round instead of technical reasons why it's a bad idea. If so, we should go with Arthur's design. The second possibility is that there is some showstopper problem which is so niche and weird that nobody can remember it exactly, but when someone remembers or reinvents it, suddenly destructive moves will be dead in the water. If so, we will need to go with my design.

I honestly have no idea. I did read both preceding proposals in detail and apart from being over engineered, they looked fine to me. Arthur's proposal also looks fine.

But my proposal wasn't designed by me, it was designed by exhaustively straw polling various design options on senior members of the committee whom I would assume know what they are doing. I'd personally consider my proposal a "fall back" proposal. It'll get you 80% of the way there for 20% of the effort.

Niall

Ville Voutilainen

unread,
Apr 18, 2018, 5:24:11 AM4/18/18
to ISO C++ Standard - Future Proposals
On 18 April 2018 at 11:31, Niall Douglas <nialldo...@gmail.com> wrote:
> Nobody is saying it is anything but a stopgap measure. In particular, not a
> single person at the conferences could remember exactly what the problem
> with destructive moves was or is, only that "it's bad" for some reason. And
> we've got very senior folk who I talked to about this unable to remember the
> precise details of why it's a bad thing, Roger Orr, Alasdair Meridith,
> Richard Smith and so on.

I, however, have no trouble remembering what that problem is. The problem is
that the previous proposals suggested that it would be possible to end
the lifetime
of an object with a non-trivial destructor without calling that destructor.
That breaks allocators that track lifetimes, and that breaks all sorts
of assumptions by
programmers and their programs; the usual suggested solution is an
additional opt-in trait,
but then it becomes a question what the supposedly strong motivation
for a destructive move is.
And at that point the proposal authors have backed away, saying "I'm
no longer sure I have
such a strong motivation".

Despite your proposal solving the issue with skipping a destructor and
that it doesn't require
magical trait opt-in, the motivation question still stands. The
previous proposals cited performance
improvements in std::list as motivation, and then it was claimed by
some folks that the same
improvements can be achieved without destructive move.

Nicol Bolas

unread,
Apr 18, 2018, 1:30:10 PM4/18/18
to ISO C++ Standard - Future Proposals
On Tuesday, April 17, 2018 at 10:41:52 PM UTC-4, Arthur O'Dwyer wrote:
On Tue, Apr 17, 2018 at 6:52 PM, Nicol Bolas <jmck...@gmail.com> wrote:
On Tuesday, April 17, 2018 at 5:01:46 PM UTC-4, Arthur O'Dwyer wrote:
On Tue, Apr 17, 2018 at 1:05 PM, Nicol Bolas <jmck...@gmail.com> wrote:
In Niall's proposal, the attribute is not just about "trivial relocatability" as your proposal defines it. The attribute allows for more than just memcpy+drop (indeed technically, it doesn't even allow for that). The use of the attribute requires that the compiler will never call the move constructor. Anytime move construction happens, the compiler will replace the constructor call with a pair of `memcpy`-equivalent operations. Coupled with the statement that calling the destructor on a default-constructed value is a no-op, this ensures that user code is never involved in move+destroy operations.

It is physically possible for a type to be "trivially relocatable" (that is, move+destroy be equivalent to memcpy+drop) without having the property "destroying a default-constructed object is a no-op."  I admit I'm not sure how likely that is in idiomatic C++ code.
A simple example is nn_unique_ptr<T>, which is trivially relocatable but is not default-constructible at all. (There is a sense in which this type has a "default-constructed empty state", but that state is not literally produced by the default constructor.)
The other examples I can think of off the top of my head are fairly contrived.

Such a type may be relocatable for library purposes, but it is not one for which you can apply the [[move_relocatable]] attribute.

Right. Niall's attribute doesn't work for nn_unique_ptr<T>, but...

That being said, I think Niall's definition for this promise is overly restrictive. There's no reason why an `nn_unique_ptr` type should be forbidden from being [[move_relocatable]]. A different set of promises can allow it to be move-relocatable.

Right.

My promise list would be:

1. The type shall have a publicly-accessible move constructor.
2. The type shall have a publicly-accessible destructor.
3. The writer of the type warrants that the move constructor gives the newly constructed object a value equivalent to performing a `memcpy` from the original state of the moved-from object.
4. The writer of the type warrants that the move constructor puts the moved-from object in a state such that calling the destructor on it has no side effects and does nothing.

My promise list, as to-be-presented on May 8, is even simpler:
1. The type shall have an accessible move constructor. (Not necessarily public, but that's a super nitpick.)
2. The type shall have an accessible destructor.
3. The writer of the type warrants that the move constructor followed by the destructor (of the source) produces results semantically equivalent to "memcpy+drop".

I don't bother to add silly restrictions about the allowable state of the moved-from object in the instant between move-from and destruction.
Adding those arbitrary restrictions gains you nothing, and costs you the ability to relocate types such as std::list<T> (assuming one of those old implementations where default-constructing a std::list<T> allocates a sentinel node).

Remember: we're not talking about relocation as a library feature; we're talking about move-relocatable as a language feature. I see no particular need to allow a type like `std::list` to enjoy the benefits of [[move_relocatable]]. It can enjoy the benefits of library relocation, but not of the compiler optimizations around [[move_relocatable]].

Don't conflate the two concepts; they are similar (in that types for which the language feature is appropriate are those for which the library feature is appropriate), but distinct.

As for why move-relocatable needs that specific warrant about calling the destructor for moved-from objects being trivial (having no side-effects, etc), see Ville Voutilainen's post. The sanctity of the object model on relocation seems to require that. Or at least, not without causing potential problems.

The upsides of this to me are:

* expanding the number of types that can be [[move_relocatable]]
* since the move constructor can still be called, you don't have the luxury of `= default`ing it, and thus cannot create inconsistent behavior.
* compilers don't have to have special code to turn `memcpy+memcpy+destroy" into "memcpy+drop"; they can just go straight there.

The only downside I can see is that this is identical to destructive move. And that's terrible... somehow.

All correct. :)  Except that these ideas are significant different from the original old-school "destructive move" ideas; the major point in favor of the library approach I propose is that it is not only implementable but implemented (in Qt, EASTL, BSL, etc).


ABIs have the freedom to store TriviallyCopyable types in registers [...]

The compiler can make other types live in registers too, if it wants. Here is an example of GCC placing a unique_ptr in a register.

int *p(int *q)
{
    std::unique_ptr<int> u(q);
    return u.release();
}

Objects can "live" in registers whenever the compiler feels like it, with one major exception: At cross-module call boundaries, both sides must agree on where the function parameter object is expected to live!  In practice, this means that the location of the parameter must be determined only by the properties of its type, and not by more local properties (such as the escape analysis that permitted GCC to place the local variable u in a register).
"Both sides must agree" is just another way of saying "there must be a standard calling convention."
The standard calling convention for Linux is defined by the Itanium C++ ABI. The Itanium C++ ABI defines where parameters are passed based on the properties of their types (a wise, but not inevitable, strategy). In particular, Itanium looks at the trivial copyability of the parameter type to decide where it's passed. (And, in more recent/future revisions, the Itanium C++ ABI will also look at whether the type has __attribute__((trivial_abi)), to decide where it's passed.)

Nicol, what in your view is the relationship between Niall's attribute proposal and __attribute__((trivial_abi))?  Would you describe Niall's proposal as just an ISO-adoption of the Itanium-status-quo, or do you see other important components as well in Niall's current proposal?

Now, I haven't read the entire thread from the link you provided, but it seems to be that [[trivial_abi]] has the substitutability of one type for another as a main feature. That if you have a function that returns a `T*`, you could turn it into an `observer_ptr<T>` without the ABI of the function being considered different. That seems a primary goal of the feature.

No, that's incorrect.

And yet, that is literally what the first and second posts in that thread are about: changing an API function that used a pointer to use a smart pointer without breaking ABI compatibility.

Can you link directly to something that lays out what the behavior of types with this attribute will be?

This is not abstract — this is literally implemented in Clang. You can go play with it on Godbolt and see what's true and what's false.

Playing with it in a compiler says only what the compiler does, not the specification behind it. Say what you will about [[move_relocatable]], but there's an actual proposal behind it, where we can discuss what the spec wording would look like and how it interacts with the object model.

[[trivial_abi]] may be implemented, but I have difficulty divining from that thread or from the compiler what it actually means for the abstract machine and object model. Without that knowledge, it's hard to compare the two. Can you point to at least a semi-formal sketch of what [[trivial_abi]] means?

Thiago Macieira

unread,
Apr 18, 2018, 2:13:23 PM4/18/18
to std-pr...@isocpp.org
On Tuesday, 17 April 2018 14:01:43 PDT Arthur O'Dwyer wrote:
> It is physically possible for a type to be "trivially relocatable" (that
> is, move+destroy be equivalent to memcpy+drop) *without* having the
> property "destroying a default-constructed object is a no-op." I admit I'm
> not sure how *likely* that is in idiomatic C++ code.

Reference-counted types that share a global, default instance.

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



Nicol Bolas

unread,
Apr 18, 2018, 3:11:41 PM4/18/18
to ISO C++ Standard - Future Proposals
On Wednesday, April 18, 2018 at 5:24:11 AM UTC-4, Ville Voutilainen wrote:
On 18 April 2018 at 11:31, Niall Douglas <nialldo...@gmail.com> wrote:
> Nobody is saying it is anything but a stopgap measure. In particular, not a
> single person at the conferences could remember exactly what the problem
> with destructive moves was or is, only that "it's bad" for some reason. And
> we've got very senior folk who I talked to about this unable to remember the
> precise details of why it's a bad thing, Roger Orr, Alasdair Meridith,
> Richard Smith and so on.

I, however, have no trouble remembering what that problem is. The problem is
that the previous proposals suggested that it would be possible to end
the lifetime
of an object with a non-trivial destructor without calling that destructor.

If that's the problem, then all of these proposals, Niall's and Arthur's, have the same problem. Since that "problem" is literally the reason why all of them exist ;)

That breaks allocators that track lifetimes,

When you say "allocators", what exactly do you mean here? Do you mean `allocator::destruct`, or do you mean the destructor tracking lifetimes?

If you're talking about the `allocator` case, where a user-provided Allocator uses its `destruct` function to know when a lifetime has ended, then that's not a problem in these cases. In both Niall's and Arthur's proposals, library-based relocation is only valid if the allocator does not provide `construct` or `destruct`. Well, Arthur's proposal is a bit more complex, but the general gist of it is that your Allocator has to agree to allow it. So if your Allocator tracks lifetimes, you simply don't agree to the optimization ;)

If you're talking about code in the destructor tracking lifetimes, that may not be a problem. In both proposals, library-based relocation is opt-in. You declare that your type has this property. So if your destructors track lifetimes, then you don't declare this property and nobody can force it upon you. Granted, if you don't know if your subobjects' destructors track lifetimes, then it may indeed be a problem. But that is ultimately the responsibility of the person putting the larger type together.

and that breaks all sorts
of assumptions by
programmers and their programs; the usual suggested solution is an
additional opt-in trait,
but then it becomes a question what the supposedly strong motivation
for a destructive move is.
And at that point the proposal authors have backed away, saying "I'm
no longer sure I have
such a strong motivation".

Despite your proposal solving the issue with skipping a destructor and
that it doesn't require
magical trait opt-in, the motivation question still stands.

The prime motivation for Niall's proposal is not really stated in the proposal itself (and it kind of needs to be), but it's been tossed around in commentary on the SG14 mailing list.

See, you can't use `unique_ptr` or `shared_ptr`-like types in places like lightweight exceptions, but we really need the ability to do so if we're going to make lightweight exceptions work with a reasonable set of types (specifically, `exception_ptr`). This requires that the type agree to live within certain restrictions, with the attribute being assigned to types that agree to do so.

Lightweight exceptions need to be able to work without allocating storage (note: the following is my limited understanding of the issue. It may be wildly incorrect). That means that they either need to be able to live within registers or they live on the stack and are moved around with each unwinding. But the thing is, lightweight exceptions are intended to behave like regular exceptions. And regular exception objects don't get moved around. So lightweight exception typoes need to be able to pretend that they aren't being moved around.

If the ABI for the exception puts them in registers, then the type needs to be coded such that it's OK to copy the data from a live object into registers and from the registers into a live object. Without any user-code being involved in that operation (since we're pretending). And that's only OK if you can memcpy the bytes of the object and trivially destroy the moved-from object.

If the ABI for the exception puts it on the stack, then it will have to be moved around the stack. But since we're pretending that we're not moving it around, we have to move it in a way that user-code isn't involved. For trivially destructible types, that's fine, but why should a `unique_ptr` not be allowed? The many move constructor and destructor calls will not in any way affect what the catching code gets.

Without some language-supported mechanism to allow types with non-trivial destructors to declare that they can be destructed trivially in moved-from circumstances, lightweight exceptions would be limited to only TriviallyCopyable types. Which, according to Niall, has been indicated by some committee members to be an unacceptable limitation on the feature.

Niall's proposal mainly exist to overcome this limitation, to allow `std::exception_ptr` and similar types to work with lightweight exceptions. The rest of the proposal is simply an acknowledgement of the fact that, now that we have to have a language feature that acknowledges the idea of relocation, we may as well have some way for users to speed up container code for such types. As well as a recognition that library optimization would be permissible in a broader array of circumstances.
 

Niall Douglas

unread,
Apr 18, 2018, 6:08:42 PM4/18/18
to ISO C++ Standard - Future Proposals
> we've got very senior folk who I talked to about this unable to remember the
> precise details of why it's a bad thing, Roger Orr, Alasdair Meridith,
> Richard Smith and so on.

I, however, have no trouble remembering what that problem is. The problem is
that the previous proposals suggested that it would be possible to end
the lifetime
of an object with a non-trivial destructor without calling that destructor.

They did raise this issue, and hence why in my proposal we don't mess with move semantics. The destructor is still called on the moved-from object, same as now.
 
That breaks allocators that track lifetimes, and that breaks all sorts
of assumptions by
programmers and their programs;

As a language only proposal which does not modify the lifetime model at all, this sort of code is unaffected by my proposal.
 
the usual suggested solution is an
additional opt-in trait,
but then it becomes a question what the supposedly strong motivation
for a destructive move is.
And at that point the proposal authors have backed away, saying "I'm
no longer sure I have
such a strong motivation".

The motivation for my paper is that at least one of the senior committee leadership will not greenlight Herb's deterministic exceptions proposals unless his proposed std::error object can transport a std::exception_ptr within itself. My current reference std::error object stuffs the exception_ptr into a threadsafe global list, and tracks it by handle in order to keep my current reference std::error object trivially copyable. We need additional language support to make std::exception_ptr register storable, then it can be stored directly within the std::error object.

The proposed std::error object must be register storable, otherwise it cannot be packed into the stack frame during unwinds. That would substantially affect the gains of Herb's deterministic exceptions proposal. We are looking for single digit CPU cycles between a new throw and a new catch, not even ten CPU cycles. If you're going to disrupt C++ exceptions, might as well get it perfect.
 

Despite your proposal solving the issue with skipping a destructor and
that it doesn't require
magical trait opt-in, the motivation question still stands.

Hopefully things are clearer to you now.

In the next draft of my proposal paper, I will be dropping any mention of library code. It'll be a pure language proposal only. Arthur's proposal can fill in the library stuff.

Niall
 

Niall Douglas

unread,
Apr 18, 2018, 6:16:01 PM4/18/18
to ISO C++ Standard - Future Proposals
Thanks for your detailed reply, I just literally posted a duplicate. Sorry. I am very, very tired after the ACCU + LLVM conferences, I didn't see it.
 
The prime motivation for Niall's proposal is not really stated in the proposal itself (and it kind of needs to be), but it's been tossed around in commentary on the SG14 mailing list.

I had been thinking the following on that:
  1. The proposal should not depend on Herb's proposal. In fact, it's the other way round.
  2. The proposal should not mention Herb's proposal, as otherwise WG21 will end up discussing Herb's proposal, not this proposal.
  3. Herb's proposal may not be ready for Rapperswil, but seeing as this may be my one and only WG21 meeting if all my proposal papers get rejected, I'd like to submit my proposal independent of Herb's.
Is this wise in your opinion, or am I being too risk adverse?

Niall

Arthur O'Dwyer

unread,
Apr 18, 2018, 10:41:33 PM4/18/18
to ISO C++ Standard - Future Proposals
On Wed, Apr 18, 2018 at 3:08 PM, Niall Douglas <nialldo...@gmail.com> wrote:
> we've got very senior folk who I talked to about this unable to remember the
> precise details of why it's a bad thing, Roger Orr, Alasdair Meridith,
> Richard Smith and so on.

I, however, have no trouble remembering what that problem is. The problem is
that the previous proposals suggested that it would be possible to end
the lifetime
of an object with a non-trivial destructor without calling that destructor.

They did raise this issue, and hence why in my proposal we don't mess with move semantics. The destructor is still called on the moved-from object, same as now.
 
That breaks allocators that track lifetimes, and that breaks all sorts
of assumptions by
programmers and their programs;

As a language only proposal which does not modify the lifetime model at all, this sort of code is unaffected by my proposal.
 
the usual suggested solution is an
additional opt-in trait,
but then it becomes a question what the supposedly strong motivation
for a destructive move is.
And at that point the proposal authors have backed away, saying "I'm
no longer sure I have
such a strong motivation".

The motivation for my paper is that at least one of the senior committee leadership will not greenlight Herb's deterministic exceptions proposals unless his proposed std::error object can transport a std::exception_ptr within itself. My current reference std::error object stuffs the exception_ptr into a threadsafe global list, and tracks it by handle in order to keep my current reference std::error object trivially copyable. We need additional language support to make std::exception_ptr register storable, then it can be stored directly within the std::error object.

When you say "register storable", what do you mean exactly?
Please first read my response to Nicol where I show (via Godbolt assembly dump) that many non-trivial types, such as std::unique_ptr, can in fact be stored in registers, and compilers regularly do store them in registers, even without any special annotations.


If by "register storable" you mean [[trivial_abi]], then you do not need any ISO proposal; vendors are free to make std::exception_ptr have the [[trivial_abi]] attribute right now, today, as a conforming (but ABI-breaking) extension.  Currently it is disallowed for the end-user to wrap a std::exception_ptr in a [[trivial_abi]] struct wrapper, but I don't think that's completely impossible for LLVM to implement; they just haven't gotten around to implementing it yet.

Bonus: Here is a user-space implementation of "wrapping std::exception_ptr in a [[trivial_abi]] wrapper" so that it is passed in %rdi and returned in %rax, instead of on the stack.  Unfortunately, it appears that at the moment Clang is not capable of inlining the call to exception_ptr's destructor — the function body ends up spilling %rdi to the stack and issuing an actual callq to the destructor, before returning the result in %rax.


Now, the next problem you're going to have, when trying to implement Herb's "lightweight EH" proposal, is that Herb wants it to be "lighter than [[trivial_abi]]."

In particular, we intend for this code...

    double foo(int x) throw {
        if (x) throw std::error(42);
        return 3.14;
    }

...to be lowered by the compiler into something morally equivalent to this...

    std::expected<double, std::error> foo(int x) noexcept {
        if (x) return make_unexpected(42);
        return 3.14;
    }

...which means that we don't just need std::error to fit in a register — we need std::expected<double, std::error> to fit in a register!  (That is, we need the ABI to understand a new way of returning this extra channel, that is not equivalent to any existing C++ construct.)  We might expect that the calling convention for `double() throw` functions would return data in both %xmm0 and %rax.  We might have to make up a completely new calling convention for `throw`-colored functions whose return value already occupies both %rax and %rdx.

On top of that, there had been some talk of a calling convention in which some flag bit (e.g. the overflow bit) was used to communicate back to the caller whether the exceptional path should be taken.  That is, we would expect `foo` above to be codegenned into something like...

    _foo:
      movsd .L3p14, %xmm0
      testq %rdi, %rdi
      setnz %al
      cmpb %al, -127
      movl $42, %eax
      ret
    _caller_of_foo:
      call foo
      jo .L1
      # handle the double value in %xmm0
    .L1:
      # handle the error value in %rax

The calling-convention problems here are tough IMHO, and unfortunately more in the wheelhouse of individual compiler vendors/projects than in WG21's wheelhouse.
I'm not sure there is any problem from WG21's point of view. We already have plenty of tools to deal with the easy bits, like "can we store non-trivial objects in registers" (the answer is "yes of course").

Maybe some of this is FUD, but I'm trying to look and see where the end of this line of proposals actually is. So far it seems like you're focused on getting over a hurdle that may not even end up being part of the racecourse at all.

–Arthur

(I'll be on vacation starting tomorrow, so expect less from me for a while. Clearly I owe a blog post on trivial_abi.)

Nicol Bolas

unread,
Apr 18, 2018, 11:39:44 PM4/18/18
to ISO C++ Standard - Future Proposals

Here's the thing. Move-Relocation as a language feature doesn't offer much performance improvement. Oh sure, you may get some memcpy+drop performance improvements by returning objects with complex destructors by value. But considering that both your and my versions of the idea rely on the move constructor being `noexcept` (and thus exempting `std::list` and other types with potentially throwing moves), the most expensive relocatable objects would not be able to use the [[move_relocatable]] language feature. They could only use relocation as a library operation.

By contrast, library-based relocation has plenty to offer in terms of performance. And Arthur's pure-library ideas cover those bases very well.

The thing you have to provide motivation for is not relocation as a concept, but move-relocation as a language feature. And I'd say the most compelling motivation for any language feature is to point to something important that you flat-out cannot do or is really inconvenient which your language feature solves. Rvalue references allow you to distinguish between "reference I can steal resources from" and "reference I can't steal from". `operator<=>` takes something really inconvenient and makes it trivial. Concepts allow us to stop using Byzantine `std::enable_if/void_t` gymnastics. Coroutines allows us to write asynchronous code that looks synchronous. And so on.

Few are the language features whose only motivation is "allows compiler optimizations". Oh sure, there have been some features of that sort. But they're few and far between, and the optimizations they allow are really significant.

So what you're looking for are places in the standard that are restricted to TriviallyCopyable types, but could be expanded to include MoveRelocatable types. I don't think `std::atomic<T>` would apply; its restrictions to TriviallyCopyable types don't really have to do with move construction+destruct cycles. And while you might be able to justify allowing MoveRelocatable types in `basic_string`, I don't think anyone would actually care.

Thus far, I cannot think of any such place in the standard at the present time. Herb's proposal is one of the few language features that actually restricts the acceptable types to TriviallyCopyable ones.

Nicol Bolas

unread,
Apr 19, 2018, 12:31:06 AM4/19/18
to ISO C++ Standard - Future Proposals
On Wednesday, April 18, 2018 at 6:08:42 PM UTC-4, Niall Douglas wrote:
> we've got very senior folk who I talked to about this unable to remember the
> precise details of why it's a bad thing, Roger Orr, Alasdair Meridith,
> Richard Smith and so on.

I, however, have no trouble remembering what that problem is. The problem is
that the previous proposals suggested that it would be possible to end
the lifetime
of an object with a non-trivial destructor without calling that destructor.

They did raise this issue, and hence why in my proposal we don't mess with move semantics. The destructor is still called on the moved-from object, same as now.

[basic.life]/1.3 says that types with non-trivial destructors end their lifetimes when "the destructor call starts". Not when "you write code that invokes the destructor". The lifetime of an object is a runtime property, as is the starting of the destructor's execution. The one is tied to the other.

Your proposal says:

> Note that by ‘as-if’, we mean that the compiler can fully optimise the sequence, including the elision of calling the destructor if the destructor would do nothing when supplied with a default constructed instance, which in turn would elide entirely the second memory copy.

So the compiler is allowed to not execute the destructor, regardless of what is written in code. Which means the destructor never starts. Which means that the object's lifetime is not ended at the same time it would have been if the destructor had actually been started.

I don't think there's any way to write spec language for your proposal without changing [basic.life]/1.

Niall Douglas

unread,
Apr 19, 2018, 4:38:33 AM4/19/18
to ISO C++ Standard - Future Proposals

Here's the thing. Move-Relocation as a language feature doesn't offer much performance improvement.

Untrue. Code which passes a lot of std::shared_ptr and std::exception_ptr around by value would see big gains. Far more important is the aggregate effect of writing your code to always use trivially copyable or relocatable types.
 
The thing you have to provide motivation for is not relocation as a concept, but move-relocation as a language feature. And I'd say the most compelling motivation for any language feature is to point to something important that you flat-out cannot do or is really inconvenient which your language feature solves. Rvalue references allow you to distinguish between "reference I can steal resources from" and "reference I can't steal from". `operator<=>` takes something really inconvenient and makes it trivial. Concepts allow us to stop using Byzantine `std::enable_if/void_t` gymnastics. Coroutines allows us to write asynchronous code that looks synchronous. And so on.

Few are the language features whose only motivation is "allows compiler optimizations". Oh sure, there have been some features of that sort. But they're few and far between, and the optimizations they allow are really significant.

So what you're looking for are places in the standard that are restricted to TriviallyCopyable types, but could be expanded to include MoveRelocatable types. I don't think `std::atomic<T>` would apply; its restrictions to TriviallyCopyable types don't really have to do with move construction+destruct cycles. And while you might be able to justify allowing MoveRelocatable types in `basic_string`, I don't think anyone would actually care.

Thus far, I cannot think of any such place in the standard at the present time. Herb's proposal is one of the few language features that actually restricts the acceptable types to TriviallyCopyable ones.

I appreciate that I'm about to go into SG14 speak, but relocation support in the language is imperative to retaining C++ as a useful language for bare metal applications.

Already one of the key disciplines in the freestanding/low latency/bare metal/kernel crowd is to never use types which are not trivially copyable. You see wonderful cumulative gains in predictability and efficiency across the board if you only ever use trivially copyable i.e. C types. It is from this position is why you see Linux kernel folk talk about using C++ to compile the Linux kernel, but destructors and constructors are banned. They're really saying "you can use C++, but only with C types".

I, and others, would like to expand their horizons a bit, give them the freedom to safely use destructors. For that we need to retain trivial copies, that's by far the most important part. Destructors which fire at the end of a long sequence of maybe-copies can be acceptable. Destructors which fire every potential copy are anathema.

Herb's problem merely brought this issue up sooner than it would have otherwise. I was going to propose this next year in fact, after I had the low level file i/o library on track, because it is written to be freestanding compatible and all its types, including all the handle classes, are trivially copyable or at worst relocatable. It was a key design decision, one which the Filesystem and kernel crowd who have examined AFIO greatly applaud. This is "their kind of C++", the kind of C++ acceptable to kernel folk. The kind of low latency C++ which SG14 was formed to advance.

So that's the philosophical, as well as technical rationales. I appreciate that you will still feel that relocation offers little performance gain. At the micro level, I agree. In aggregate, I believe not, there are profound gains available when coming from a whole program perspective because the compiler is given the freedom to assume that copies and relocates have no side effects.

Niall
 

Niall Douglas

unread,
Apr 19, 2018, 6:09:30 PM4/19/18
to ISO C++ Standard - Future Proposals
Ok, draft 4 of this paper is attached. Changes:
  • No longer mention anything library. That's Arthur's paper.
  • Move constructor must always be defined now.
  • Base classes and member variables must also be tagged with [[move_relocates]
  • Even more simplification and removal of unclear language.
  • Expanded the section comparing before and after assembler codegen in a probably vain attempt to persuade the committee of the performance gains available.
Feedback welcome!

Niall
DEEEER0 SG14 move_relocates draft 4.pdf

inkwizyt...@gmail.com

unread,
Apr 19, 2018, 7:48:06 PM4/19/18
to ISO C++ Standard - Future Proposals


On Thursday, April 19, 2018 at 12:08:42 AM UTC+2, Niall Douglas wrote:

The motivation for my paper is that at least one of the senior committee leadership will not greenlight Herb's deterministic exceptions proposals unless his proposed std::error object can transport a std::exception_ptr within itself.

Where I could find this proposal? Fast googling do not show anything and last mailing list do not had it.

Niall Douglas

unread,
Apr 20, 2018, 4:13:17 AM4/20/18
to ISO C++ Standard - Future Proposals

The motivation for my paper is that at least one of the senior committee leadership will not greenlight Herb's deterministic exceptions proposals unless his proposed std::error object can transport a std::exception_ptr within itself.

Where I could find this proposal? Fast googling do not show anything and last mailing list do not had it.

SG14 members only so far. It may appear at Rapperswil, it may not.

Niall 

John McFarlane

unread,
Apr 20, 2018, 7:32:00 AM4/20/18
to std-pr...@isocpp.org
Curious: does this mean that I am not an SG14 member?

Thanks
John

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.

Michał Dominiak

unread,
Apr 20, 2018, 7:35:23 AM4/20/18
to std-pr...@isocpp.org
Does this mean that SG14 suddenly has some special visibility rules for its proposals? If so, then it's extremely unhelpful, especially if we start having proposals outside of it, motivated by proposals *inside*.

Ville Voutilainen

unread,
Apr 20, 2018, 7:43:28 AM4/20/18
to ISO C++ Standard - Future Proposals
On 20 April 2018 at 14:35, Michał Dominiak <gri...@griwes.info> wrote:
> Does this mean that SG14 suddenly has some special visibility rules for its
> proposals? If so, then it's extremely unhelpful, especially if we start
> having proposals outside of it, motivated by proposals *inside*.


I am under the impression that the proposal just hasn't been published
yet, but SG14 were
shown a sneak peek for a teleconference. It is indeed unfortunate to
try to discuss the paper
without having seen recent revisions of it, but I don't think there's
any special rules going on.

inkwizyt...@gmail.com

unread,
Apr 20, 2018, 8:39:51 AM4/20/18
to ISO C++ Standard - Future Proposals
Ok, I understood.
BTW on what stage was this preview? Around first draft or something more advanced?

John McFarlane

unread,
Apr 20, 2018, 8:53:13 AM4/20/18
to std-pr...@isocpp.org
Not everyone on the forum was present at the meeting or in the telecon. I don't recall a paper being discussed on the forum which SG14 members were not all able to read before.

I would at least like to get confirmation that the paper number is D0709R12 (Ben mentioned this number in a thread but I didn't see anything in the minutes) and for people to use the paper number when discussing it. That way, at least I know it's a conversion to which I cannot contribute an informed opinion.

--
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,
Apr 20, 2018, 12:10:46 PM4/20/18
to ISO C++ Standard - Future Proposals

SG14 members only so far. It may appear at Rapperswil, it may not.

Curious: does this mean that I am not an SG14 member?

It more means that Herb forgot to CC you, nothing more. He did ask if he'd remembered everybody, and I think we can all hang our heads in shame that we did not think of you. So it's our fault really. Sorry.

Niall
 

Niall Douglas

unread,
Apr 20, 2018, 12:13:04 PM4/20/18
to ISO C++ Standard - Future Proposals
Does this mean that SG14 suddenly has some special visibility rules for its proposals? If so, then it's extremely unhelpful, especially if we start having proposals outside of it, motivated by proposals *inside*.

I would hope that all my proposals stand on their own merit. Some of them have been brought into paper form sooner than planned due to Herb's proposal, but they were all coming in any case and are entirely independent of Herb's proposal.

If Herb's proposal fails, I have a backup, less ambitious, near substitute ready to go.

So tl;dr; nobody needs to see Herb's proposal to understand mine, and if you feel you do, that's a bug in my papers. Bug reports welcomed.

Niall 

Niall Douglas

unread,
Apr 20, 2018, 12:15:14 PM4/20/18
to ISO C++ Standard - Future Proposals
I would at least like to get confirmation that the paper number is D0709R12 (Ben mentioned this number in a thread but I didn't see anything in the minutes) and for people to use the paper number when discussing it. That way, at least I know it's a conversion to which I cannot contribute an informed opinion.


I think the draft number is a bit higher now.

Indicate on the SG14 reflector that you were left out, and please can you have a copy. I don't doubt you'll receive one.

Niall 

John McFarlane

unread,
Apr 20, 2018, 6:57:15 PM4/20/18
to std-pr...@isocpp.org
I don't think he did. Only attendees of the meeting were sent the paper.

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.

olee...@gmail.com

unread,
Apr 21, 2018, 8:38:15 AM4/21/18
to ISO C++ Standard - Future Proposals
The default constructor is an implementation detail, called from within the compiler generated move constructor. Therefore, a private default constructor should be fine.
Also, the move constructor clearly doesn't need to be public, just accessible at the call site.

From the draft 4 paper, section 3.4:
It is considered good practice that the move constructor be implemented to cause the exact
same effects as [[move_relocates]] i.e. copying the bits of source to destination followed
by copying the bits of a default constructed instance to source.
This suggests that it might be a good idea to memcpy the whole object yourself in the fallback implementation. But this could break horribly with changes to the members or base classes. And you might not even get a warning. Rather, this should just be a normal, correct move constructor.

But the important thing the paper and this discussion has missed as far as I can tell, is that we really want the compiler to infer this property automatically when there is nothing to prevent it, similar to a type being trivially copyable. Consider a simple struct like this:
struct Foo
{   unique_ptr<Bar> p;
   
int i;
};
Requiring users to put [[move_relocates]] manually on every type like this to get the optimization would be very unfortunate.
Message has been deleted
Message has been deleted
Message has been deleted

olee...@gmail.com

unread,
Apr 21, 2018, 9:27:58 AM4/21/18
to ISO C++ Standard - Future Proposals
On Tuesday, April 17, 2018 at 10:26:33 AM UTC+2, Avi Kivity wrote:



On 2018-04-17 10:56, Alberto Barbati wrote:
BTW, since all this is about the move constructor, wouldn't it be better to put the attribute on the move constructor itself? For example:

  type& operator(type&&) [[can_relocate]] { /* definition in case the compiler doesn't relocate */ }

if the condition to apply relocation are met (these conditions includes all considerations about the other constructors), the body of the move constructor is disregarded, the move is performed as-if by memcpy and the move source is not destroyed. If the conditions for relocation are not met or if the compiler decides to ignore the attribute, a valid implemenation of the move constructor is still available and can be used to provide the correct observable behaviour.



Why not tell the compiler to relocate? With a context keyword.

 type(type&&) = relocate;  // instead of "= default"

I must say, I like this idea the most, since it's consistent with how you currently ask the compiler to generate a special member, and it would be implicitly noexcept.

But if an attribute would be easier to get into the standard, I think putting it on the move constructor is the way to go. It would be easier to see if the implementation agrees with the attribute. Furthermore, if you don't want to bother with a fallback and just rely on the compiler, you could write something like:

type(type &&) noexcept [[bitwise_relocate]];
This should still have fairly clear meaning, and you will get a linker error if the attribute is ignored.
Reply all
Reply to author
Forward
0 new messages