Destructive move by base class destructor

410 views
Skip to first unread message

David Krauss

unread,
Aug 1, 2015, 11:28:31 PM8/1/15
to ISO C++ Standard - Future Proposals, Pablo Halpern, Ville Voutilainen
Following up on the thread “movable class property,” here’s the beginning of a proposal to use base class destructors as an alternative to N4393’s __COOKIE__.

Advantages:

— No new syntax.
— Does not treat non-POD members as POD.
— Handles non-POD members using their actual constructors and destructors.
— Does still effectively treat a non-POD derived class as POD, given a suitable POD base.
— Fully generic implementation of uninitialized_destructive_move.
— Thrillingly named Sliced destructionPDF.

denis bider

unread,
Aug 2, 2015, 1:05:44 AM8/2/15
to ISO C++ Standard - Future Proposals, phal...@halpernwightsoftware.com, ville.vo...@gmail.com
The problem I see with this is that although it could be "like" memcpy, it is not in fact memcpy.

As far as I can tell, this prevents use of realloc.

Arguably, though, we could propose a new function, try_realloc, which would fail and not change or move anything, if it fails to enlarge memory in place.

A function like try_realloc would benefit all containers, including those - and especially those - that must perform deep copy on reallocation.

Possibly, then - this + try_realloc could get the job sort of done.

But I have another problem with this - which is that it seems to prevent, or make harder, introduction of the feature I really want. What I really want is a standardized rule that would allow the developer to rely on the compiler to automatically mark types is_location_agnostic or is_relocatable. In my opinion, the compiler should do the deduction, and the developer should check the result. This way, the compiler and the developer make each other stronger.

The other way around cannot be done: if we require the developer to do the deduction, the compiler can't check the result.

To me, this is the core feature for which I want language support. If I can't rely on the compiler to help me correctly mark my types, then to me, there isn't a major difference to having a standardized property, as opposed to rolling my own. In either case, I won't use it, because it's too dangerous. I don't trust myself to not introduce errors if I'm required to manually mark every type.

Thiago Macieira

unread,
Aug 2, 2015, 2:22:57 AM8/2/15
to std-pr...@isocpp.org
On Saturday 01 August 2015 22:05:43 denis bider wrote:
> As far as I can tell, this prevents use of realloc.
>
> Arguably, though, we could propose a new function, try_realloc, which would
> fail and not change or move anything, if it fails to enlarge memory in
> place.
>
> A function like try_realloc would benefit all containers, including those -
> and especially those - that must perform deep copy on reallocation.

Right, we need that function anyway, but it's not completely enough.

A realloc() function that manipulated memory mappings could keep an existing
page of allocation and simply remap it elsewhere to enlarge, with no copy
required. try_realloc + copy can't do zero copies.

> Possibly, then - this + try_realloc could get the job sort of done.
>
> But I have another problem with this - which is that it seems to
> prevent, or make harder, introduction of the feature I *really* want. What
> I *really* want is a standardized rule that would allow the developer to
> rely on the compiler to automatically mark types is_location_agnostic or
> is_relocatable. In my opinion, the compiler should do the deduction, and
> the developer should check the result. This way, the compiler and the
> developer make each other stronger.

For that, we need something that may have non-zero false negatives, but it
cannot have any false positive, for any reason. Can you guarantee that the
compiler would never think a type is relocatable when it really isn't?

> To me, this is *the* core feature for which I want language support. If I
> can't rely on the compiler to help me correctly mark my types, then to me,
> there isn't a major difference to having a standardized property, as
> opposed to rolling my own. In either case, I won't use it, because it's too
> dangerous. I don't trust myself to not introduce errors if I'm required
> to manually mark every type.

For me, it isn't. The core feature for me is the ability to memcpy or realloc
confidently. But it's an optimisation, so if I miss a type or two, the worst
case scanerio is that it's slightly slower than it could have been.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

David Krauss

unread,
Aug 2, 2015, 2:26:01 AM8/2/15
to std-pr...@isocpp.org
On 2015–08–02, at 1:05 PM, denis bider <isocp...@denisbider.com> wrote:

The problem I see with this is that although it could be "like" memcpy, it is not in fact memcpy.

If you have a trivially movable base subobject B, and no other bytes in the object, then this constructor does a memcpy and nothing else:

D::D( B && b ) : B{ std::move( b ) } {}

If you call this constructor in a loop, then nothing is stopping the compiler from detecting that it’s nothing but a sequence of memcpy calls, and merging them into one big memcpy. (I’m not checking, right now, whether such optimizations are actually done. It would be surprising if it were implemented by all C++ compilers, or none.)

Note, the compiler can use memcpy even in cases where nothing is officially trivial at all. It can do anything that behaves correctly in the end.

As far as I can tell, this prevents use of realloc.

I’d rather write a semantically correct program and let the optimizer make it fast.

But I have another problem with this - which is that it seems to prevent, or make harder, introduction of the feature I really want. What I really want is a standardized rule that would allow the developer to rely on the compiler to automatically mark types is_location_agnostic or is_relocatable. In my opinion, the compiler should do the deduction, and the developer should check the result. This way, the compiler and the developer make each other stronger.

The programmer marks pointers as relocatable or (potentially) not, then the compiler summarizes those attributes, and the programmer uses the summary to decide whether or not to call realloc? No thanks.

The other way around cannot be done: if we require the developer to do the deduction, the compiler can't check the result.

To me, this is the core feature for which I want language support. If I can't rely on the compiler to help me correctly mark my types, then to me, there isn't a major difference to having a standardized property, as opposed to rolling my own. In either case, I won't use it, because it's too dangerous. I don't trust myself to not introduce errors if I'm required to manually mark every type.

I don’t want any properties or traits. I just want a way to eliminate wasted effort where the last few actions of the move constructor are cancelled by the first few actions of the destructor.

I also don’t particularly like fiddling with object representations. It’s not a very forward-looking approach.

So, my proposal defines a bare minimum one trait, which enables safe support for all types regardless of triviality.

denis bider

unread,
Aug 2, 2015, 3:28:58 AM8/2/15
to ISO C++ Standard - Future Proposals
> The programmer marks pointers as relocatable or (potentially) not,
> then the compiler summarizes those attributes, and the
> programmer uses the summary to decide whether
> or not to call realloc? No thanks.

What? I can't imagine how you thought that could be what I meant.

No, what I meant is this:


class Str relocatable { (insert implementation) };

struct A { Str x; };

static_assert(std::is_relocatable<A>::value, "");


This is the feature I need.

If I implement my own basic types that are relocatable or location agnostic, I need the compiler to infer that my other types, which:

- derive from or aggregate my basic types, and

- have an implicit or defaulted copy constructor,

are also relocatable (or location agnostic).

That's the feature I need.

I really don't give a rat's rear end if this feature is not made available. If I cannot count on the compiler's support, then any other "support" for this means nothing to me. Zero.

David Krauss

unread,
Aug 2, 2015, 3:52:33 AM8/2/15
to std-pr...@isocpp.org
On 2015–08–02, at 3:28 PM, denis bider <isocp...@denisbider.com> wrote:

> The programmer marks pointers as relocatable or (potentially) not,
> then the compiler summarizes those attributes, and the
> programmer uses the summary to decide whether
> or not to call realloc? No thanks.

What? I can't imagine how you thought that could be what I meant.

This is the feature I need.

If I implement my own basic types that are relocatable or location agnostic,

… presumably marking them as such…

I need the compiler to infer that my other types, which:

… summarizing the attributes…

- derive from or aggregate my basic types, and

- have an implicit or defaulted copy constructor,

are also relocatable (or location agnostic).

That's the feature I need.

… then what happens? You call realloc or memcpy, right?

I really don't give a rat's rear end if this feature is not made available. If I cannot count on the compiler's support, then any other "support" for this means nothing to me. Zero.

I think this discussion is off topic for this thread. It sounds like you want something like a combination of is_trivially_move_constructible and a custom contains_self_references trait. It’s not clear what you want to do with types that don’t realloc.

denis bider

unread,
Aug 2, 2015, 4:20:12 AM8/2/15
to ISO C++ Standard - Future Proposals
> … then what happens? You call realloc or memcpy, right?

I store the various types in containers; standard or otherwise. The containers call realloc or memcpy.


>  It sounds like you want something like a combination of
> is_trivially_move_constructible and a
> custom contains_self_references trait.

What I want is the is_trivially_destructive_movable trait - by whichever of its names (is_relocatable, is_location_agnostic).

As far as I can see, they are all the same trait.

This is on topic for this thread, because your proposal attempts to fill this niche in a fairly horrendous way, which would prevent the definition of a proper trait with compiler support for inferring it.

Bo Persson

unread,
Aug 2, 2015, 4:30:24 AM8/2/15
to std-pr...@isocpp.org
On 2015-08-02 08:19, David Krauss wrote:
>
>> On 2015–08–02, at 1:05 PM, denis bider <isocp...@denisbider.com
>> <mailto:isocp...@denisbider.com>> wrote:
>>
>> The problem I see with this is that although it could be "like"
>> memcpy, it is not in fact memcpy.
>
> If you have a trivially movable base subobject B, and no other bytes in
> the object, then this constructor does a memcpy and nothing else:
>
> D::D( B && b ) : B{ std::move( b ) } {}
>
> If you call this constructor in a loop, then nothing is stopping the
> compiler from detecting that it’s nothing but a sequence of memcpy
> calls, and merging them into one big memcpy. (I’m not checking, right
> now, whether such optimizations are actually done. It would be
> surprising if it were implemented by all C++ compilers, or none.)
>
> Note, the compiler can use memcpy even in cases where nothing is
> officially trivial at all. It can do anything that behaves correctly in
> the end.
>

And in cases where the object is properly aligned and an even number of
registers in size, the compiler can even do BETTER than a call to memcpy.

Here is my favorite example (by myself :-) of a std::string copy
construcor optimized down to 4 machine instructions:

http://stackoverflow.com/a/11639305/597607


This in reply to a question about implementing swap with memcpy...

http://stackoverflow.com/questions/11638271/examples-of-when-a-bitwise-swap-is-a-bad-idea


Bo Persson



David Krauss

unread,
Aug 2, 2015, 4:33:02 AM8/2/15
to std-pr...@isocpp.org

On 2015–08–02, at 4:20 PM, denis bider <isocp...@denisbider.com> wrote:

This is on topic for this thread, because your proposal attempts to fill this niche in a fairly horrendous way, which would prevent the definition of a proper trait with compiler support for inferring it.

The binary is_trivially_destructive_movable_v would be approximated well enough by is_trivially_movable_v< destructive_move_base_t< T > > && is_trivially_destructible_v< destructive_move_base_t< T > >. No language extension needed.

If an extension discourages people from using realloc to write containers, that’s a positive aspect in my book.

David Krauss

unread,
Aug 2, 2015, 4:40:40 AM8/2/15
to std-pr...@isocpp.org

On 2015–08–02, at 4:20 PM, denis bider <isocp...@denisbider.com> wrote:

This is on topic for this thread, because your proposal attempts to fill this niche in a fairly horrendous way, which would prevent the definition of a proper trait with compiler support for inferring it.

The binary is_trivially_destructive_movable_v would be approximated well enough by is_trivially_movable_v< destructive_move_base_t< T > > && is_trivially_destructible_v< destructive_move_base_t< T > >. No language extension needed.

… by the way, the committee and compiler authors have better things to do than define “proper” builtin traits for such narrow use cases. Pinning all your hopes on highly-specific core language extensions will likely result in getting nothing.

denis bider

unread,
Aug 2, 2015, 6:01:25 AM8/2/15
to ISO C++ Standard - Future Proposals
> If an extension discourages people from using realloc to
> write containers, that’s a positive aspect in my book.

Why?

denis bider

unread,
Aug 2, 2015, 6:16:57 AM8/2/15
to ISO C++ Standard - Future Proposals

> For that, we need something that may have non-zero false
> negatives, but it cannot have any false positive, for any
> reason. Can you guarantee that the compiler would
> never think a type is relocatable when it really isn't?


Yes, I believe so.

We already have rules as to when a move constructor is implicitly declared or defined.

When a move constructor is implicitly declared, mark the type relocatable iff:

- the implicitly declared move constructor is non-deleted;

- all non-static data members and direct bases are relocatable.

If the rules for implicit declaration of a move constructor are sound, then this is also.

denis bider

unread,
Aug 2, 2015, 6:44:46 AM8/2/15
to ISO C++ Standard - Future Proposals, b...@gmb.dk
In practical observation on x64 Windows, memcpy is 2 - 9 times faster than a loop of placement new followed by destructor.

Tested VS 2015 and GCC 4.9.2.

The greatest speedup - factor of 5-9 - is with commonly used container sizes of 100 - 1000 objects.


>  nothing is stopping the compiler from detecting that
> it’s nothing but a sequence of memcpy calls

Nothing is stopping the compiler, except for that it isn't yet a fully evolved AI, and relies on humans to craft optimization rules that fail about as often as they succeed, subject to quirky conditions; and the developer has no way of knowing which is going to happen in advance.

One of the main advantages of C++ is that it does not separate the developer from the platform, and does not require trusting complex infrastructure to either optimize code, or fail to.

David Krauss

unread,
Aug 2, 2015, 8:12:01 AM8/2/15
to std-pr...@isocpp.org
On 2015–08–02, at 6:44 PM, denis bider <isocp...@denisbider.com> wrote:

In practical observation on x64 Windows, memcpy is 2 - 9 times faster than a loop of placement new followed by destructor.

“Placement new” means calling a constructor. Yes, it’s possible to write a benchmark with constructors which goes faster when they’re not called. That doesn’t make the optimization valid.

The greatest speedup - factor of 5-9 - is with commonly used container sizes of 100 - 1000 objects.


>  nothing is stopping the compiler from detecting that
> it’s nothing but a sequence of memcpy calls

Nothing is stopping the compiler, except for that it isn't yet a fully evolved AI,

Well, Clang at least does a pretty good job: http://goo.gl/x188jS (godbolt.org link)

The other compilers there aren’t too shabby either. Can’t speak for MSVC.

and relies on humans to craft optimization rules that fail about as often as they succeed, subject to quirky conditions; and the developer has no way of knowing which is going to happen in advance.

One of the main advantages of C++ is that it does not separate the developer from the platform, and does not require trusting complex infrastructure to either optimize code, or fail to.

In that case, my proposal shouldn’t interfere with the motivation for your effort to make a trait that automatically computes whether a class is compatible with realloc.

However, it sounds like the language you’re describing is C (with optimizations disabled), not C++.

Thiago Macieira

unread,
Aug 2, 2015, 1:18:40 PM8/2/15
to std-pr...@isocpp.org
On Sunday 02 August 2015 14:19:02 David Krauss wrote:
> If you have a trivially movable base subobject B, and no other bytes in the
> object, then this constructor does a memcpy and nothing else:
>
> D::D( B && b ) : B{ std::move( b ) } {}
>
> If you call this constructor in a loop, then nothing is stopping the
> compiler from detecting that it’s nothing but a sequence of memcpy calls,
> and merging them into one big memcpy. (I’m not checking, right now, whether
> such optimizations are actually done. It would be surprising if it were
> implemented by all C++ compilers, or none.)

Why should this optimisation apply only to classes with inline constructors?
Absolutely none of the compilers will optimise the following B base class:

B.h:
struct B
{
B(const B &);
B(B &&);
int i,j,k,l;
};

B.cpp:
B::B(const B &) = default;
B::B(B &&) = default;

denis bider

unread,
Aug 2, 2015, 2:58:20 PM8/2/15
to ISO C++ Standard - Future Proposals
> “Placement new” means calling a constructor.

I meant placement new that calls a move constructor.

However, you are correct: the benchmark I was referring to was calling standard, non-sliced std::string move constructor and destructor.

I tested right now instead on a POD class of similar size (24 bytes, 3 long long members), with a move constructor that just copies the members, and no destructor - mimicking your slicing proposal.

You are correct that performance in this circumstance reaches reasonable levels. At worst, memcpy is better by a factor of 2.

I therefore withdraw this part of my complaint.

But:


> Yes, it’s possible to write a benchmark with
> constructors which goes faster when
> they’re not called. That doesn’t make
> the optimization valid.

This leaves the problem that your proposal is indescribably ugly.

What you're doing is forcing all objects that want to implement relocation to split their data members into a base away from the code, and then to nominate that base for destructive moving.

You are doing this to shoehorn support for relocation, while preserving a conceptual language deficiency in place. You're doing this to work around the status quo, but while you do so, you don't even benefit from not having to change the language. You're still having to change the language, in order to allow slicing to work in the first place.

You are doing all of this, for the sole and only purpose of having this:



template <typename T, typename B =
    typename destructive_move_base_t<B> >
T* holy_blessed_relocate(void* d, T* s, size_t n) {
    T* dt { (T*) d };
    for (size_t i=0; i!=n; ++i) {
        new (dt+i) T(std::move(s[i]));
        s[i].~T();
    }
    return dt;
}



instead of this:



template <typename T, enable_if_t<is_relocatable_v<T>, int>=0>
T* holy_blessed_relocate(void* d, T* s, size_t n)
    { memcpy(d, s, n*sizeof(T)); return (T*) d; }

template <typename T, enable_if_t<!is_relocatable_v<T>, int>=0>
T* holy_blessed_relocate(void* d, T* s, size_t n) {
    T* dt { (T*) d };
    for (size_t i=0; i!=n; ++i) {
        new (dt+i) T(std::move(s[i]));
        s[i].~T();
    }
    return dt;
}



These lines of code is the whole difference your slicing approach boils down to.

In exchange for this, you are asking everyone in the world to pay for your "conceptual beauty" - such as it is - by forcing them to separate their relocatable implementations into two separate objects.

This is a violation of common sense, a humiliation for C++, and proof that the language is being run by people out of touch with the world.

Whether we call holy_blessed_relocate<T> implemented one way, or the other way, static analysis tools continue to work exactly the same. The only difference is a trivial implementation detail. And the cost you're paying for this detail is to make all the rest of the code ugly.

Ville Voutilainen

unread,
Aug 2, 2015, 3:18:14 PM8/2/15
to ISO C++ Standard - Future Proposals
On 2 August 2015 at 21:58, denis bider <isocp...@denisbider.com> wrote:
> This is a violation of common sense, a humiliation for C++, and proof that
> the language is being run by people out of touch with the world.

While you're at it, I recommend remembering that C++ is not run by the people
on this forum.

When it comes to destructive move, I want to see Pablo's follow-up, because he
and Chandler were tasked to look at whether the destructive moves the proposed
are truly necessary. If you think you have useful material for such a
follow-up, talk to him.

Thiago Macieira

unread,
Aug 2, 2015, 3:36:52 PM8/2/15
to std-pr...@isocpp.org
On Sunday 02 August 2015 11:58:20 denis bider wrote:
> This is a violation of common sense, a humiliation for C++, and proof that
> the language is being run by people out of touch with the world.

Denis, please stop attacking the people behind ideas. You can be as much frank
as you want on technical terms, but the moment that you resort to attacking
people, you lose credit.

I've been supporting your proposal so far, but if this continues I will stop
reading your emails.

David Krauss

unread,
Aug 2, 2015, 11:59:34 PM8/2/15
to std-pr...@isocpp.org
On 2015–08–03, at 2:58 AM, denis bider <isocp...@denisbider.com> wrote:

What you're doing is forcing all objects that want to implement relocation to split their data members into a base away from the code, and then to nominate that base for destructive moving.

It’s a trade-off between alternatives:

1. Enable destructive move only by a trait, only when it’s trivial. Classes that would be nontrivially destructive movable get nothing. The trait is supposed to be computed magically somehow, so classes aren’t aware of their own triviality. This seems to be your direction, because you haven’t acknowledged the possibility of nontriviality.

2. Require the user to write their own overload of uninitialized_destructive_move when it’s nontrivial. This overload needs to dance around the object lifetime model, which requires a __COOKIE__. It needs to implement the parts of the move constructor and destructor which remain necessary, which can be tricky. This is demonstrated in N4393.

3. Try to make an uninitialized_destructive_move template which works in the nontrivial case, by letting the class slice its own move constructor and destructor. If a class wants to refine its own lifetime semantics, that’s its own responsibility.

You are doing this to shoehorn support for relocation, while preserving a conceptual language deficiency in place.

The object lifetime model is not a language deficiency, it’s a cornerstone. The reason I CC’ed Ville is that he often has insightful opinions about destructors.

You're doing this to work around the status quo, but while you do so, you don't even benefit from not having to change the language. You're still having to change the language, in order to allow slicing to work in the first place.

No, slicing already works. You can already try my proposal by using the syntax derived_ptr->base::~base(). No UB happens until you create a new object at the derived_ptr address, and it only makes a difference if you’re running a sanitizer that exhaustively tracks object lifetimes (or the implementation otherwise does so, which would be quite exotic).

My proposed language change is to allow users to do the same thing, without incurring a sanitizer’s wrath. I picked a currently disallowed syntax derived_ptr->~base() to avoid breaking the very narrow use case where the user destroys a base subobject yet continues to call member functions on the derived object (which has absolutely no remaining state!).

You are doing all of this, for the sole and only purpose of having this:

T* holy_blessed_relocate(void* d, T* s, size_t n) {

No, I’m doing it to avoid requiring the user to write their own unholy_destructive_move which, as reflected in N4393 §4.1’s example of nontriviality, is likely to forget to call member destructors that may actually be necessary.

The tendency to forget nontriviality is a force to be reckoned with. I think it’s fundamentally what’s driving your opposition here. You want things to be trivial. But, destructors shouldn’t be ignored so easily.

These lines of code is the whole difference your slicing approach boils down to.

In exchange for this, you are asking everyone in the world to pay for your "conceptual beauty" - such as it is - by forcing them to separate their relocatable implementations into two separate objects.

Everyone in the world shouldn’t be implementing data structure classes. As I describe in §3.1 of my latest draft, applicable classes already tend to have separation of such concerns. “If these libraries wished to optimize their lists, it could be done without introducing new classes or breaking the library ABI.”

This is a violation of common sense, a humiliation for C++, and proof that the language is being run by people out of touch with the world.

So far, I’ve dug into some standard library implementations and worked through a disassembly example. What real-world analysis are you bringing to this discussion?

I didn’t consider composition of destructive-movable objects, though. std::pair<std::string, std::string> should be destructive movable. My draft proposal really doesn’t handle this. N4393 handles it in the trivial case, given trait specialization.

Whether we call holy_blessed_relocate<T> implemented one way, or the other way, static analysis tools continue to work exactly the same. The only difference is a trivial implementation detail. And the cost you're paying for this detail is to make all the rest of the code ugly.

Of more concern here is dynamic analysis, which might detect that a lifetime hasn’t properly ended. That’s the motivation behind __COOKIE__. If it weren’t for that, N4158 probably wouldn’t have led to N4393.

David Krauss

unread,
Aug 3, 2015, 12:21:07 AM8/3/15
to std-pr...@isocpp.org, Pablo Halpern

> On 2015–08–03, at 3:18 AM, Ville Voutilainen <ville.vo...@gmail.com> wrote:
>
> When it comes to destructive move, I want to see Pablo's follow-up, because he
> and Chandler were tasked to look at whether the destructive moves the proposed
> are truly necessary. If you think you have useful material for such a
> follow-up, talk to him.

Is N4393 not that followup?

This is why I’m asking for feedback here (and CC’ing Pablo). This proposal is not my baby, and it can be stopped in its tracks if someone points out that something critical is missing. I’d prefer not to waste effort developing something that’s doomed.

Ville Voutilainen

unread,
Aug 3, 2015, 2:40:56 AM8/3/15
to ISO C++ Standard - Future Proposals
On 3 August 2015 at 07:20, David Krauss <pot...@gmail.com> wrote:
>> On 2015–08–03, at 3:18 AM, Ville Voutilainen <ville.vo...@gmail.com> wrote:
>> When it comes to destructive move, I want to see Pablo's follow-up, because he
>> and Chandler were tasked to look at whether the destructive moves the proposed
>> are truly necessary. If you think you have useful material for such a
>> follow-up, talk to him.
> Is N4393 not that followup?

No, we looked at N4393 in Lenexa and gave the paper author some homework to do.
So that would be a follow-up subsequent to N4393.

David Rodríguez Ibeas

unread,
Aug 3, 2015, 4:56:18 AM8/3/15
to std-pr...@isocpp.org
There's a couple of things I dislike of David Krauss' proposal, while I understand that it is an attempt to make something work in a much simpler way than the alternatives in the language.

I dislike forcing the design of the types into a base with members and a derived type with functionality on purely theoretical grounds (was this the only concern I may swallow those), but in forcing a particular layout of the members and code I feel it inhibits non-trivial uses. I don't see, for example, how composibility could be obtained. In the libraries that offer relocability through traits this is easily handled in BSL:

template <typename T, typename U>
struct is_relocatable<pair<T,U>>
: integral_constant<bool, is_relocatable<T>::value 
                               && is_relocatable<U>::value>
{};

BSL: https://github.com/bloomberg/bde/blob/master/groups/bsl/bslstl/bslstl_pair.h#L1015
EASTL does not try to infer the relocatability of a generic type out of the members.
FB Folly aims to detect the trait through indirect checks on the type, but not from the members; it does not provide many generic types (at least no pair, which is what I looked for)
I have not checked Qt (not familiar with the codebase).

This works easily as memcpy over the pair is roughly equivalent to memcpy of both members (ignoring padding here, but it should be fine to ignore padding). I don't quite see how you could do the same with the proposed alternative. What would the implementation of pair need to do?

I believe that from a user's point of view (more people will develop code than the standard), the simplest approach is what current libraries do: the user that creates a type need only provide the trait, the library does the rest of the work and that work is minimal at the library level. Requiring that the movable type is split hierarchically in a particular way forces a redesign on user types that I don't quite like.

Beyond that approach, the no-op construction/destruction imposes a bit more work on the library, but still there are far less implementors of containers than general types so the burden is still small: the library needs to add some code to instruct the compiler/analyzers of lifetime changes. Compilers should not have any trouble optimizing the loops of no-op construction/destruction and while those look like real code they become no more than annotations [should we maybe consider using some form of annotation rather than the loop?]

David's approach here is the one that makes this available in the shorter term (the impact on the standard is small enough that reasoning about it is simple and, I think we can agree, safe). But at the same time it is the most expensive alternative of the three for developers, and not trivial to use (not only for the library implementor, but also for the implementor of the relocatable types).

If the other approaches hit a wall and cannot proceed, David's approach is better than nothing, but I'd rather get something less intrusive on user code.

    David


--

---
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.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.

Edward Catmur

unread,
Aug 3, 2015, 4:33:16 PM8/3/15
to ISO C++ Standard - Future Proposals, dib...@ieee.org
I agree; I don't see how this proposal can compose types. If one of the base class members is nontrivially destructively movable, this proposal nevertheless calls its destructor so it needs to be in a destructible state.

To be useful (IMO), destructive move must automatically compose for record types (i.e. structs without a user-declared destructor) as well as for pair and tuple; it should be (reasonably) easy to customize for self-referential types while optimizing to memcpy-equivalent for trivially destructive movable types.

For example, struct employee should be efficiently destructively movable despite containing one or more strings (which under pointer-to-internal-buffer SSO require special handling); its automatically generated destructive move should destructively move each data member in turn, which might optimize to one big memcpy if string is not pointer-to-internal-buffer SSO.

As I see it, this requires either a new category of special function (as with the introduction of move constructors in C++11, so the Rule of Five becomes a Rule of Six, or Seven if we're going to add a destructive move assignment operator), or introspection facilities such that Pablo's uninitialized_destructive_move (n4158) can recurse on bases and members.

For the former (strawman syntax):

struct A { int i; };
struct B : A { ~B() {} B~(B const&) = default; };
struct C { B m; int* p = &m.A::i; C~(C const& rhs) : m~{rhs.m}, p{&m.A::i} {} };
void f() {
 
char* storage = new char[sizeof C];
  C
* p = new (storage) C;
  C c
~ *p;  // destructive move construction; ends lifetime of *p
 
delete[] storage;
}

Here the destructive move constructor T::T~(T const&) is automatically defined as defaulted if T has no user-declared copy constructor, no user-declared move constructor, and no user-declared destructor. The initializers ~ expression and ~{ initializer-list } call a destructive move constructor if available, otherwise a copy constructor followed by destruction of the argument.

For the latter, the optimized variant of uninitialized_destructive_move(T* from, T* to)  would become:

new (to) T(__COOKIE__);
for_each_base
<T>([&](auto base_ptr) { uninitialized_destructive_move(&from->*base_ptr, &to->*base_ptr); });
for_each_member
<T>([&](auto mem_ptr) { uninitialized_destructive_move(&from->*mem_ptr, &to->*mem_ptr); });
from->~T(__COOKIE__);

This would be created automatically if T has no user-declared copy constructor, no user-declared move constructor, and no user-declared destructor, or on an opt-in basis (via is_memberwise_destructive_movable?).

In either case new core language is required; less in the latter case (as long as you count introspection as being outside core).

David Krauss

unread,
Aug 4, 2015, 12:52:38 AM8/4/15
to std-pr...@isocpp.org
On 2015–08–03, at 4:56 PM, David Rodríguez Ibeas <dib...@ieee.org> wrote:

There's a couple of things I dislike of David Krauss' proposal, while I understand that it is an attempt to make something work in a much simpler way than the alternatives in the language.

The priorities are:

1. Correctness. Generating wrong programs is taking a step backward.
2. Efficiency and generality. There are two dimensions of generalization: handling nontrivial move-and-destroy, and handling composition.
3. Ease of use. “Simplicity” isn’t genuine if you need to work through details of the object model, such as manually computing is_pod.

I dislike forcing the design of the types into a base with members and a derived type with functionality on purely theoretical grounds (was this the only concern I may swallow those), but in forcing a particular layout of the members and code I feel it inhibits non-trivial uses. I don't see, for example, how composibility could be obtained. In the libraries that offer relocability through traits this is easily handled in BSL:

Yeah, composition seems to merit support.

Requiring the user to factor out the base is a positive aspect. If they don’t, the compiler still has to do something very similar behind the scenes. What gets initialized by __COOKIE__? A layout-compatible but trivially constructible class that hasn’t been declared. The language works better when all the cards are on the table.

Factoring out a base class is pretty elementary. What’s the theoretical objection? Don’t like to declare classes that aren’t part of the public interface? That’s a general impediment to good OO design. Remember that this is a performance optimization. That alone puts aesthetics at risk, and something’s gotta give: The alternative is to open namespace std for the sake of adding a manual specialization.

There is one thing that can break when factoring a base. The type of address-of-member expressions &foo::mem becomes mem_t base::*, which doesn’t implicitly convert back to the original type mem_t derived::*. Perhaps this should be revisited; AFAIK it’s only done that way for ease of implementation and it often causes headaches when using PTMs.

Back to composition: If the base class is required to be an aggregate, then it’s not too “dirty” to let destructive move construction propagate down to its members. As for the mechanics of that, I’ll have to let this simmer on the back burner for a little while.

I believe that from a user's point of view (more people will develop code than the standard), the simplest approach is what current libraries do: the user that creates a type need only provide the trait, the library does the rest of the work and that work is minimal at the library level. Requiring that the movable type is split hierarchically in a particular way forces a redesign on user types that I don't quite like.

The trait is ugly and tricky to declare (sacrifice #3 ease) and tricky to specify (you need to remember to compose the types of all the members, sacrifice #1 correctness). Doubly so for traits of templates. The trait only covers the trivial case, otherwise you need to reimplement the destructive move algorithm (sacrifice #2 generality or double down on sacrifices of #1 and #3).

The established practice of libraries is a local optimum for metaprogramming. Adding a feature to the core language should be a deeper process than internalizing a metaprogram.

David's approach here is the one that makes this available in the shorter term (the impact on the standard is small enough that reasoning about it is simple and, I think we can agree, safe). But at the same time it is the most expensive alternative of the three for developers, and not trivial to use (not only for the library implementor, but also for the implementor of the relocatable types).

If the other approaches hit a wall and cannot proceed, David's approach is better than nothing, but I'd rather get something less intrusive on user code.

My approach isn’t primarily motivated by minimizing the changes to the standard, it’s for correctness and generality.

I need to think about how aggregates can be accommodated (without any user intervention, not even a trait), and whether that extension is sufficient.

Pablo Halpern

unread,
Aug 4, 2015, 11:44:27 AM8/4/15
to David Krauss, std-pr...@isocpp.org
Chandler and I have not connected yet on this topic. N4393 was not well
received at the last meeting and Chandler claimed to have better ideas,
but I have not learned from him what those ideas are. I'm afraid I lost
a bit of momentum since that meeting in not following up immediately
with Chandler. I will need to put that back on the front burner soon.

David, I'm afraid I don't understand your base-class destructor idea.
Possibly I missed the first part of the discussion. I will point out
that, for destructive move, the destructor is actually the easy part.
If I understand the language correctly, it is already valid to end the
lifetime of an object simply by reusing the memory. Therefore, a noop
destruction should be implementable this way:

template <class T>
void unbless(T& x) noexcept {
struct empty { };
empty *e = ::new (addressof(x)) empty;
e->~empty();
}

Taking a bunch of bytes and making a valid object out of them (noop
construction) is more challenging, so trivial destructive move
construction (using the equivalent of memcpy) is still problematic.
However, explicit destructive move could be implemented on a
class-by-class basis:

MyClass::MyClass(destructive_move_ref<MyClass> dr) noexcept
{
MyClass& r = dr.get_ref();
this->mem1 = dr.mem1;
this->mem2 = dr.mem2;
...
unbless(r);
}

If this is invoked in a tight loop, can the compiler's optimizer convert
it to a memcpy? If so, then perhaps the trivial variant isn't needed,
though it would be extremely convenient for a large category of types.

-Pablo

David Krauss

unread,
Aug 5, 2015, 1:04:01 AM8/5/15
to Pablo Halpern, ISO C++ Standard - Future Proposals
On 2015–08–04, at 11:44 PM, Pablo Halpern <phal...@halpernwightsoftware.com> wrote:

David, I'm afraid I don't understand your base-class destructor idea.  Possibly I missed the first part of the discussion.  I will point out that, for destructive move, the destructor is actually the easy part.  If I understand the language correctly, it is already valid to end the lifetime of an object simply by reusing the memory.  

Only for trivially destructible objects. As I understand the original concern, the problem is that a nontrivial destructor must be executed before constructing a new object at the same location. Otherwise it’s a runtime violation of the one-object-per-address rule [intro.object] §1.8/6. That rule is usually taken to constrain ABIs, but sanitizers may also rely on and/or check it. On the other hand, reviewing [basic.life] §3.8/1.3, this interpretation requires the final “or” to be taken to mean “otherwise” which isn’t really what it says.

Besides such a technicality, the user might really need a destructor call. I’ve used nontrivially destructible fancy pointers to manage garbage collection. (Though structured a bit differently, that library should have been implementable as a custom allocator on std::string, but for the lack of a shrink_to_fit hook.) Allocator objects likewise may retain a shared memory pool.

The normative permission to end an object’s lifetime without calling its nontrivial destructor §3.8/4 has a stern warning about undefined behavior. The standard library should avoid skipping nontrivial destructors.

Therefore, a noop destruction should be implementable this way:

template <class T>
void unbless(T& x) noexcept {
   struct empty { };
   empty *e = ::new (addressof(x)) empty;
   e->~empty();
}

~empty() should be unnecessary if ~T() is. Also, ~empty() is trivial so it can certainly be omitted. The new-expression fails to begin the lifetime of *e because it is vacuous per §3.8/1.

This should be sufficient (though I’m not recommending it):

struct empty { empty() {} };

template <class T>
void unbless(T& x) noexcept {
   new (addressof(x)) empty;
}

If this is invoked in a tight loop, can the compiler's optimizer convert it to a memcpy?  If so, then perhaps the trivial variant isn't needed,

Yeah, that’s my thinking.

though it would be extremely convenient for a large category of types.

Even more convenient would be getting an an implicit definition to do it for you, regardless of triviality. But, that sticks something like destructive_move_ref into the core language.

David Rodríguez Ibeas

unread,
Aug 5, 2015, 5:41:02 AM8/5/15
to std-pr...@isocpp.org
On Wed, Aug 5, 2015 at 6:03 AM, David Krauss <pot...@gmail.com> wrote:

The normative permission to end an object’s lifetime without calling its nontrivial destructor §3.8/4 has a stern warning about undefined behavior. The standard library should avoid skipping nontrivial destructors.

The warning in 3.8/4 would not apply to a type that has been marked as destructive-movable as marking it as such would be the programmer's blessing that it is fine (and even desirable) to skip the destructor, that it is fine and desirable for the program not to have the side effects of the destructor when used in a destructive move operation.
 
Even more convenient would be getting an an implicit definition to do it for you, regardless of triviality. But, that sticks something like destructive_move_ref into the core language.

 Having a 'destructive_move_ref' in the core language could also allow for alternative implementations of destructive-move-constructor where a self referencing type could overload an specific constructor/destructor to do something like memcpy + patching of references. For example, in the case of a string doing RVO and holding a possibly self-referencing pointer it could do a memcpy of the string object and conditionally adjust the pointer to the internal buffer if it is using the small buffer.  Then again, at this time it would (guessing) not be more efficient than the equivalent move-constructor.

On a wicked attempt at bike-shedding the operation, we could add a move-destructor:

template <typename T, typename A = std::allocator<T> >
class list {
   // Imagine this as Dinkumware's implementation of 'std::list', assume SFINAE for a destructive-movable allocator
   Node *sentry;  // for exposition

   list(list&& src) noexcept(false);
   ~list(void *dst) noexcept(true) { // ~list([[uninitialized]] list *dst)
       memcpy(dst, this, sizeof *this);
   }

With that move-destructor being blessed by the standard as both terminating the lifetime of 'this' and creating a new object over 'dst', behaviorally equivalent, with respect to lifetimes, to:

new (dst) list(std::move(*this));
this->~list();

The optimizer would have to figure out, does not seem too complicated, that multiple memcpy's for contiguous memory can be coalesced into a single memcpy/memmove.  The 'memmove' is because while the move-destructor cannot be called (documented as undefined behavior) with 'dst == this', a loop of move-destructor's may be shifting elements in overlapping regions in a vector.

The move-destructor would not be implicitly declared, but could be defaulted to just do the memcpy, so implementors can just do:

~list(void *dst) = default;

which would at the same time be detectable by a 'is_move_destructible<T>', the real change to 'std::list' in such implementations would be:

template <typename T, typename A = allocator<T>>
class list {
// ...
    template <typename U = A, typename _ = typename enable_if<is_move_destructible<A<T>>::type>
    ~list(void *dst) = default

Which is a bit obscure, but not too horrible for a library implementation. (I feel that the argument being 'list *dst' would be nicer for the developer, but I am not sure about passing a pointer to a memory location that is not a 'T' as a 'T*', if that is fine I'd prefer to have the argument typed).

    David

David Rodríguez Ibeas

unread,
Aug 5, 2015, 5:43:03 AM8/5/15
to std-pr...@isocpp.org
Ouch, let me fix that declaration:

template <typename T, typename A = allocator<T>>
class list {
// ...
    template <typename U = A, typename _ = typename enable_if<is_move_destructible<U>>::type>
    ~list(void *dst) = default

Ion Gaztañaga

unread,
Aug 5, 2015, 7:20:45 PM8/5/15
to std-pr...@isocpp.org, Pablo Halpern
El 05/08/2015 a las 7:03, David Krauss escribió:
> Only for trivially destructible objects. As I understand the original
> concern, the problem is that a nontrivial destructor must be executed
> before constructing a new object at the same location. Otherwise it’s a
> runtime violation of the one-object-per-address rule [intro.object]
> §1.8/6. That rule is usually taken to constrain ABIs, but sanitizers may
> also rely on and/or check it. On the other hand, reviewing [basic.life]
> §3.8/1.3, this interpretation requires the final “or” to be taken to
> mean “otherwise” which isn’t really what it says.

Sorry for jumping in the middle of the discussion, I don't think the
standard requires calling the destructor at all. The standard states
that the lifetime of an object ends when the storage is reused:

"4 A program may end the lifetime of any object by reusing the storage
which the object occupies or by explicitly calling the destructor for an
object of a class type with a non-trivial destructor. For an object of a
class type with a non-trivial destructor, the program is not required to
call the destructor explicitly before the storage which the object
occupies is reused or released; however, if there is no explicit call to
the destructor or if a delete-expression (5.3.5) is not used to release
the storage, the destructor shall not be implicitly called and any
program that depends on the side effects produced by the destructor has
undefined behavior."

So not calling the destructor is not UB, it's UB if any other code
depends on the work the destructor does (that is, the destructor frees
resources to avoid leaks or blocked resources, the destructor
communicates with other classes or observers that always need to track
when the object is being destroyed because external objects track the
address of the object to be destroyed, ...). In any case, the writer of
the destructive move operation can take care of such side-effects the
same way as the destructor does.

IMHO, destructive move semantics are allowed by current C++ rules, it
needs a constructor (which is required to start the lifetime of an
object) for destructive move construction and a plain function for the
destructive move assignment. I don't think any special rule or cookie
constructor/destructor is absolutely needed. Maybe those special
constructors/destructors could help the compiler to do a better job.
memcpy-ing for destructive move constructors is another issue, but
related to the start of the lifetime of the new objects.

Just my 2 cents,

Best,

Ion

Pablo Halpern

unread,
Aug 5, 2015, 8:34:47 PM8/5/15
to Ion Gaztañaga, std-pr...@isocpp.org
Thanks, Ion. That is my understanding as well. One question that
doesn't seem to be answered: can you end the lifetime of an object by
deallocating its storage (again, without using a delete expression). If
so, then there is no need for the trick I used in overwriting the object
with an empty class type.

-Pablo

Ion Gaztañaga

unread,
Aug 6, 2015, 1:15:36 AM8/6/15
to Pablo Halpern, std-pr...@isocpp.org
El 06/08/2015 a las 2:34, Pablo Halpern escribió:
> Thanks, Ion. That is my understanding as well. One question that
> doesn't seem to be answered: can you end the lifetime of an object by
> deallocating its storage (again, without using a delete expression). If
> so, then there is no need for the trick I used in overwriting the object
> with an empty class type.

According to the standard, you can (3.8 Object lifetime):

The lifetime of an object of type T ends when:
(1.3) — if T is a class type with a non-trivial destructor (12.4), the
destructor call starts, or
(1.4) — the storage which the object occupies is reused or *RELEASED*

I understand that 1.4 applies also to types with non-trivial destructors.

So you can:
- release that memory (but this is outside the scope of the destructive
move operation)
- explicitly "reuse" memory
- just wait until it's reused someday, as the side effects are already
committed by the destructive move operation. Whether the lifetime of the
original object was destroyed or not it's irrelevant for the rest of the
program, as no additional side effects related to that object will be
visible once the destructive move ends. This could make some sanitizers
work harder, who knows.

The trick with the empty class (or just using a "char", might be enough,
so the alignment is always guaranteed) would "end" the lifetime of the
original object before the destructive move operation ends. I don't know
if the compiler will be happier with this "explicit" lifetime end, I
suspect just waiting the memory to be reused (my the memory
allocator/stack handling or a placement new/memcpy) or released (maybe
at program exit) will be fine.

Just a note: since "empty" has a trivial destructor, then this:

e->~empty();

would not end the lifetime of the object as (1.3) applies only to types
with non-trivial destructors ;-).

IMHO it's an oversight, bullet 1.3 should read:

(1.3) — the destructor invocation starts, or
(1.4) — the storage which the object occupies is reused or released

You could define an inline empty destructor in empty to "guarantee"
current (1.3) wording.

Best,

Ion

David Rodríguez Ibeas

unread,
Aug 6, 2015, 4:35:08 AM8/6/15
to std-pr...@isocpp.org, Pablo Halpern
I wasn't as much concerned with the destruction as the construction. The question comes back again as of whether a compiler could be smart enough to merge multiple 'memcpy', and while in the trivial case it would seem reasonable, I fear the are some cases that are not so trivial in the presence of padding, for example.

Additionally, my understanding --we need measurements here, I had not had the time to test this-- is that a big part of the gain would be blasting contiguous objects with a single call to 'memcpy' or 'memmove'. For the example of a vector that needs to grow the internal buffer a compiler merging the 'memcpy' for the destructive-move-constructor suffices. But there are other operations like insert in the middle of a vector (assume capacity is enough for the insertion) where an implementation could do something like:

vector<T>::insert_impl(iterator pos, T&& value, destructive_move_tag) 
    // picked by tag-dispatch for destructive-movable types T
{
    // Create the object into a local buffer:
    alignas(T) char buffer[sizeof value];
    new (buffer) T(move(value));

    // From here on nothing can throw
    for (iterator e = __end; e > pos; --e) {
        new (addressof(*e)) T(destructive_move_tag, e-1);  // internally 'memcpy'
    }
    new (addressof(*pos)) T(destructive_move_tag, reinterpret_cast<T*>(buffer));
    ++__end;
}

And hope that the compiler transforms that into:

…  // From here on nothing can throw
     memmove(addressof(*pos)+1, addressof(*pos), (__end-pos) * sizeof(T));  // not memcpy!!
     memcpy(addressof(*pos), reinterpret_cast<T*>(buffer), sizeof(T));
     ++__end;
}

At the same time, this depends on the destructive-move constructor being visible at all to the compiler (either it is inline, or it whole-program optimization somehow detected and decided to re-inline the function.

Beyond the implementation details, users must provide the destructive-move constructors (I am fine, this is a dangerous thing to do, be conscious!) and the implementation of that function has to be palatable for the optimizer to do the transformation.

Again, padding might have a negative effect here, consider a non-empty comparator in std::set that is destructive-movable. Consider also that the size of such object is smaller than the natural alignment for the platform, so that there is internal padding inside std::set<T> between the comparator and the other members. The implementation cannot just "blast" the comparator if, being user provided, it may do something other than memcpy.  It would have to call the destructive-move on the comparator and then blast the rest of the members, generating a destructive-move constructor with 3 memcpy (allocator, comparator, rest of the members), yet those memcpy are not contiguous due to internal padding.  

Whether an implementation is allowed to detect that moving the padding around is not problematic and can be merged or not determines whether a vector<set<T>> can do a single 'memcpy' to grow the buffer or needs to loop over each one of the elements and run the constructor one by one.

To some extents these are concerns more on the feasibility of the implementation (optimizer) than the language itself, and it can be argued that if a tag for the constructor, a wrapper ('destructive_move_ref<T>()') or any other mechanism is standardized, compiler writers would have an incentive to work towards providing these optimizations.

Yet another concern is that the source object is left in an invalid state, which opens the path for misuse, for example the implementation of 'insert_impl' above could be attempted by a non-expert user as:

vector<T>::insert_impl(iterator pos, T&& value, destructive_move_tag) 
   T tmp(move(value));
   …

And at the end of the function when the destructor runs you are likely to hit undefined behavior as the object is invalid.  Worse, the 'tmp' could be omitted altogether and the last line transformed into:

     new (addressof(*pos)) T(destructive_move_tag, value); // reference to externally managed object


    David



Best,

Ion

Ion Gaztañaga

unread,
Aug 6, 2015, 6:46:35 AM8/6/15
to std-pr...@isocpp.org, Pablo Halpern
El 06/08/2015 a las 10:35, David Rodríguez Ibeas escribió:
> I wasn't as much concerned with the destruction as the construction. The
> question comes back again as of whether a compiler could be smart enough
> to merge multiple 'memcpy', and while in the trivial case it would seem
> reasonable, I fear the are some cases that are not so trivial in the
> presence of padding, for example.

I don't think the compiler will merge it, at least many compilers don't
optimize even simple loops into memcpy-s:

http://nadeausoftware.com/articles/2012/05/c_c_tip_how_copy_memory_quickly

A different issue is the direct use of memcpy for "is_relocatable"-like
types to implement highly optimized destructive move semantics. It's UB
even with N3751 which proposes that object lifetime starts with memcpy
for trivially copyable types:

http://open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3751.pdf

Of course in practice this kind of destructive move semantics work, as
used in BDE, Folly and other libraries:

https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md

"Only a tiny minority of objects are genuinely non-relocatable"

Supporting "trivially destructive movable" without UB is a much harder
work for the language.

Ion

Edward Catmur

unread,
Aug 6, 2015, 7:04:14 AM8/6/15
to std-pr...@isocpp.org
On Thu, Aug 6, 2015 at 11:43 AM, Ion Gaztañaga <igazt...@gmail.com> wrote:
El 06/08/2015 a las 10:35, David Rodríguez Ibeas escribió:
I wasn't as much concerned with the destruction as the construction. The
question comes back again as of whether a compiler could be smart enough
to merge multiple 'memcpy', and while in the trivial case it would seem
reasonable, I fear the are some cases that are not so trivial in the
presence of padding, for example.

I don't think the compiler will merge it, at least many compilers don't optimize even simple loops into memcpy-s:

http://nadeausoftware.com/articles/2012/05/c_c_tip_how_copy_memory_quickly


That's several years out of date; modern compilers (gcc 5; clang 3.7; icc) have no problem ignoring padding and optimizing to memcpy. Example: http://goo.gl/a0Xka7

Of course in practice this kind of destructive move semantics work, as used in BDE, Folly and other libraries:

https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md

"Only a tiny minority of objects are genuinely non-relocatable"

That tiny minority includes libstdc++ std::string, though. You might not agree with internal pointer SSO, but branch-free reads have a considerable performance benefit.
 

David Rodríguez Ibeas

unread,
Aug 6, 2015, 7:17:36 AM8/6/15
to std-pr...@isocpp.org
On Thu, Aug 6, 2015 at 12:04 PM, 'Edward Catmur' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:
That's several years out of date; modern compilers (gcc 5; clang 3.7; icc) have no problem ignoring padding and optimizing to memcpy. Example: http://goo.gl/a0Xka7

So that solves the padding issue, but we are left with merging multiple memcpy into a single one, as in http://goo.gl/7SPzPl then again, if this becomes common I'd assume compiler vendors would add that as an optimization.

   David

Ion Gaztañaga

unread,
Aug 6, 2015, 7:35:44 AM8/6/15
to std-pr...@isocpp.org
El 06/08/2015 a las 13:17, David Rodríguez Ibeas escribió:
>
> On Thu, Aug 6, 2015 at 12:04 PM, 'Edward Catmur' via ISO C++ Standard -
> Future Proposals <std-pr...@isocpp.org
> <mailto:std-pr...@isocpp.org>> wrote:
>
> That's several years out of date; modern compilers (gcc 5; clang
> 3.7; icc) have no problem ignoring padding and optimizing to memcpy.
> Example: http://goo.gl/a0Xka7
>
> So that solves the padding issue, but we are left with merging multiple
> memcpy into a single one, as in http://goo.gl/7SPzPl then again, if this
> becomes common I'd assume compiler vendors would add that as an
> optimization.

Right. Extremely simple destructive moves loop are not merged to a
memcpy. See:

http://goo.gl/7AN4wH

Best,

Ion

David Krauss

unread,
Aug 6, 2015, 11:06:06 AM8/6/15
to std-pr...@isocpp.org

On 2015–08–06, at 7:18 AM, Ion Gaztañaga <igazt...@gmail.com> wrote:

Sorry for jumping in the middle of the discussion, I don't think the standard requires calling the destructor at all. The standard states that the lifetime of an object ends when the storage is reused:

It’s fine to jump into the middle of the discussion, but please do consider the entire message to which you’re replying. You quoted only my first paragraph, your reply amounts to a recap of its last sentence, and the standard paragraph you quoted recaps the standard paragraph I cited there.

To summarize all three arguments in my last message:

1. The committee has expressed concern at unceremoniously ending object lifetimes. I don’t recall reviewing the rules there in the room, but there was a sense of need for a core language unbless feature. Sanitizers provide a practical motivation, and they might care to warn about things that are suspicious but not UB.

I wasn’t clear enough in saying “As I understand the original concern…” but I’m trying to reflect what I heard at the review of N4158. (The minutes record similar concerns in the review of N4393.) I did already note that the normative lifetime rules don’t seem to form a basis for this concern, but they don’t need to.

2. Nontrivial destructive move operations are important. It’s not safe to assume that allocators and Allocator::pointer types are trivially destructible. I linked a practical example.

Another example was cited earlier (and again, since): std::string may use a self-reference to indicate SSO, in which case it isn’t trivially destructive movable, but it could still provide a destructive move operation and avoid dirtying cache lines by nulling the source.

3. The standard library should have exemplary behavior. Absence of immediate UB is only a weak argument in favor of an approach.


On 2015–08–06, at 7:33 PM, Ion Gaztañaga <igazt...@gmail.com> wrote:

Right. Extremely simple destructive moves loop are not merged to a memcpy. See:

http://goo.gl/7AN4wH

Wrong. Alias analysis prevents using memcpy there. The compiler also prefers to be working with POD types when generating memcpy, which is why the base slice technique works where yours doesn’t: http://goo.gl/ZL5yhW

I had already provided a godbolt.org link in this thread. Please take the time to review earlier posts.

Matthew Woehlke

unread,
Aug 6, 2015, 11:36:58 AM8/6/15
to std-pr...@isocpp.org
On 2015-08-06 04:35, David Rodríguez Ibeas wrote:
> I wasn't as much concerned with the destruction as the construction. The
> question comes back again as of whether a compiler could be smart enough to
> merge multiple 'memcpy', and while in the trivial case it would seem
> reasonable, I fear the are some cases that are not so trivial in the
> presence of padding, for example.

I think the 'constructing destructor' pattern, with ability to check for
those being trivial, would solve this problem (as well as allowing
cheaper relocations even for classes that are self-referencing). Users
that care (e.g. containers) can check for defaulted 'constructing
destructors' and explicitly do a memmove instead; no compiler cleverness
needed.

It occurs to me that *ALL* classes should *have* a cting-dtor, but that
the same is not necessarily trivial.

The implicit behavior should be to move-construct the target and then
destruct the object. The explicitly defaulted behavior should be a
memberwise call to the cting-dtors of each member. A trivial cting-dtor
(e.g. implicit for POD types) is a memcpy. The compiler should be able
to combine multiple trivial cting-dtors in a compound object into one or
more memcpy's (e.g. could be more than one in case of a member with
non-trivial cting-dtor in the middle of the compound type).

This allows us to easily optimize the relocation operation for types
that consist /mainly, but not entirely/ of trivially relocatable
members. It also allows us to potentially relax the rules (either now,
or at some future date) for when the implicit cting-dtor can be made
trivial. This would allow for example 'struct Point { double x, y; };'
to be given a trivial cting-dtor without having to explicitly declare a
defaulted cting-dtor, which is an obvious win.


IOW:

// implicit cting-dtor
foo::~foo(foo* new_foo) // or char* / void*
{
new (new_foo)(std::move(this));
this->~foo();
}

// trivial cting-dtor
foo::~foo(foo* new_foo) // or char* / void*
{
memcpy(new_foo, this, sizeof(foo));
__builtin_create<foo>(new_foo); // bless new object as created
__builtin_release(this); // pedantic, probably no-op
}

(I think having a concept of triviality of the cting-dtor might help
with some of the other examples you gave...)

> To some extents these are concerns more on the feasibility of the
> implementation (optimizer) than the language itself, and it can be argued
> that if a tag for the constructor, a wrapper ('destructive_move_ref<T>()')
> or any other mechanism is standardized, compiler writers would have an
> incentive to work towards providing these optimizations.

Yes. (And again, having triviality would enable the library to tackle
probably many of the most critical cases if the optimizer can't.)

> Yet another concern is that the source object is left in an invalid state,
> which opens the path for misuse

If you implement this by cting-dtor, the source *has been destroyed*. So
while the above is true, it's no worse than:

pfoo->~foo();
// referring again to pfoo at this point would be bad...

The invocation would need to be something like:

// allocate non-typed buffer e.g. using malloc
old_ptr->~T(new_ptr);
// *old_ptr is now destroyed, new_ptr is now a constructed T

(And I believe the onus is already on the compiler to know that the
object is already destroyed if a dtor is explicitly called?)

> for example the implementation of
> 'insert_impl' above could be attempted by a non-expert user as:
>
> vector<T>::insert_impl(iterator pos, T&& value, destructive_move_tag)
> T tmp(move(value));

This can't work (i.e. can't use a cting-dtor); rvalue reference
semantics require that 'value' is left in a constructed (if unspecified)
state. Use emplace if you want to avoid the additional allocation.

The only other way this could be solved is to invent a new indirection
type, probably '~', that says that the callee will destroy the object.

Anyway, this isn't the major bottleneck, which is relocating a large
number of contiguous values e.g. when resizing an array.

--
Matthew

Ion Gaztañaga

unread,
Aug 6, 2015, 4:10:56 PM8/6/15
to std-pr...@isocpp.org
David Krauss wrote:

> It’s fine to jump into the middle of the discussion, but please do
> consider the entire message to which you’re replying. You quoted only my
> first paragraph, your reply amounts to a recap of its last sentence, and
> the standard paragraph you quoted recaps the standard paragraph I cited
> there.

Sorry for any noise, it was not my intention.

> To summarize all three arguments in my last message:

Thanks, that will help me summarize also my points.

> 1. The committee has expressed concern at unceremoniously ending object
> lifetimes. I don’t recall reviewing the rules there in the room, but
> there was a sense of need for a core language unbless feature.
> Sanitizers provide a practical motivation, and they might care to warn
> about things that are suspicious but not UB.

That's a logical concern, my only comment was that current core language
is quite clear IMHO. In theory, library-only destructive move semantics
are possible, this is a good thing since we could just implement a
library to explore the field and catch potential problems. Current rules
might not be sanitizer-friendly, though. There is room from improvement,
no doubt.

> 2. Nontrivial destructive move operations are important. It’s not safe
> to assume that allocators and Allocator::pointer types are trivially
> destructible. I linked a practical example.
>
> Another example was cited earlier (and again, since): std::string may
> use a self-reference to indicate SSO, in which case it isn’t trivially
> destructive movable, but it could still provide a destructive move
> operation and avoid dirtying cache lines by nulling the source.

We agree that they are important. There are quite a few self-referencing
types in the library, like std::slist/std::list/std::map/set
implementations.

> 3. The standard library should have exemplary behavior. Absence of
> immediate UB is only a weak argument in favor of an approach.

Agreed.

Best,

Ion

Ion Gaztañaga

unread,
Aug 6, 2015, 4:57:39 PM8/6/15
to std-pr...@isocpp.org
David Krauss wrote:

> Wrong. Alias analysis prevents using memcpy there. The compiler also
> prefers to be working with POD types when generating memcpy, which is
> why the base slice technique works where yours doesn’t: http://goo.gl/ZL5yhW

Thanks for the aliasing point. It's strange that POD-ness restricts the
memcpy-ability. Is also interesting to see that GCC (5.2) skips memcpy
on both cases.

Best,

Ion

isocp...@denisbider.com

unread,
Aug 7, 2015, 3:50:23 AM8/7/15
to ISO C++ Standard - Future Proposals, dib...@ieee.org
I agree with this suggestion. I think the move destructor idea is ingenious.

A move destructor:

- Solves the problem of figuring out syntax for a destructive move constructor. Destructors can't have overloads, so this works great. (The parameter to the move destructor should probably a reference, though, not a pointer.)

- Provides for an easy way to declare a type relocatable AND provide compiler inference:

struct A : B {
  ~A(A& x) = default;
  C x;
};

The defaulted move destructor can do whatever is appropriate depending on the definitions of B and C types.

A move destructor can even be generated implicitly in certain cases.

Genius idea. Elegant syntax. Covers all use cases. Supports types that require patch-ups for relocation. Completely general.

Thumbs up!

isocp...@denisbider.com

unread,
Aug 7, 2015, 4:00:59 AM8/7/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
It would be nice if the move destructor does not expose too much of a "nuts and bolts" interface - such as requiring the developer to call placement new, explicitly call a destructor, and so forth.

What if the move destructor is called with an object reference that has ALREADY had memcpy performed, and is expected to simply perform a patch-up?

This way, if the move destructor does nothing, the object has been trivially moved. No additional code required.

If the move destructor is declared deleted, the object cannot be destructively moved.

The way an array of objects is moved could then consist of:

1. memcpy whole array of objects
2. call move destructors to perform any patch-ups


Static analysis tools would detect step (2) as transition of objects to new location.

No need to perform magic to consolidate the memory copies, and static analysis works.

isocp...@denisbider.com

unread,
Aug 7, 2015, 5:03:59 AM8/7/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com, isocp...@denisbider.com
To avoid any issues with the memcpy/memmove step (1) being separate from the call-move-destructor step (2), we can make the compiler generate code that encapsulates this; similar in nature to how the keywords new and delete work.

So, if we have an object with a move destructor:

class StringType {
  ~StringType(StringType& x) {
    // x is already a bitwise copy of *this
    // perform patch-up of self-references
  }
};


The developer does not invoke this directly, but via a compiler-generated static destructor, declared like this:

StringType* StringType::~StringType(void* d, StringType* s, std::size_t n=1);


When this is invoked, the compiler then generates the equivalent of this:

memmove(d, s, n * sizeof(StringType));
StringType* dt = (StringType*) d;
for (std::size_t i=0; i!=n; ++i)
  s[i].~StringType(dt[i]);
return dt;


For trivial types, and for types that are trivially relocatable (empty move destructors) - the for loop is simply optimized away.

For types that require patching-up during relocation, the for loop calls the move destructors that take care of it.

But crucially - the developer does not perform these individual steps. The developer just calls:

StringType* dest = StringType::~StringType(destptr, src, n);


This construction doesn't work with realloc - it requires simultaneous access to both old memory and new, which realloc does not provide - but some type of realloc_in_place_only could be provided for this purpose.

Matthew Woehlke

unread,
Aug 7, 2015, 10:25:22 AM8/7/15
to std-pr...@isocpp.org
On 2015-08-07 04:00, isocp...@denisbider.com wrote:
> It would be nice if the move destructor does not expose too much of a "nuts
> and bolts" interface - such as requiring the developer to call placement
> new, explicitly call a destructor, and so forth.

*Something* has to bless the new object as created, and the old object
as destroyed (that's the whole reason we're having so much discussion).

Note that you *aren't* required to use placement new. Rather, you have
the *option* to use placement new, as one way of initializing the new
object. I think this is a very good thing because it allows you to
control which ctor is used. Similarly, explicitly calling the dtor
allows you to control when - or *if* - it is called.

Note also that you are referring to the *default implicit* cting-dtor.
If you are writing your own, it is probably because you are doing
something special to construct the new object, and *need* that level of
control... and because you aren't actually going to call the old
object's dtor at all!

A better question is if the compiler should implicitly add the necessary
calls to "bless" the new object as created and the old object as
destroyed, regardless of what actually happens in the cting-dtor
(excepting perhaps that it throws, though of course a cting-dtor that
isn't nothrow is of limited value). Quite possibly "yes", especially as
those calls may not be portable. The only potential issue there is
ensuring we don't run into some odd trap with "twice blessing", but
since all of this is ultimately language-lawyering issues and not
anything that is a problem for actual current generated assembly, I
imagine we can work around that, if there is even a problem in the first
place.

> What if the move destructor is called with an object reference that has
> ALREADY had memcpy performed, and is expected to simply perform a patch-up?

This is not allowed. You must not use memcpy to relocate an object
unless its cting-dtor is trivial. If it isn't, you MUST actually call
the cting-dtor, and are not allowed to do anything outside of the same
to create or destroy the new/old objects.

At least that's my current take. It does slightly pessimize the
std::string case, though I'm not convinced the difference is that great.

(I'd also be concerned that the concept of a "patch up operator" runs
afoul of the same lifetime issues.)

I think it would be better to say that compilers should detect and
optimize the case of running a loop of cting-dtors over a contiguous
memory region where a) the cting-dtor is inline, b) the first thing the
cting-dtor does is memcpy the old object to the new, and c) the
cting-dtor does not access the old memory.

In fact, that last point is why I would be really nervous about a
patch-up operator; you'd have to pass it a (non-writable!) memory
address that contains memory in an undetermined state.

> If the move destructor is declared deleted, the object cannot be
> destructively moved.

My knee-jerk reaction is to forbid that, as the implicit cting-dtor is
possible for any type with a move and/or copy ctor and an accessible
dtor. Deleting the cting-dtor would imply that the type lacks one or
more of those.

But... such a type would be unusable in e.g. a std::vector anyway... so
I suppose there is no harm. I would assert however that container
classes should refuse to instantiate for such a type, i.e. it is not
expected that a container class that *can* use a cting-dtor will or
should provide fallbacks for a type that explicitly deletes it.

--
Matthew

isocp...@denisbider.com

unread,
Aug 7, 2015, 11:42:01 AM8/7/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
> *Something* has to bless the new object as created, and the old
> object as destroyed (that's the whole reason we're having so
> much discussion).

See my last message for that:

StringType* StringType::~StringType(void* d, StringType* s, std::size_t n=1);


This would perform the destructive move of one or more objects as a transaction, just like invoking keywords new or delete is a transaction.

It makes most sense to me that the implementation of this transactional move concept performs the equivalent of memcpy for an object before calling the move destructor. It makes things a lot simpler.

Edward Catmur

unread,
Aug 7, 2015, 11:53:10 AM8/7/15
to std-pr...@isocpp.org
On Fri, Aug 7, 2015 at 4:42 PM, <isocp...@denisbider.com> wrote:
> *Something* has to bless the new object as created, and the old
> object as destroyed (that's the whole reason we're having so
> much discussion).

See my last message for that:
StringType* StringType::~StringType(void* d, StringType* s, std::size_t n=1);
This would perform the destructive move of one or more objects as a transaction, just like invoking keywords new or delete is a transaction.

It makes most sense to me that the implementation of this transactional move concept performs the equivalent of memcpy for an object before calling the move destructor. It makes things a lot simpler.

How does that deal with composition? Unless otherwise specified each base and member should be destructively move constructed from the corresponding entity in the source; or at least there needs to be a mem-initializer-list style syntax so that the compiler can check that each base and member is dealt with. Adding a std::string member to a class should not result in UB.
 
> What if the move destructor is called with an object reference that has
> ALREADY had memcpy performed, and is expected to simply perform a patch-up?

This is not allowed. You must not use memcpy to relocate an object
unless its cting-dtor is trivial. If it isn't, you MUST actually call
the cting-dtor, and are not allowed to do anything outside of the same
to create or destroy the new/old objects. 
 
Absolutely. I think the great thing about this is that having a trivial cting-dtor is precisely the semantics necessary to say that a type can be destructively moved via memcpy; and with appropriate lawyering aggregates get their trivial cting-dtor for free.

> If the move destructor is declared deleted, the object cannot be
> destructively moved.

My knee-jerk reaction is to forbid that, as the implicit cting-dtor is
possible for any type with a move and/or copy ctor and an accessible
dtor. Deleting the cting-dtor would imply that the type lacks one or
more of those. 

That depends on what the implicit behavior might be. If the implicit cting-dtor were to perform memberwise destructive move, declaring it deleted would be the instruction to fall back to move/copy + destruct. 

Matthew Woehlke

unread,
Aug 7, 2015, 12:08:05 PM8/7/15
to std-pr...@isocpp.org
On 2015-08-07 11:42, isocp...@denisbider.com wrote:
> StringType* StringType::~StringType(void* d, StringType* s, std::size_t n=1);
>
> This would perform the destructive move of one or more objects as a
> transaction, just like invoking keywords new or delete is a transaction.

The "one *or more*" (emphasis added) part would be a significant
departure from the current language rules. I'm having a hard time seeing
the justification.

The closest we have currently is delete[], which still calls each
object's dtor individually.

If nothing else, 'this' makes no sense in such a context, i.e. you are
at least missing a 'static'.

> It makes most sense to me that the implementation of this transactional
> move concept performs the equivalent of memcpy for an object before calling
> the move destructor. It makes things a lot simpler.

...and far less generic.

What if doing a memcpy is *wrong*? Now you've wasted a bunch of work
copying data that isn't useful.

Having something like a cting-dtor has obvious benefit (don't waste work
maintaining the old object in a valid state when it's about to be
destroyed anyway). Having a cting-dtor *that can be trivial* has
*enormous* obvious value; many, many objects can be trivially
"relocated"... and indeed libraries already do this despite that it is
UB; having a way to "bless" such operation has obvious benefit.

I'm less convinced that there is significant benefit to optimizing
corner cases. The *only* benefit, AFAICT, to e.g. the std::string case
is merging the memcpy's for an array of such objects into a larger memcpy.

Do you have benchmarks showing that this is a significant improvement?
Keep in mind that the small memcpy will most likely be inlined, and that
there is at least a branch per object in either case in addition to
whatever memcpy(s) happen. (If the patch-up operator isn't inlined,
that's probably going to swamp the memcpy(s).)

--
Matthew

Matthew Woehlke

unread,
Aug 7, 2015, 12:28:30 PM8/7/15
to std-pr...@isocpp.org
On 2015-08-07 11:53, Edward Catmur wrote:
> I think the great thing about [cting-dtors] is that having a trivial
> cting-dtor is precisely the semantics necessary to say that a type can be
> destructively moved via memcpy; and with appropriate lawyering aggregates
> get their trivial cting-dtor for free.

Exactly ;-).

> On 2015-08-07 10:25, Matthew Woehlke wrote:
>> On 2015-08-07 04:00, isocp...@denisbider.com wrote:
>>> If the move destructor is declared deleted, the object cannot be
>>> destructively moved.
>>
>> My knee-jerk reaction is to forbid that, as the implicit cting-dtor is
>> possible for any type with a move and/or copy ctor and an accessible
>> dtor. Deleting the cting-dtor would imply that the type lacks one or
>> more of those.

(Note: my follow up comment, which Edward did not quote, was to allow
deleting the cting-dtor. I can't imagine a case where doing so is useful
- the cases where the operation is not possible should result in it
already being implicitly deleted - except maybe for improving
diagnostics, but I don't see any particular reason to forbid doing so.)

> That depends on what the implicit behavior might be. If the implicit
> cting-dtor were to perform memberwise destructive move, declaring it
> deleted would be the instruction to fall back to move/copy + destruct.

Please re-read my message :-). I clearly stated that the *implicit*
behavior is to do a move/copy + regular destruct. (Which... may still be
trivial, i.e. if the ctors and regular dtor are trivial and all members'
cting-dtors are trivial.)

The *(explicitly) defaulted* cting-dtor calls the cting-dtors for each
member. IOW, explicitly declaring the cting-dtor '= default' is telling
the compiler that even though the ctors and/or regular dtor may be
non-trivial, that the object can be relocated in said manner (i.e. the
ctors / regular dtor can be skipped).

If the cting-dtor is deleted, you can't use it. Period. It should never
be deleted, however, unless the implicit implementation is impossible,
i.e. the dtor is protected, and/or there is no public move/copy ctor. As
noted above, this should already result in it being implicitly deleted
due to the implicit implementation being ill-formed. So the only benefit
I can think of to explicitly deleting a cting-dtor is to get a more
concise error message if someone tries to use it ("is deleted" vs. "is
implicitly deleted because...").

--
Matthew

Edward Catmur

unread,
Aug 7, 2015, 12:49:39 PM8/7/15
to std-pr...@isocpp.org
Ah; I see what you mean now.

I think my confusion is due to the fact that it's a novelty to have = default mean something different to the implicit behavior; for the other special functions an explicitly-defaulted declaration has the effect of restoring the (suppressed) special function of that signature, with the same behavior it would have were it not suppressed. That seems something of a gotcha; it certainly got me.

I may have missed it above, but what is your reasoning for having the implicit behavior perform move/copy+destruct rather than memberwise destructive move? If it's safety, that could be handled by having the implicit behavior predicated on the presence of a user-defined dtor or copy/move ctor/assign, on the reasonable assumption that anything that is location-aware (above its members) would have at least one of those special functions user-defined.



--
Matthew

--

---
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/f1zaOjyUem0/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@isocpp.org.

Matthew Woehlke

unread,
Aug 7, 2015, 1:36:35 PM8/7/15
to std-pr...@isocpp.org
On 2015-08-07 12:49, Edward Catmur wrote:
> Ah; I see what you mean now.
>
> I think my confusion is due to the fact that it's a novelty to have =
> default mean something different to the implicit behavior; for the other
> special functions an explicitly-defaulted declaration has the effect of
> restoring the (suppressed) special function of that signature, with the
> same behavior it would have were it not suppressed. That seems something of
> a gotcha; it certainly got me.

That's a fair point. OTOH I can claim that that's a naming bikeshed :-).
IMO the implicit behavior should definitely be the boring behavior, i.e.
invoke a copy/move ctor and the regular dtor. There should also be a
concise way to specify calling the cting-dtors of all members without
calling the class's own ctor (any) or (regular) dtor. I used '= default'
for that, but it could be something else.

An example would be std::unique_ptr; it has non-trivial move ctor and
dtor, but the cting-dtor can be trivial. The compiler may not be able to
infer this¹, so a straight forward way to declare it as just doing
cting-dtor(s) on the member(s) is desired. (And when the member
cting-dtor(s) are trivial, the class's cting-dtor will also be trivial.
But we'd also still get correct behavior if they weren't.)

(¹ In the specific case of std::unique_ptr, with everything inlined,
it's actually not all *that* far-fetched that the compiler might be able
to infer that the cting-dtor is trivial, even without being so marked,
but that won't always be the case.)

> I may have missed it above, but what is your reasoning for having the
> implicit behavior perform move/copy+destruct rather than memberwise
> destructive move? If it's safety, that could be handled by having the
> implicit behavior predicated on the presence of a user-defined dtor or
> copy/move ctor/assign, on the reasonable assumption that anything that is
> location-aware (above its members) would have at least one of those special
> functions user-defined.

It is (for safety). If the ctor(s)/dtor are non-trivial, the implicit
behavior should not be allowed to skip them. The "normative" behavior
therefore should be move/copy ctor + regular dtor.

Note that I *did* say that the implicit behavior can be "trivial", in
which case despite the "normative" specification, the cting-dtor does in
fact 'degenerate' into a memcpy.

Basically, what that amounts to is there is *always* a cting-dtor unless
it is deleted (implicitly or explicitly), with the implicit behavior
always being The Right Thing™, but also being efficient when it is
provably correct to do so.

I think where we're actually debating is on the implicit behavior being
memberwise copy/move followed by memberwise destroy vs. memberwise
cting-dtor. Those *should* be the same thing, unless there are
order-dependent side effects. Even so, I can probably live with that;
I'm just approaching the problem from something of an "as if" angle
starting from the safest thing to do, while you're fully chasing down
possible optimizations.

--
Matthew

Edward Catmur

unread,
Aug 7, 2015, 2:30:44 PM8/7/15
to std-pr...@isocpp.org
If the ctor(s) and dtor are non-user-defined (defaulted or declared as defaulted) then the user has chosen to defer responsibility for copy/move operations to the bases and members, so why not let the bases and members decide? There's no user-written code being skipped.

I really don't want to have to teach beginning programmers that they have to add some weird incantation to their aggregates to attain the full performance of the library types. I also don't want to have to go over my codebase, adding default cting-dtor to my aggregates; when move constructors were added for C++11, user-defined aggregates benefited even if they were originally written under C++03.

Finally, requiring explicit opt-in is dangerous! If a class evolves to become location-aware then the maintainer adding the move ctor may well miss the presence of the defaulted cting-dtor. If the implicit behavior is OTOH dependent on the presence/absence of the other special functions then it will gracefully and automatically change from memberwise destructive move to move ctor + dtor, as the added move ctor suppresses the implicit (memberwise destructive) cting-dtor.


--
Matthew

Matthew Woehlke

unread,
Aug 7, 2015, 3:14:59 PM8/7/15
to std-pr...@isocpp.org
On 2015-08-07 14:30, Edward Catmur wrote:
> If the ctor(s) and dtor are non-user-defined (defaulted or declared as
> defaulted) then the user has chosen to defer responsibility for copy/move
> operations to the bases and members, so why not let the bases and members
> decide? There's no user-written code being skipped.

Hmm... well, let's write it out. We're talking about e.g. this case:

class Foo { public: Foo(Foo&&); ~Foo(); };
struct Bar { Foo foo1; Foo foo2; }

So what does the implicit cting-dtor do here?

// option 1 (my suggestion)
Bar::~Bar(Bar* new_bar)
{
new (&new_bar->foo1) Foo{this->foo1};
new (&new_bar->foo2) Foo{this->foo2};
this->foo1.~Foo();
this->foo2.~Foo();
}

// option 2 (your suggestion)
Bar::~Bar(Bar* new_bar)
{
this->foo1->~Foo(&new_bar->foo1);
this->foo2->~Foo(&new_bar->foo2);

/* ...which often is equivalent to this:
new (&new_bar->foo1) Foo{this->foo1};
this->foo1.~Foo();
new (&new_bar->foo2) Foo{this->foo2};
this->foo2.~Foo();
*/
}

Well... okay; "often" may be the key word there. When it isn't, you *do*
end up paying more. OTOH, your way means a potential behavior change for
existing classes. (Yes, the whole feature is technically a behavior
change - except in the non-optimized cases - but in the trivial case,
there should be no *observable* change.)

Still... I am potentially convinced :-).

> Finally, requiring explicit opt-in is dangerous!

I still assert that if you *have* a user-defined copy/move ctor or dtor,
you must opt in to the cting-dtor doing other than copy/move plus
destroy of your class. Do we agree on that point?

Anyway, my intent was that one would not declare an explicitly
"defaulted" (or whatever ends up being used) cting-dtor unless the type
already has a non-default copy/move ctor and/or non-default regular
dtor. (And in that case, if you change the class without noticing the
cting-dtor, you are up a creek with either of our approaches :-).)

> If the implicit behavior is OTOH dependent on the presence/absence of
> the other special functions [...]

I never said it wasn't :-). Even with my previous suggestion, I said
that if the members' cting-dtors are all trivial, the aggregate's is
also trivial. The only case we disagreed on was when some member has a
non-trivial cting-dtor *but* the class has default copy/move ctor and
default regular dtor.

(So in either case, anything that e.g. looks like a C struct would
become trivially relocatable.)

--
Matthew

isocp...@denisbider.com

unread,
Aug 7, 2015, 4:47:13 PM8/7/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
Hello everyone,

I believe the destructive move Jesus is upon us!

 I have uploaded the following draft:



@Matthew:

> What if doing a memcpy is *wrong*? Now you've wasted a
> bunch of work copying data that isn't useful.

True. I have addressed this in the above draft.


@Edward:

> How does that deal with composition?

Same way constructors deal with composition. I invite you to check out the above draft.


@Matthew:

> IMO the implicit behavior should definitely be the boring behavior,
> i.e. invoke a copy/move ctor and the regular dtor.

I disagree with this very passionately. This is a similar kind of default-bundling as getting IE with Windows.

Move destructor invoking copy/move ctor:

- Prevents the move destructor from being able to always provide the noexcept guarantee.

- Takes choice away from the user. The user may want to use destructive move if it's available; but otherwise, something else.

- Provides trifling convenience than can be trivially provided with a template along the lines of move_if_noexcept.


If you check out the draft, feedback would be appreciated.

Matthew Woehlke

unread,
Aug 7, 2015, 5:32:51 PM8/7/15
to std-pr...@isocpp.org
On 2015-08-07 16:47, isocp...@denisbider.com wrote:
> I believe the destructive move Jesus is upon us!
>
> I have uploaded the following draft:
>
> http://www.denisbider.com/MoveDestructor.pdf

Editorial comments:

- Might want to explain "destructive move" in the opening paragraph.

- Might want to mention std::unique_ptr? This is a really simple example
of a class that is trivially relocatable but has an "interesting" move
constructor and "interesting" destructor.


Nits:

- In the defaulted example, s.b. ~int(int&). That is, define the default
behavior as cting-dtor for ALL members, and explain elsewhere when a
cting-dtor is trivial (i.e. for POD types; the "consists of trivially
relocatable members" case seems sufficiently covered) and that trivial
means to just copy the bits. (Also, it's expected that the compiler will
combine trivial cting-dtors of adjacent members into a single memcpy.)
At least, I'd do that... seems cleaner. (Shouldn't actually change
anything.)


Not-so-nits:

- The multiple object helper should work on overlapping memory. I
imagine you intended this, but I don't see any wording to that effect;
it seems to me that such wording may be important.

- Should(n't) the cting-dtor be implicit-default if the class has a) a
*default* move ctor *or copy ctor*, *and* b) a default dtor, whether or
not such are declared?

- I feel fairly strongly that there should be a cutoff point for which
we can use the same syntax to relocate objects that can't be simply
relocated. I proposed that there is *always* a cting-dtor (except for
objects that explicitly delete it, or can't be copied/moved and/or
destroyed at all, i.e. the fallback implementation would be ill-formed),
but at any rate, I think the helpers should work regardless.

- I'm not entirely confident that the composition case is solved. As
worded, the class members are relocated after the base class has been
destroyed (moveover, the derived class itself is technically "destroyed"
after the base class has been destroyed), which seems like it could
present problems... :-( Unfortunately I don't have any suggestions here.
I do hope that this won't kill the proposal though.


Comments:

- It took me a minute or two to understand the invocation helpers, but
now that I do, I like! IIUC what these mean is that instead of libraries
having to check a type trait and write a memmove themselves, the
compiler will do so. Cool!


Bikesheds (JFTR):

- ~A(A&) vs. ~A(A*) vs. ~A(void*).

- "cting-dtor" vs. "move-dtor" ;-).

- Type trait names.


Thanks for sticking with this!

--
Matthew

isocp...@denisbider.com

unread,
Aug 7, 2015, 5:51:41 PM8/7/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
Great responses! Thank you!

I urgently have to sleep, but I've had the following idea:

Do you think we should approach this from the opposite direction - a destructing constructor?

I went with constructing destructor because it's easy to extend syntax for it in an unambiguous way. However,

If we're willing to introduce an explicit reference type for xvalues, we could have this, which could arguably be more general:

struct A : B {
  A(A~& x) : B(x), c(std::xref(x.c)), i(x.i) {}
  C c;
  int i;
};


Pros:

- everything is like move constructor - except this takes xvalue reference instead of rvalue reference, and relies on std::xref instead of std::move

- fully recognizes xvalues, gives them a reference type (~&)

- naturally interacts with copy/move constructors

- allows for xvalue assignment operator - maybe useful for returns from functions when assigned to pre-existing object?

Cons:

- fully recognizes xvalues - yet another reference type...

- naturally interacts with copy/move constructors - easy to slip up and forget std::xref, resulting in a deep copy

- allows for xvalue assignment operator - yet another special member...

Thoughts?

isocp...@denisbider.com

unread,
Aug 7, 2015, 5:54:27 PM8/7/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com, isocp...@denisbider.com
> - naturally interacts with copy/move constructors -
> easy to slip up and forget std::xref, resulting in a deep copy

And there we go, I did exactly that in the example:


  A(A~& x) : B(x), c(std::xref(x.c)), i(x.i) {}


Should be:


  A(A~& x) : B(std::xref(x)), c(std::xref(x.c)), i(x.i) {}


Arguably, we have the same problem with move constructors...

And arguably - it might be better if we didn't... :)

isocp...@denisbider.com

unread,
Aug 8, 2015, 4:37:38 AM8/8/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
I have published an updated draft:


I have consistently renamed move destructor -> relocator and move destruction -> relocation. Reason is explained in a new section, Rationales, on last page.


Comments:

> Might want to explain "destructive move"
> in the opening paragraph.

Very good, done.


> - Might want to mention std::unique_ptr?
> This is a really simple example  of a class that is
> trivially relocatable but has an "interesting" move
> constructor and "interesting" destructor.

I've spent a fair amount of time thinking about this, but I cannot think of a container scenario involving unique_ptr that's worse than string. unique_ptr already has nothrow move construction; and has nothrow destruction as long as the object has nothrow destruction; and in any case, it will not throw after a move. I'm having some trouble conjuring a situation where the lack of relocation for unique_ptr does not result in strictly a performance penalty for having to execute a non-trivial destructor.

Did you have a specific scenario involving unique_ptr in mind?


> In the defaulted example, s.b. ~int(int&).

Good point, done that.


> explain elsewhere when a cting-dtor is trivial

Edited text where trait types are defined in an attempt to better explain this.


> The multiple object helper should work
> on overlapping memory.

It should indeed. :) Fixed!


> Should(n't) the cting-dtor be implicit-default if the
> class has a) a *default* move ctor *or copy ctor*,
> *and* b) a default dtor, whether or not such are declared?

Hmm. I wouldn't necessarily mind, but current rules for move constructor wouldn't implicitly declare one if a copy constructor or destructor, or any other special member, is user-declared. The way I read 12.8, para 9, the following:

struct A {
  A(A const&) = default;
};

would result in a move constructor NOT being implicitly declared.

As far as I can test right now, this is the case. If A inherits from B, which implements both move and copy construction, I can observe that trying to move A actually results in copy construction of B. I need add:

struct A : B {
  A(A const&) = default;
  A(A&&) = default;
};

... in order for B(B&&) to be called.

For consistency as well as safety, it would make sense to me to use the same rules for the relocator.


> I feel fairly strongly that there should be a cutoff point
> for which we can use the same syntax to relocate
> objects that can't be simply relocated.

I agree, but I think it should be at a level above this. I have added a discussion in the Rationales section.


> I'm not entirely confident that the composition case is
> solved. As worded, the class members are relocated
> after the base class has been destroyed

The relocator performs the tasks of both a constructor and a destructor, so one of the two orders have to be chosen. I have added a section for this in the Rationales section.

I hope also that calling the special member a "relocator"; expressing its duality and impartiality, rather than a bias toward construction or destruction; might also help alleviate this concern.



On Friday, August 7, 2015 at 3:32:51 PM UTC-6, Matthew Woehlke wrote:

Vitali Lovich

unread,
Aug 8, 2015, 6:27:16 PM8/8/15
to std-pr...@isocpp.org, mwoehlk...@gmail.com
I like many things about this proposal & thank you for trying to tackle this.  Apologies if already covered, but can you clarify why there is a distinction between the multiple & single relocators?  Am I correct in guessing that it’s to try & avoid the loop overhead when you only have a single object?  Some other reason?

The “composition” section seems to read kind of contradictory to the terminology being used to frame the solution.  If construction order is indeed more appropriate (which I think you are correct on), doesn’t it speak that this is more naturally thought of as a destructive move constructor?  Beyond just semantics, I think users writing their own may easily get confused about which order they need to destroy/construct & bugs in this area will be very subtle, hard-to-find & particularly nasty.

nitpicks:

The static keyword preceding the function A::~A seems out-of-place no?  I could be mistaken, but the static keyword is illegal in this context & can only be there on free-standing functions or on functions as part of the class declaration; it cannot be used for external class function definition.  Is the static keyword in this place new to this proposal or C++17?

While discussion relocation, there is what amounts to a footnote mentioning realloc.  Is it correct that this is the language proposal that would complement any eventual library features that would enable the use of realloc within containers?  I think that could do with some clarification to properly frame the discussion; it sounds like this is trying to solve the memcpy problem but it seems like it solves both that & realloc, no?

I’m not very familiar with the standardese terminology, so apologies for the silly question, but In the "Implicit relocator” section, does "user-declared” also cover declarations that are "= default”?

Thanks,
Vitali


--

---
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.

isocp...@denisbider.com

unread,
Aug 9, 2015, 6:57:09 AM8/9/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
> If construction order is indeed more appropriate (which I
> think you are correct on), doesn’t it speak that this is
> more naturally thought of as a destructive move constructor?

Yes, I think you're very right!

I have updated the proposal with an annex that presents an alternate constructor-like syntax, and discusses the merits and demerits of the two approaches:


It seems to me that the constructor-like approach may in fact be superior; no more complex, yet more intuitive; and potentially more extensible, if needed.

If folks like this, I'm leaning to promote constructor-like syntax from the annex, and place it in the center of the proposal.


> distinction between the multiple & single relocators?

Yes - it's just a formal distinction to emphasize cost-less optimization of the single-object case. I have added to the text to make this clearer.


> The static keyword preceding the function A::~A
> seems out-of-place no?

It is illegal in the definition, yes. It's okay in the declaration.

I have changed the definition to put "static" into C-style comments. I'm still keeping it, to make it clear that the wrappers are invoked as static members.


> it sounds like this is trying to solve the memcpy problem
> but it seems like it solves both that & realloc, no?

To properly solve the realloc problem, it would be necessary to have the equivalent of realloc_in_place.

If realloc_in_place or equivalent fails:

- The equivalent of malloc needs to be used to allocate new memory, while keeping old memory.

- The static relocator needs to be called to move objects from old location to new. This could be as trivial as memcpy, or may require fix-ups.

- The old memory is freed.

In the absence of realloc_in_place; and for trivially relocatable objects only; it would work for a container to call realloc, also.

To permit use with realloc with non-trivially relocatable objects, the relocator would have to:

- use constructor-like syntax (the old location is gone when it is called);

- accept a ptrdiff_t instead of reference to the old location.

The ptrdiff_t would be the offset between the old location and the new one, and would allow the relocator to perform any patch-ups. 

The drawback of this approach is that, if you have a small object; e.g. libc++ std::string, which is just one pointer in size; and this object requires a patch-up during relocation; it's twice as costly to execute realloc + patch-up, than to execute a relocator that performs the copy as part of its process.

Basically, the lack of a realloc_in_place or equivalent is the deficiency. A feature such as relocation should not be designed around a library deficiency that should relatively trivial to solve.


> does "user-declared” also cover declarations that are "= default”?

Yes, it does. For example, if you have this:

  struct B {
    B(B const&) { ... }
    B(B&&) { ... }
  };

  struct A : B {
    A(A const&) = default;
  };

The user declaration of A(A const&) - even as default - blocks an implicit declaration of a move constructor for A. To get an implicitly defined move constructor, one must use either this:

  struct A : B {
  };

or this:

  struct A : B {
    A(A const&) = default;
    A(A&&) = default;
  };

isocp...@denisbider.com

unread,
Aug 9, 2015, 8:00:49 AM8/9/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com, isocp...@denisbider.com
In fact - I am sufficiently convinced that constructor-like syntax is more intuitive, that I have gone ahead and converted the proposal. It now puts constructor-like syntax front and center.

Just in case, I still have the previous version. But I think this will be more intuitive.

jgot...@gmail.com

unread,
Aug 9, 2015, 9:14:19 PM8/9/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com, isocp...@denisbider.com
Very nice, but I have one question.  How do you handle member variables that might or might not be self-references?  For instance, in your example, if the variable a is guaranteed to be either equal to this or a pointer to an object allocated with new, how would you write the relocator function?  Most other members of class A could have code like

if (a == this) {
   
//do something
} else {
   
//do something else
}

but in the case of the relocator function, by the time we get to the a variable half the object has been relocated and it's not at all obvious what needs to be tested.

Joe Gottman

Nicol Bolas

unread,
Aug 9, 2015, 9:58:39 PM8/9/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com, isocp...@denisbider.com, jgot...@gmail.com


On Sunday, August 9, 2015 at 9:14:19 PM UTC-4, jgot...@gmail.com wrote:
Very nice, but I have one question.  How do you handle member variables that might or might not be self-references?

As I understand it, that's quite simple.

If you have a class that potentially contains a self-reference, then there are certain things you must do in order for it to be functional. Such a class must have a non-trivial copy constructor/assignment operator; if you user-define it, the copy's pointer must now refer to the copy (you could also delete it). Such a class must have a non-trivial move constructor/assignment operator; if you user-define it, the moved object's pointer must now refer to the target of the move.

In short: even before this proposal was enacted, if such a class was to be functional, it could never have been trivially copyable or trivially moveable. And by inference, it cannot under this proposal be trivially relocatable. So such a class cannot be memcpy'd or realloc'd. It can still undergo the process of relocation, but it cannot be trivial relocation. So you get the benefit of a combined move+destructor. But that's all.

Edward Catmur

unread,
Aug 10, 2015, 9:40:20 AM8/10/15
to std-pr...@isocpp.org
On Fri, Aug 7, 2015 at 8:14 PM, Matthew Woehlke <mwoehlk...@gmail.com> wrote:
I still assert that if you *have* a user-defined copy/move ctor or dtor,
you must opt in to the cting-dtor doing other than copy/move plus
destroy of your class. Do we agree on that point?

Absolutely.
 
Anyway, my intent was that one would not declare an explicitly
"defaulted" (or whatever ends up being used) cting-dtor unless the type
already has a non-default copy/move ctor and/or non-default regular
dtor. (And in that case, if you change the class without noticing the
cting-dtor, you are up a creek with either of our approaches :-).)

> If the implicit behavior is OTOH dependent on the presence/absence of
> the other special functions [...]

I never said it wasn't :-). Even with my previous suggestion, I said
that if the members' cting-dtors are all trivial, the aggregate's is
also trivial. The only case we disagreed on was when some member has a
non-trivial cting-dtor *but* the class has default copy/move ctor and
default regular dtor.

(So in either case, anything that e.g. looks like a C struct would
become trivially relocatable.)

Sounds great, thanks for working through this with me. 

Matthew Woehlke

unread,
Aug 10, 2015, 10:05:17 AM8/10/15
to std-pr...@isocpp.org
On 2015-08-07 17:51, isocp...@denisbider.com wrote:
> Great responses! Thank you!
>
> I urgently have to sleep, but I've had the following idea:
>
> Do you think we should approach this from the opposite direction - a
> destructing constructor?

No. IIRC that was actually the original idea, and I'd also thought on it
a good bit, but...

Being able to pass an object destructively, to a ctor or otherwise,
feels like it would be a really useful feature. However, the problem I
keep running into when going down that train of thought is how on earth
the compiler is supposed to track object lifetimes in order to know to
not destroy the object again. (There's also the problem that a
destructed parameter needs to be destroyed somehow, and likely not in
the usual manner.)

Consider, for example:

void* pfoo = allocate_storage<Foo>();
if (expr) // assume 'expr' is something that changes at runtime
new (pfoo) Foo(std::destruct(old_foo));
else
new (pfoo) Foo{};

I suppose this already comes up with explicit calls to dtors, so the
compiler in theory does already know how to deal with it(?), but it
still makes me twitchy. The advantage of a cting-dtor/move-dtor is that
it a) should more directly leverage such existing support, and b)
clearly looks like a dtor, i.e. someone reading the code is less likely
to overlook that an object was destroyed.

Anyway, that's my take... I wouldn't say I'm firmly opposed to it, just
that I thought about it and it seemed that a cting-dtor was preferable.
(There's also no¹ syntax change, vs. introducing a 'T~'/'T~&'/etc..)

(¹ ...depending on if dtors are precisely specified syntactically to
take no parameters.)

--
Matthew

Matthew Woehlke

unread,
Aug 10, 2015, 10:29:32 AM8/10/15
to std-pr...@isocpp.org
(Note: just replying to comments for now; haven't yet read the updated
draft.)

On 2015-08-08 04:37, isocp...@denisbider.com wrote:
>> - Might want to mention std::unique_ptr?
>> This is a really simple example of a class that is
>> trivially relocatable but has an "interesting" move
>> constructor and "interesting" destructor.
>
> I've spent a fair amount of time thinking about this, but I cannot think of
> a container scenario involving unique_ptr that's worse than string.

Certainly, and that's sort of my point; std::string is a pathological
case; it is NEVER trivially relocatable. unique_ptr is an example of the
opposite; a class with "important" move ctor and dtor, but that *is*
trivially relocatable.

Basically, there are four classes of object, for this purpose:

- C-like types (trivial ctors, dtor, trivially relocatable)
- "Interesting" but trivially relocatable (std::unique_ptr)
- Relocatable with repair (std::string)
- Not (practically) relocatable

> unique_ptr already has nothrow move construction; and has nothrow
> destruction as long as the object has nothrow destruction; and in any case,
> it will not throw after a move. I'm having some trouble conjuring a
> situation where the lack of relocation for unique_ptr does not result in
> strictly a performance penalty for having to execute a non-trivial
> destructor.

I... think that's my point? It's not that std::unique_ptr is worse than
std::string... it's *better*, because it's trivially relocatable.
Therefore it makes a good poster child for how relocatability is
extremely useful in trivial cases (which can be reduced entirely to
memmove).

Basically, what I meant is that I would cover (i.e. call out
specifically) the first three cases above, at least with brief mentions.
The first case, we hopefully get implicit relocatability and a huge win
because we can use a straight up memmove with no looping (besides the
memmove itself). The first case we have to explicitly declare trivial
relocatability, but we get the same huge win (more so, even, because the
ctor and/or dtor was non-trivial). The third case needs to be handled
more carefully, and that's the case you've covered thoroughly already.

So, really, that's all I was getting at... just that it seems it would
strengthen the case to give a little more mention to the easy examples
also and how we get an easy but major win from those.

Hope that makes sense...

>> Should(n't) the cting-dtor be implicit-default if the
>> class has a) a *default* move ctor *or copy ctor*,
>> *and* b) a default dtor, whether or not such are declared?
>
> Hmm. I wouldn't necessarily mind, but current rules for move constructor
> wouldn't implicitly declare one if a copy constructor or destructor, or
> any other special member, is user-declared. The way I read 12.8, para 9,
> the following:
>
> struct A {
> A(A const&) = default;
> };
>
> would result in a move constructor NOT being implicitly declared.

Truuuuuue... see however my comment about libraries being able to use
this for any type that is relocatable - whether or not that has to fall
back on (separate) move-construct and destroy - without having to check
a type trait. It's mostly for that reason that I think you should always
have a cting-dtor unless it is suppressed either explicitly, or by there
being no sensible implicit definition possible.

--
Matthew

isocp...@denisbider.com

unread,
Aug 10, 2015, 12:00:02 PM8/10/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
Matthew:

> I wouldn't say I'm firmly opposed to it, just that I
> thought about it and it seemed that a cting-dtor
> was preferable.  (There's also no¹ syntax change,
> vs. introducing a 'T~'/'T~&'/etc..)

After studying the syntax of ~T(T&), which operates on old object, and takes new object as parameter; and >>T(T&) - which is identical in function, but takes old object as parameter; it seems to me that implementation of >>T(T&) is much more intuitive.

With the invocation I propose, I think that would look intuitive also:

T* oldArray;
void* newStorage = allocate_storage<T>(n);
T* newArray = T::>>T(newStorage, oldArray, n);

It even has a built-in arrow, showing that things are going from T to T... :)


> Certainly, and that's sort of my point; std::string
> is a pathological case; it is NEVER trivially relocatable.

Ah, gotcha!

It seems like the misunderstanding was in our different assumptions.

From my point of view (VS), what I usually see is a string that's 4 pointer types in size, and is trivially relocatable. A string with SSO is the weird case.

From your point of view (libc++), you always see a string that's 1 pointer type in size, and has SSO. A string without SSO is the weird case.

I have edited a sentence in the draft to recognize this:


"For example, fix-ups are needed by std::string in libc++, which uses self-references for short string optimization; yet they are not needed by std::unique_ptr, or std::string in Visual Studio."

Matthew Woehlke

unread,
Aug 10, 2015, 12:21:58 PM8/10/15
to std-pr...@isocpp.org
On 2015-08-10 12:00, isocp...@denisbider.com wrote:
>> Certainly, and that's sort of my point; std::string
>> is a pathological case; it is NEVER trivially relocatable.
>
> Ah, gotcha!
>
> It seems like the misunderstanding was in our different assumptions.
>
> From my point of view (VS), what I usually see is a string that's 4 pointer
> types in size, and is trivially relocatable. A string with SSO is the weird
> case.
>
> From your point of view (libc++), you always see a string that's 1 pointer
> type in size, and has SSO. A string without SSO is the weird case.

Ah! Yes, that would be it. I was assuming you were using (e.g.
libstdc++) std::string as an example where memcpy (esp. large block
memcpy of a vector of adjacent objects) can *help*, but the object still
needs to be corrected after, and not as a trivially relocatable object.

--
Matthew

Thiago Macieira

unread,
Aug 10, 2015, 12:22:28 PM8/10/15
to std-pr...@isocpp.org
On Monday 10 August 2015 09:00:02 isocp...@denisbider.com wrote:
> "For example, fix-ups are needed by std::string in libc++, which uses
> self-references for short string optimization; yet they are not needed by
> std::unique_ptr, or std::string in Visual Studio."

That's libstdc++, not libc++.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

isocp...@denisbider.com

unread,
Aug 10, 2015, 1:45:46 PM8/10/15
to ISO C++ Standard - Future Proposals
Thanks! Fixed. I appreciate the correction.

Vitali Lovich

unread,
Aug 10, 2015, 3:52:32 PM8/10/15
to std-pr...@isocpp.org, mwoehlk...@gmail.com


On Aug 9, 2015, at 3:57 AM, isocp...@denisbider.com wrote:

> If construction order is indeed more appropriate (which I
> think you are correct on), doesn’t it speak that this is
> more naturally thought of as a destructive move constructor?

Yes, I think you're very right!

I have updated the proposal with an annex that presents an alternate constructor-like syntax, and discusses the merits and demerits of the two approaches:


It seems to me that the constructor-like approach may in fact be superior; no more complex, yet more intuitive; and potentially more extensible, if needed.

If folks like this, I'm leaning to promote constructor-like syntax from the annex, and place it in the center of the proposal.


> distinction between the multiple & single relocators?

Yes - it's just a formal distinction to emphasize cost-less optimization of the single-object case. I have added to the text to make this clearer.
I misread the part where it says these can only be generated by the compiler so I thought people would have to write them some times.  From the call-site, distinguishing the one/many case makes things more legible.

> The static keyword preceding the function A::~A
> seems out-of-place no?

It is illegal in the definition, yes. It's okay in the declaration.
I have changed the definition to put "static" into C-style comments. I'm still keeping it, to make it clear that the wrappers are invoked as static members.
Yup.  I was just thrown by the static keyword on the definition.


> it sounds like this is trying to solve the memcpy problem
> but it seems like it solves both that & realloc, no?

To properly solve the realloc problem, it would be necessary to have the equivalent of realloc_in_place.

If realloc_in_place or equivalent fails:

- The equivalent of malloc needs to be used to allocate new memory, while keeping old memory.

- The static relocator needs to be called to move objects from old location to new. This could be as trivial as memcpy, or may require fix-ups.

- The old memory is freed.

In the absence of realloc_in_place; and for trivially relocatable objects only; it would work for a container to call realloc, also.

To permit use with realloc with non-trivially relocatable objects, the relocator would have to:

- use constructor-like syntax (the old location is gone when it is called);

- accept a ptrdiff_t instead of reference to the old location.

The ptrdiff_t would be the offset between the old location and the new one, and would allow the relocator to perform any patch-ups. 

The drawback of this approach is that, if you have a small object; e.g. libc++ std::string, which is just one pointer in size; and this object requires a patch-up during relocation; it's twice as costly to execute realloc + patch-up, than to execute a relocator that performs the copy as part of its process.
Probably off-topic, but can you expand on this point?  Why 2x?  For instance, realloc frequently will have a ptrdiff of 0 which can be optimized out, no?  Especially for larger regions of memory where all the OS needs to do is tack on an extra VM page which is when you'd get the most benefit from realloc.  Even if there is an offset, surely realloc + patch up can't be more costly than malloc+memcpy + relocate since patch necessarily is a simpler operation and you avoid a memcpy (or worse if you have non-trivial relocation).

That being said, I'm not sure you could even do the patch-up for something that contains a self-reference without invoking undefined behavior...

Basically, the lack of a realloc_in_place or equivalent is the deficiency. A feature such as relocation should not be designed around a library deficiency that should relatively trivial to solve.
Well, as you point out, realloc requires the object to patch itself which can't be done as a library-level feature, can it?  I guess it could have a non-special function that the library calls to "fixup" an object but that feels dangerous.  Anyway, if realloc is a different problem, does it make sense to put in clarifying wording about what specific problem this is trying to solve and perhaps mention the story about realloc more clearly?  I'm obviously late to the discussion, am missing context and haven't reread it in a few days, so if you feel that's already covered in the proposal, feel free to ignore.

Edward Catmur

unread,
Aug 10, 2015, 4:20:28 PM8/10/15
to ISO C++ Standard - Future Proposals, isocp...@denisbider.com
A few queries:

What exactly are the semantics of the mem-initializer-list, particularly with regard to omitted, duplicated or misplaced bases/members:

struct A {
    std
::string q, r, s;
   
>>A(A& x) : >>r(x.r), >>s(x.r) { /* oops */ }
};

Is it ever legitimate for a mem-initializer to be omitted or to take any form other than >>id(x.id) where the two identifiers are equivalent? If so, whose responsibility is it to call the destructor on the corresponding member of the source object? If not, would it be better to omit the mem-initializer-list entirely, as with destructors?

Within the body of the relocator special member function, what are the lifetime statuses of the source and destination objects, particularly with regard to [class.cdtor]? Is it permissible to form lvalues to the bases and members of the source object, or is this undefined behavior as they are destructed? If the type is polymorphic, is it permissible to dynamic_cast or invoke virtual functions on the source? Indeed, is it permissible to invoke any member functions on the source?

In particular, I'd like to make this utility class legal (noting that aligned_storage_t is a POD but my utility class is not):

template<class T>
struct relocate_wrapper {
 
template<class... Args> relocate_wrapper(Args&&... args) {
   
new (&buf) T(std::forward<Args>(args)...);
 
}
 
~relocate_wrapper() { reinterpret_cast<T&>(buf).~T(); }
 
>>relocate_wrapper(relocate_wrapper& rhs) {
   
new (&buf) T(reinterpret_cast<T&&>(rhs.buf));
   
reinterpret_cast<T&>(rhs.buf).~T();
 
}
  std
::aligned_storage_t<sizeof(T), alignof(T)> buf;
};

Hm... it might be nice to ensure that the relocator doesn't do any unnecessary work copying the buffer that's just going to be placement new moved into. Is that an argument against an implicit mem-initializer-list?

isocp...@denisbider.com

unread,
Aug 10, 2015, 4:42:01 PM8/10/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
With regard to realloc - it boils down to that the following combination:

(1)
- copying relocator with signature >>T(T&)
- logic: if (!realloc_in_place) { malloc, copying_relocator, free }

is strictly at least as efficient, or more efficient than:

(2)
- patching relocator with signature >>T(ptrdiff_t)
- logic: if (realloc(p) != p) { patching_relocator }

under the reasonable assumption that realloc_in_place is a strict subset of realloc, like this:

realloc = if (!realloc_in_place) { malloc, memcpy, free }

The reason is that the first strategy boils down to:

if (!realloc_in_place) { malloc, copying_relocator, free }

Whereas the second strategy boils down to:

if (!realloc_in_place) { malloc, memcpy, free, patching_relocator }

You seem to be assuming that we have realloc, but we don't have realloc_in_place.

However, it seems reasonable to me that if we can add support for relocation, we can also add the equivalent of realloc_in_place.

And if we do add relocation, but do not add an equivalent of realloc_in_place - which would be kinda sad, but okay - we can nevertheless use platform-specific functionality that provides the equivalent of realloc_in_place.

For example, on Windows, I have:

template <class T>

bool ReAllocInPlace (T* p, size_t n) noexcept

   { return HeapReAlloc(s_processHeap, HEAP_REALLOC_IN_PLACE_ONLY, p, n*sizeof(T)) != nullptr; }

isocp...@denisbider.com

unread,
Aug 10, 2015, 5:59:07 PM8/10/15
to ISO C++ Standard - Future Proposals, isocp...@denisbider.com
Hmm, good points.

There are definitely situations where you don't want to mem-initialize a trivial member, such as in the example:

    >>A(A& x) : >>B(x), >>c(x.c), >>i(x.i)

      { a = this; }


However, one might definitely argue that situations where you want to mem-initialize a base or a member, but with a non-corresponding member, are practically nonexistent. So, we may in fact prefer the following:

    >>A(A& x) : >>B, >>c, >>i

      { a = this; }


The relocate_wrapper you suggest I think looks neat, and should be legal. I don't see a reason why it wouldn't be legal, if the mem-initializer-list is not implicit.

It seems to me, intuitively, that there's no reason to make it illegal to access any data member of the moved-from subobject until the outermost relocator exits.

I see no obvious reason why virtual methods shouldn't continue to work, as well. But this is not to say, of course, that one should call them.

In general, anything that's const ought to be safe to call. Which is an argument in favor of making the signature >>T(T const&) - except that would prevent zeroing out of members.

isocp...@denisbider.com

unread,
Aug 10, 2015, 6:17:01 PM8/10/15
to ISO C++ Standard - Future Proposals, isocp...@denisbider.com
However, if there's real desire for the wrapper you show to be legal, a better way to do it would probably be to allow this:

 

  >>A(A& x) : B(std::move(x)), >>c(x.c), >>i(x.i) { a = this; }

 


But this means we:

- say goodbye to the noexcept guarantee;

- define the point when ~B is called in the original location - probably when >>A returns;

- introduce another type trait, is_nothrow_relocatable

I would have preferred to avoid this, but - I guess, if people really want to merge move and copy construction into relocation... (shudder :)

isocp...@denisbider.com

unread,
Aug 10, 2015, 6:45:58 PM8/10/15
to ISO C++ Standard - Future Proposals, isocp...@denisbider.com
Actually, if we want to give the implementer this much control, there's no reason we shouldn't put the burden of calling ~B on the implementer.

I thought this would be a problem if ~B is virtual, but it turns out the following is legal:

 

struct A     { virtual ~A() { cout << "~A()" << endl; } };

struct B : A { virtual ~B() { cout << "~B()" << endl; } };

struct C : B { virtual ~C() { cout << "~C()" << endl; } };

 

void DestroySubobject(B* b) { b->B::~B(); }

 


It turns out, this calls ~A and ~B, but not ~C.

It seems to me it follows: if we allow >>A to initialize the subobjects in whatever fashion; including with copy or move construction; then it would make sense to expect the author of >>A to call B::~B.

Thiago Macieira

unread,
Aug 10, 2015, 7:17:46 PM8/10/15
to std-pr...@isocpp.org
On Monday 10 August 2015 09:00:02 isocp...@denisbider.com wrote:
> T* newArray = T::>>T(newStorage, oldArray, n);

I don't think this should be a built-in function. Instead, the array move
should be an external, template function in the std namespace.

T *newArray = std::relocate(newStorage, oldArray, n);

Internally, it should just DTRT and either:
1) memcpy / memmove if T is trivially movable and trivially destructible
2) memcpy / memmove OR call the relocating constructor for the array
[The effect should be the same, so it's a QoI detail how it's implemented]
3) call the move constructor + destructor

I'd also be ok if #3 weren't implemented and caused a compilation error.

Edward Catmur

unread,
Aug 11, 2015, 4:35:15 AM8/11/15
to std-pr...@isocpp.org
Right; so it's the implementer's responsibility to ensure that all source subobjects with non-trivial destructor are destructed, either by deferring to the subobject relocator or by calling the destructor on the subobject. So we end up writing:

  >>A(A& x) : B(std::move(x)), >>c(x.c)>>i(x.i) x.B::~B(); = this; }


There's a highly compelling reason to allow arbitrary mem-initializers: reference members. These have to be initialized within the mem-initializer-list, as they can't be reseated within the body of the relocator:

struct A { int i; int& r = i; >>A(A& x) : >>i, r(i) {} };

With regard to noexcept, I think this has to be kept; otherwise you have to be able to rollback the relocation which might not be feasible (e.g. if pointers elsewhere have already been updated to point to relocated objects or subobjects); or destruct the source object which would also be very difficult and would rarely be the desired behavior.

--

---
You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-proposals/f1zaOjyUem0/unsubscribe.
To unsubscribe from this group and all its topics, send an email to std-proposal...@isocpp.org.

isocp...@denisbider.com

unread,
Aug 11, 2015, 7:05:57 AM8/11/15
to ISO C++ Standard - Future Proposals
You're probably right. I was thinking a static T::>>T could be an anchor for static analysis tools; but I see no reason an std::relocate cannot also serve this purpose.

I have modified the proposal to make the wrapper a library function.

I still believe each facility should do one thing, so I do not propose bundling of copy/move construction into std::relocate. I propose that this be implemented by expanding std::move_if_noexcept, or an alternative such method.

isocp...@denisbider.com

unread,
Aug 11, 2015, 7:09:12 AM8/11/15
to ISO C++ Standard - Future Proposals
You're right. I have updated the proposal as follows:


The latest versions now also include an Annex discussing the need for in-place reallocation, as pointed out by Vitali Lovich.
...

Matthew Woehlke

unread,
Aug 11, 2015, 10:22:44 AM8/11/15
to std-pr...@isocpp.org
On 2015-08-08 04:37, isocp...@denisbider.com wrote:
> I have published an updated draft:
> http://www.denisbider.com/Relocator.pdf
>
>> In the defaulted example, s.b. ~int(int&).
>
> Good point, done that.

Thanks!

>> The multiple object helper should work
>> on overlapping memory.
>
> It should indeed. :) Fixed!

Thanks!

>> Should(n't) the cting-dtor be implicit-default if the
>> class has a) a *default* move ctor *or copy ctor*,
>> *and* b) a default dtor, whether or not such are declared?
>
> Hmm. I wouldn't necessarily mind, but [...]
>
> For consistency as well as safety, it would make sense to me to use the
> same rules for the relocator.

To revisit this, my main "concern" is to not suppress relocatability for
classes that are trivially relocatable, merely because a copy/move ctor
or dtor was explicitly defaulted. IOW, relocation should always be
available if the default implementation is provably correct. Otherwise
we are penalizing existing code needlessly.


Er... on that note, I just realized, we probably should make it UB to
invoke the relocator on an object's base class outside of the relocator
itself doing

IOW, this is UB:

class Foo { public: virtual ~Foo(); ... };
class Bar : public Foo { ... };

auto pfoo1 = new Bar;
auto pfoo2 = (Foo*)malloc(sizeof(Foo));
pfoo1-> >>(*pfoo2);

I don't see that this should be a problem with the proposal, since the
main use case is containers where the final type is definitely known and
the same on both ends of the relocation, but it would be possible to
write code like the above, and I don't see offhand how such code could
be made to do anything reasonable.

>> I feel fairly strongly that there should be a cutoff point
>> for which we can use the same syntax to relocate
>> objects that can't be simply relocated.
>
> I agree, but I think it should be at a level above this. I have added a
> discussion in the Rationales section.

I can live with it, I guess. I'd feel better if the proposal also
specified such function, though. (std::relocate?)

>> I'm not entirely confident that the composition case is
>> solved. As worded, the class members are relocated
>> after the base class has been destroyed
>
> The relocator performs the tasks of both a constructor and a destructor, so
> one of the two orders have to be chosen. I have added a section for this in
> the Rationales section.
>
> I hope also that calling the special member a "relocator"; expressing its
> duality and impartiality, rather than a bias toward construction or
> destruction; might also help alleviate this concern.

The only "true" solution I can envision is that the old base is not
destroyed until the new object is fully constructed. This would conflict
with the notion of relocation destroying the (old copy of the) thing
relocated.

On the other hand, it's expected that relocation does not change the
actual bytes of the old object, so if relocation of the derived type
needs to read those, it can. (Which also makes me wonder if relocation
should be const w.r.t. the old object?)

Anyway, still just thinking out loud, really...



Okay, on to the new draft...

I'm not sure I agree that argument order for a cting-dtor is
counterintuitive :-). Basically, the question here is if we use push
order (source first) or pull order (destination first). You're using
pull order, which honestly, IMHO is counterintuitive :-). OTOH, memcpy
also uses pull order. Essentially this is the same as the AT&T vs. Intel
assembly dialect... as much a matter of personal taste as anything.

That said, I'd probably keep the static operator order as-is for
consistency with memcpy. The only other comment I'd make is that I feel
like there should be some indication that the source is destroyed by the
call. This is accomplished by cting-dtor, as you're calling a dtor. It's
also accomplished by e.g. 'T const~' (not sure if there should also be a
'&' there, or if '~' should imply by-reference...).

Pedantic: I want to say I've seen try_realloc more often than
realloc_in_place. Not terribly important however since AFAIK neither
exists yet.

Last, and not a comment on the paper as such, we really could use such a
function. Besides allowing us to work around using straight realloc (and
thus as mentioned, causing problems for static analysis tools), it may
be the case that a realloc is less efficient. As I just pointed out on
the Qt list, if you are growing a container in order to do a middle
insertion, and cannot grow in place, it is more efficient to allocate
uninitialized memory and do a piecewise move (which we can do with the
proposal in a "correct" manner) of the portions before and after the
insertion point to their correct final locations.


p.s. FWIW I do prefer the type trait names 'is_[trivially_]relocatable' :-).

--
Matthew

Matthew Woehlke

unread,
Aug 11, 2015, 10:39:20 AM8/11/15
to std-pr...@isocpp.org
> On 2015-08-08 18:27, Vitali Lovich wrote:
>> can you clarify why there is a distinction between the multiple &
>> single relocators? Am I correct in guessing that it’s to try & avoid
>> the loop overhead when you only have a single object?

That could be one reason. I'd also suggest that they should be different
for some of the same reasons that delete and delete[] are different.

Which brings up another point... cosmetic, but the second form should
take an 'A[]' rather than an 'A*'. (Which is the same thing, I know,
hence why it's a cosmetic change, but writing it that way makes it more
clear that the argument is expected to be an array of objects.)

>> If construction order is indeed more appropriate (which I think you
>> are correct on), doesn’t it speak that this is more naturally thought
>> of as a destructive move constructor?

Riiiight, that reminds me of another comment I forgot. Namely, this
looks strange:

dt-> >>A(*s);

...because you are calling what looks like a member function on
something that hasn't been constructed yet. It seems it would "feel"
better if this was written as some form of placement new.

On 2015-08-09 06:57, isocp...@denisbider.com wrote:
> Yes, I think you're very right!
> [...]
> If folks like this, I'm leaning to promote constructor-like syntax from the
> annex, and place it in the center of the proposal.

This brings us back to destructive move :-). (And I'll reiterate my
comments on the argument being somehow marked.) Although... I'd consider
calling it instead a "relocating ctor". Same thing, but possibly better
terminology.

>> While discussion relocation, there is what amounts to a footnote
>> mentioning realloc. Is it correct that this is the language proposal
>> that would complement any eventual library features that would
>> enable the use of realloc within containers?

Yes, and see also my other e-mail.

>> it sounds like this is trying to solve the memcpy problem but it
>> seems like it solves both that & realloc, no?

Yes and no... it doesn't really solve realloc, because realloc doesn't
specify anything w.r.t. object lifetime. Again, see also my other
e-mail. What it *would* solve is being able to do something sensible and
maximally efficient *if* we had try_realloc / realloc_in_place.

--
Matthew

Matthew Woehlke

unread,
Aug 11, 2015, 10:45:08 AM8/11/15
to std-pr...@isocpp.org
On 2015-08-09 21:14, jgot...@gmail.com wrote:
> Very nice, but I have one question. How do you handle member variables
> that might or might not be self-references? For instance, in your example,
> if the variable a is guaranteed to be either equal to this or a pointer to
> an object allocated with new, how would you write the relocator function?
> Most other members of class A could have code like
>
> if (a == this) {
> //do something
> } else {
> //do something else
> }
>
> but in the case of the relocator function, by the time we get to the a
> variable half the object has been relocated and it's not at all obvious
> what needs to be tested.

You would compare to the old object location rather than 'this'.
Examples like these are why if we were to have a separate fix-up
operator (i.e. able to "mostly" construct the new objects via memcpy),
it would need the original object address and *possibly* also need for
that memory to be unmodified.

With a one-at-a-time relocation operator/ctor/whatever you don't run
into these sorts of problems, although it does penalize the case of such
classes somewhat.

--
Matthew

Matthew Woehlke

unread,
Aug 11, 2015, 11:04:55 AM8/11/15
to std-pr...@isocpp.org
On 2015-08-10 17:59, isocp...@denisbider.com wrote:
> However, one might definitely argue that situations where you want to
> mem-initialize a base or a member, but with a non-corresponding member, are
> practically nonexistent. So, we may in fact prefer the following:
>
> >>A(A& x) : >>B, >>c, >>i

I like that... but I might consider deferring it. Besides that it might
be contentious, if we allow this for relocation, it would be nice to
allow it for move/copy also at the same time.

IOW, for a copy/move/relocate ctor, these:

T(T /*...*/ other) : a /*...*/
T(T /*...*/ other) : >>a /*...*/

...are equivalent to these:

T(T /*...*/ other) : a{other.a} /*...*/
T(T /*...*/ other) : >>a(other.a) /*...*/

> It seems to me, intuitively, that there's no reason to make it illegal to
> access any data member of the moved-from subobject until the outermost
> relocator exits.

D'oh! Why didn't I think of that earlier? If we define relocation as
making the old object destroyed *when the OUTERMOST relocation exits*,
and *not* destroying the thing being relocated as soon as that thing's
relocation operation exits, this also solves the composition problems!

I think you've covered the ins and outs of how and why to allow
non-relocations. On that note, I'll just say that I agree with those.
(And in particular, only allow them if they are noexcept.)

It seems like this might also allow relaxing some of the inhibitions on
implicitly defined relocate-ctors?

--
Matthew

isocp...@denisbider.com

unread,
Aug 11, 2015, 3:45:55 PM8/11/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
Good points, thank you!

I have uploaded a new version of the draft addressing the following:

- proposed single object invocation is now similar to placement new

- there is now an Annex 2 defining a proposed std::relocate_or_move

- proposed implicit declaration rules have been relaxed, so that explicitly defaulting a special member does not block implicit relocator declaration

- added explicit statement that lifetime of object in original location ceases when outermost relocator completes

- added explicit statement that invocation of subobject relocator outside of outer object relocation is undefined behavior

New version:



> the second form should take an 'A[]' rather than an 'A*'.

I have decided against this because the argument may in fact be a slice of an array, not a whole array. Overlapping memory is also explicitly supported, so I would not like to suggest that only a whole array may be passed.


>  (Which also makes me wonder if relocation
> should be const w.r.t. the old object?)

I think not, because:

- the user may want to explicitly destroy subobjects if initializing their counterparts via means other than relocation;

- the user may want to zero out memory being moved from.


> I'm not sure I agree that argument order for a cting-dtor
> is counterintuitive :-).

Besides whether or not cting-dtor syntax is intuitive - there's the additional issue that it doesn't blend well with this:

>>A(A& x) : B(std::move(x)) { x.B::~B(); }

Providing a way to do this with destructor-like syntax would be inventing alternate syntax for something we already have.


> I've seen try_realloc more often than realloc_in_place.

I find realloc_in_place more descriptive. Strictly speaking, the existing realloc already has "try" implied, since it won't block or abort the program if it fails. So the "try" is implied, and what one really wants to express is which sub-step of realloc one is attempting.


> if you are growing a container in order to do a
> middle insertion, and cannot grow in place,
> it is more efficient to allocate uninitialized
> memory and do a piecewise move

Interesting point, very true.

Matthew Woehlke

unread,
Aug 13, 2015, 12:12:15 PM8/13/15
to std-pr...@isocpp.org
On 2015-08-11 15:45, isocp...@denisbider.com wrote:
> Good points, thank you!
>
> I have uploaded a new version of the draft addressing the following:

Finally read it; sorry it took so long.

Thanks for the improved examples section!

I still wonder if the indirection for the relocation operator shouldn't
somehow indicate that the parameter will be destroyed when the outermost
relocation completes. (If not 'T~'/'T~&', I wonder if it should at least
be a 'T&&'? Not sure though...)

> - proposed single object invocation is now similar to placement new

Cool :-).

> - there is now an Annex 2 defining a proposed std::relocate_or_move

Cool :-).

I wonder if there should also be single-object versions?

Nit: Might want to say, instead of "std::relocate, defined as" with
"might be defined as", in case implementations need to insert some
compiler voodoo to denote creation and deletion of objects in the
memmove case. (Presumably, the relocation operator has this built in,
but the trivial case isn't calling that...) Or, just if libraries come
up with some better implementation. I don't think the actual definition
needs to be codified, just the effect.

> - proposed implicit declaration rules have been relaxed, so that explicitly
> defaulting a special member does not block implicit relocator declaration

Cool; thanks :-).

> - added explicit statement that lifetime of object in original location
> ceases when outermost relocator completes

Cool :-). I think we needed this, and your wording LGTM. Thanks.

> - added explicit statement that invocation of subobject relocator outside
> of outer object relocation is undefined behavior

Erk... yes, it had better be ;-). Good catch; thanks.


Other comments:

I think this isn't exactly an omission vs. a language loophole that
should be closed, but the relocator should also be deleted if any
subobject *lacks* a relocator (i.e. "not declared"). Or if not declared
and the conditions for implicit declaration are not met. (Both will
close the loophole, the former might be preferred however.)

I'm not sure this is as tightly coupled to realloc_in_place as is
suggested. If we have realloc_in_place, we don't need relocation for the
case where that succeeds. If we assume that realloc always fails, we
don't see any benefit from realloc_in_place. (That's not to say that we
don't *want* realloc_in_place, just that Annex 1 makes it sound like
both are significantly less useful without the other, which I don't
believe is true. I think they are closer to orthogonal.)

Overall, looks very good; thanks again!

>> the second form should take an 'A[]' rather than an 'A*'.
>
> I have decided against this because the argument may in fact be a slice of
> an array, not a whole array. Overlapping memory is also explicitly
> supported, so I would not like to suggest that only a whole array may be
> passed.

Okay, I can follow that.

>> (Which also makes me wonder if relocation should be const w.r.t.
>> the old object?)
>
> I think not, because:
>
> - the user may want to explicitly destroy subobjects if initializing their
> counterparts via means other than relocation;
>
> - the user may want to zero out memory being moved from.

These are good points, but can cut both ways. Where you run into
problems is if the base class relocator fiddles with memory that the
derived class still needed.

I'm not saying I feel strongly about it either way, just trying to
explore the possible consequences of either decision.

>> I've seen try_realloc more often than realloc_in_place.
>
> I find realloc_in_place more descriptive.

Fair enough; I meant it more as an observation.

--
Matthew

isocp...@denisbider.com

unread,
Aug 13, 2015, 10:54:17 PM8/13/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com
Thanks for your reply!

> Finally read it; sorry it took so long.

No worries at all.

I have uploaded a new version again, based on this feedback:


- It now has a proper proposal number, P0023, currently R0.

- It adds a single-object version of relocate_or_move, as suggested.

- It switches around Annex 1 and Annex 2, so that relocate_or_move is in Annex 1, reflecting it's more central.

- Changed wording related to realloc_in_place, now in Annex 2, to make it clearer that it's related to, but not essential to the proposal.

- Added discussion headings "Why not a const reference?" and "Why not an rvalue reference?"



> I still wonder if the indirection for the relocation
> operator shouldn't somehow indicate that the
> parameter will be destroyed when the outermost
> relocation completes. (If not 'T~'/'T~&', I wonder
> if it should at least be a 'T&&'? Not sure though...)

The problem with an rvalue reference is that you then have to consistently do this:

 

  >>A(A&& x) : >>B(std::move(x)), >>c(std::move(x.c)) {}

 


I think this would be inconvenience for the sake of inconvenience alone. Not much is gained, since an rvalue reference is not needed to e.g. resolve an overload conflict.

A new reference type is a whole new animal altogether and probably not warranted as long as this would be the only usage case. There is no technical problem we'd be solving with a new reference type, just a subjective problem of indication.

I would argue that indication is already handled sufficiently by the syntax >>A, so that there's no need to handle it additionally via reference type, also.


> Might want to say, instead of "std::relocate,
> defined as" with "might be defined as",

Good point. I have changed that to "behaving as follows".


> the relocator should also be deleted if any
> subobject *lacks* a relocator (i.e. "not declared").

Good point. I have added this stipulation.


> I'm not sure this is as tightly coupled to
> realloc_in_place as is suggested.

I agree, the championing of realloc_in_place was overdone. I have toned it down to make it clear that the relocator proposal stands on its own, without it. :-)


> Where you run into problems is if the base
> class relocator fiddles with memory that the
> derived class still needed.

True. However, I'd prefer for modification of the original object to be defined behavior, and to let the user beware and not shoot themselves in the foot; rather than to preclude access "for yer protection". :-)

isocp...@denisbider.com

unread,
Aug 13, 2015, 10:58:53 PM8/13/15
to ISO C++ Standard - Future Proposals, mwoehlk...@gmail.com, isocp...@denisbider.com
Ville informs me that someone needs to champion this proposal in front of the Evolution WG.

I reckon, if someone isn't found, the alternative is that it will be simply disregarded.

Is anyone going to the next WG meeting who is super passionate about efficient relocation, and wants to present this proposal?

If no one volunteers, I'll have to go to Hawaii, and you know how much I would hate that. :)

I believe such a volunteer should be sufficiently comfortable with the current state of the proposal as to be comfortable putting their name on it. Since this person would be presenting and defending the proposal in front of the EWG, I imagine they would be added to the proposal as co-author.

Matthew Woehlke

unread,
Aug 14, 2015, 12:51:50 PM8/14/15
to std-pr...@isocpp.org
On 2015-08-13 22:54, isocp...@denisbider.com wrote:
> I have uploaded a new version again, based on this feedback:
>
> - It adds a single-object version of relocate_or_move, as suggested.

Cool; thanks again.

> - It switches around Annex 1 and Annex 2, so that relocate_or_move is in
> Annex 1, reflecting it's more central.
>
> - Changed wording related to realloc_in_place, now in Annex 2, to make it
> clearer that it's related to, but not essential to the proposal.

Minor point: the second memcpy in the final example should be a memmove ;-).

>> I still wonder if the indirection for the relocation
>> operator shouldn't somehow indicate that the
>> parameter will be destroyed when the outermost
>> relocation completes. (If not 'T~'/'T~&', I wonder
>> if it should at least be a 'T&&'? Not sure though...)
>
> The problem with an rvalue reference is that you then have to consistently
> do this:
>
> >>A(A&& x) : >>B(std::move(x)), >>c(std::move(x.c)) {}

Yes. I was sort-of assuming that we would special-case the relocation
operator to do an implicit std::move. But I can imagine lots of
squawking about that.

Hmm... I was going to ask about the case of relocating a single item
received as an rvalue-reference, but I guess relocate_or_move makes that
moot?

Anyway, I think I am sufficiently convinced that, at least, it should
await the committee's thoughts, which are surely better informed than
mine :-).

>> Might want to say, instead of "std::relocate,
>> defined as" with "might be defined as",
>
> Good point. I have changed that to "behaving as follows".

That's good too, and probably better than my suggestion :-). Thanks.

> I agree, the championing of realloc_in_place was overdone. I have toned it
> down to make it clear that the relocator proposal stands on its own,
> without it. :-)

:-)

--
Matthew

mihailn...@gmail.com

unread,
Jul 20, 2018, 12:55:38 PM7/20/18
to ISO C++ Standard - Future Proposals
Does anyone know what happened to this proposal and destructive move in general? It seems the current [[trivially relocatable]] covers similar ground, but it is not the same - it can't, well, move-destruct (AFAICC).

Is destructive move dead now, in favor of just marking the types, already "trivially relocatable"?
 

Arthur O'Dwyer

unread,
Jul 20, 2018, 7:23:44 PM7/20/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com
On Friday, July 20, 2018 at 9:55:38 AM UTC-7, mihailn...@gmail.com wrote:

Does anyone know what happened to this proposal and destructive move in general? It seems the current [[trivially relocatable]] covers similar ground, but it is not the same - it can't, well, move-destruct (AFAICC).

Is destructive move dead now, in favor of just marking the types, already "trivially relocatable"? 

My impression is that previous proposals to introduce a new first-class spelling (such as Denis's >>T(T&)) have not made much headway against the Committee. Therefore, both my upcoming P1144 "Object relocation in terms of move plus destroy" and Niall's P1029 "[[move_relocates]]" try to express the idea in terms of composing existing operations:
- P1144 says "always (relocate = move + destroy), and sometimes (relocate = memcpy + drop)"
- P1029 says "sometimes (move = memcpy + drop + default-construct)"

I'd be interested to talk more about "[P1144] can't, well, move-destruct" — perhaps in a new dedicated thread, though.

I'd also like to hear from anyone who was involved in actual discussions of N4158, P0023, etc.

I have just now found the LEWG discussion of Pablo's N4034 (June 2014, Rapperswil):
And the EWG discussion of Pablo's N4158 (November 2014, Urbana):

It looks like things were generally favorable: LEWG sent it quickly to LWG, LWG bounced it back to EWG due to the unexploredness of std::bless/launder at the time, EWG expressed concern with the lack of std::bless/launder but "encouraged more work in this direction" (of course); and then it fell into a black hole and never emerged again.

My hope, as you can tell, is that now that we have explored ways of blessing "regions of storage" so that they become "objects" (so that std::vector is almost implementable these days! ;)) that some of EWG's concerns may have been alleviated.  Also, N4158 lacked a public implementation that provably worked with things like allocators, whereas P1144 thinks the proper interaction with allocators is so easy the paper doesn't even talk about them.

–Arthur

mihailn...@gmail.com

unread,
Jul 21, 2018, 11:25:39 AM7/21/18
to ISO C++ Standard - Future Proposals, mihailn...@gmail.com
Thanks for the heads up! I will start a new topic about trivially reloc, as I have few questions (and few concerns).
Reply all
Reply to author
Forward
0 new messages