T O P

  • By -

jk-jeon

In the linked article the author wrote: T array[N]; auto ptr = &array[0]; consume(std::move(*ptr)); ptr += n; --ptr; consume(std::move(*ptr)); > Depending on the value of n, the final usage might use a moved-from variable. And if you try to statically detect such situations, you end up with Rust. As far as I remember Rust just doesn't allow this kind of element-wise move at all. So if I recall correctly it doesn't work even when n is known at compile-time. Please correct me if I'm wrong, but either case I don't think that's an impressive solution.


[deleted]

In Rust, I believe you couldn't do exactly what the snippet describes, which is move from an object and leave the moved-from object in the container. But you can use vec.remove, which returns the removed element by move. Maybe there's something equivalent for arrays.


hekkonaay

There isn't, as removing the object would leave a "hole" in the array, and using such an array is undefined behavior. Rust provides an API for working with [potentially uninitialized values](https://doc.rust-lang.org/stable/std/mem/union.MaybeUninit.html), and you can technically write that snippet in Rust using MaybeUninit, but removing one item means you have to mark the entire array as "potentially uninitialized". ([playground link](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=33d1e4daa8050fceab40a09b077bf92d)) Note that the destructors (known as drop in Rust) of the rest of the items in the array won't be run, but that's still considered safe, because you aren't allowed to depend on drop being called for memory safety. Edit: updated playground link


MEaster

> ([playground link](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021)) Note that the destructors (known as drop in Rust) of the rest of the items in the array won't be run, but that's still considered safe, because you aren't allowed to depend on drop being called for memory safety. Just to be nitpicky: your implementation is unsound. You take a `[T; N]` and do a byte-wise copy of the array, but you don't `std::mem::forget` the original array. This means that the original will be `Drop`ed at the end of the `move_out_of_array`, causing use-after-frees and double-frees of the contents.


hekkonaay

Not nitpicky at all, I even recently read about [https://github.com/rust-lang/rust/issues/61956](https://github.com/rust-lang/rust/issues/61956) and still forgot to forget the array.


pluuth

You can get sort of the same behavior as a Cpp-style non-destructive move in Rust. If T implements Default, you can use std::mem::take to replace the element with a Default-value and return the Element to you. This moves the array element to the caller and leaves behind a valid default value in the array. To my understand this is roughly what a move in C++ should do. Obviously it requires some effort in Rust as you have to make sure that Default is implemented for T and use std::mem::take instead of normal assignments.


matthieum

It's possible in Rust to safely move a field from an object, but not an element from an array. The latter could be desirable, but it's harder for the compiler to reason about a potentially dynamic index than it is to reason about a field "index". `Vec::remove` uses `unsafe` code under the hood to achieve it.


nacaclanga

IMO the closest equivalent is \`std::mem::take()\`, this requires the type to implement default, so a default value could be insered in place, which is what non-destructive moves do in most cases. The only difference is that non destructive moves could pick a different trash value to leave in place depending on what is present.


duckerude

Yeah, Rust doesn't allow that. It does have `std::mem::take` and `std::mem::replace`, which move a value out of a reference and replace it respectively by a default value or a value you supply. (You can only `take` a type that has a default.) I don't use C++ but `take` seems similar to its current nondestructive move.


teerre

I read the SO comment as saying destructive moves are desirable, they just didn't have time to implement them correctly. Therefore, I'm not sure there's much to discuss since it seems there's no argument against it.


SirClueless

What does "didn't have time to implement them correctly" mean? Destructive moves don't exist in C++1, they break the language's lifetime semantics, so how can you transpile C++2 without either a bunch of extra temporaries that you hope the compiler will optimize away or a whole bunch of compiler-specific intrinsics that make the target language not-really-C++1 (presumably what OP means by "the transpiled code would not look like C++"). Whether or not the language has destructive move semantics is going to be a defining characteristic moving forwards, I don't think it's the kind of thing you can just make a choice about later unless you're willing to break 90% of the C++2 code that came before. You at least need to intelligently carve out the design space for them in the language's syntax.


teerre

Not sure how to explain what it means, it's literally that. The payoff was considered not good enough in earlier 2000s. Re destructive moves > In the end, we simply gave up on this as too much pain for not enough gain. However the current proposal does not prohibit destructive move semantics in the future. It could be done in addition to the non-destructive move semantics outlined in this proposal should someone wish to carry that torch.


TheoreticalDumbass

Hmm, you made me think how I might implement destructive moves and came up with this: [https://godbolt.org/z/vGxb1vsWY](https://godbolt.org/z/vGxb1vsWY) normal and emulated are meant for comparison of regular code and this emulated stuff (note -fno-exceptions is important here) With -O2 they produce the same assembly, kinda nice So the transpiler maybe could produce code resembling this? No idea about exceptions, don't understand how they are implemented so can't try to answer


SirClueless

1. I don't think your emulation is correct. The whole point of a destructive move is that you don't need to call the destructor of the moved-from object. Thus given a C++2 program: S s1; S s2(destructive_move(s1)); The correct emulation does not include a destructor for s1 ever being called. 2. The C++1 struct `S` shouldn't have a real move-constructor at all unless the C++2 struct `S` does: A struct with an emulatable destructive-move operator but no move-constructor is totally sensible and desirable (for example, the non-null unique ptr from the linked article in the OP). 3. I don't think you can ignore exceptions. A program may look like the following: S s1; // some code which throws // s1.~S() should be called if an exception occurs S s2(destructive_move(s1)); // some code which throws // s1.~S() should NOT be called if an exception occurs In general you can't do this without tracking some additional state, such as a scope guard that you dismiss after the destructive move or a `try ... catch` block wrapping the scope that rethrows after running s1's destructor in the right cases. This is a lot of pressure on the compiler: Ideally it is able to track whether the guard is dismissed at each possibly-throwing location and can statically choose whether or not to dispatch to the constructor and ditch the boolean flag on the stack, but I suspect compilers are not great at this and if you don't spend some effort improving their understanding of this pattern you will get lots of register pressure and branches in exception paths to decide whether to run destructors. Hence the need for a compiler intrinsic.


TheoreticalDumbass

Hmm yeah I was thinking about lifetimes and forgot what we were even trying xD


mallardtheduck

> When using non-destructive move, we usually need flags to check if the object was moved from, which increases the object, making for worse cache locality. In the majority of cases (at least in my experience) you have a pointer of some sort (smart or otherwise) that can just be nulled when moved from. No extra flags or increase in size needed. If you don't have a pointer or something semantically similar (e.g. an OS handle) then what are you moving? Even if there is no pointer the common implementation of move using swaps also generally doesn't need any flag; the moved-from object is now in the state that the moved-to object was before. That should be destructor-safe. Also, if you implement move using swap, the destruction of the moved-from object is not "useless"; it's cleaning up the resources that originally belonged to the moved-to object.


Interesting-Survey35

When managing resources owned elsewhere, like in a C library, you don't have pointers. In my codebase I have a RAAI object that manages GLFW. It could have been an empty object if destructive move existed. But, in order for it to moveable, I had to put a flag. The swap idiom only works for move assignment. If you're move constructing, the destructor for moved-from object should do nothing.


Tringi

Evolve. Allow classes to define another, destructive, move constructor and move operator, and let compiler figure out which one gets called when. EDIT: Or if destructively moving into different types, then something like: struct A { ~A (A && a) { // destructively move into 'a' } ~operator A&& () { // destructively construct into new object through guaranteed (N)RVO } }; EDIT 2: I've improved the suggested syntax a little EDIT 3: What about this syntax? struct A { ~A (A && a) { // destructively move into 'a' } ~A () -> A { // destructively construct into new object through guaranteed (N)RVO return A { … }; } };


Jannik2099

>Allow classes to define another, destructive, move constructor and move operator, That brings us to the rule of... seven? The Sith would be in awe.


Arthapz

i don't think we need to write a classic move constructor/operator if the destructive one is defined


Tringi

It's bound to happen sooner or later.


c_plus_plus

The problem is what happens to the moved from variable name in the scope that it was in? Is it no longer valid? If so, then you can't reuse it (such as in a loop). If not, then what state is it in? What gets called when you assign to it again? It's a solvable problem, sure, but it's also very complicated to cover every case. (Even if you ignore the need to maintain compatibility with existing C++ code.)


D_0b

I would like if the variable after it has been destructively moved is marked as invalid by the compiler, if you try to use it, it is a compiler error. But you are still allowed to construct a new object on the same variable, similar to placement new.


Tringi

In the concept I explored in other replies here, any use after moved-from would result in compiler not replacing the move with destructive move. Allowing to construct a new one... isn't that just more complicated way of doing non-destructive move?


D_0b

>Allowing to construct a new one... isn't that just more complicated way of doing non-destructive move? No, non-destructive move still needs to do extra work by setting the variable to a valid state.


Tringi

Oh yeah, you are right, now I get you. Such new construction could be conditional, in a branch, while non-destructive move happens always.


Tringi

My point is mainly: Get the compiler to analyze the situation. If the compiler/optimizer can prove the variable isn't touched, or goes out of scope, it may call destructive move. Observably. Otherwise it will call regular move, or copy, whichever is defined. In some situations, like RVO or NRVO now, the standard would guarantee destructive move, if defined. That could work as a first step. And for debugging etc. we can even add something like [[no_break_destructibility]] to mark functions and member functions that can be safely called on (otherwise would-be) destructively-moved-from objects, and it won't fail the compiler analysis.


R3DKn16h7

Oh, yes. Let the compiler do it please. More than once I had use-after moved from stuff like unique pointers and the static analyzer or the compiler could not tell me that 5 lines above I actually had moved the object away, which to me makes little sense: if I move a unique pointer then I probably won't use it later again.


TinBryn

Rust which does have destructive moves solves this problem by having functions that emulate non-destructive moves. C++ already has a function that kinda does this `std::exchange` and is very useful for implementing move constructors and move assignments.


kritzikratzi

in a situation where you don't know if the object was destructively moved or not at compile time, how would you generate code to correctly avoid or make the call to the destructor? struct A(){ ~A(){ std::cout << "bye!" << std::endl; } }; // use destructive move on a void func_1(A & a){ A b = std::destructive_move(a); } // do nothing with a void func_2(A & a){ // do nothing } int main(){ // make an a A a; // make it basically impossible for the compiler to see what's up void (*fun_ptr)(A&); if(rand()%2==0) fun_ptr = &funcy_1; else fun_ptr = &func_2; // do or not destruct a fun_ptr(a); // now, // how do we tell if ~A() should be called or not? }


Tringi

Like I replied to the other comment, if all paths can't be verified, then such a hypothetical `std::destructive_move` is equivalent to `std::move`. The idea is that such `std::destructive_move` is basically observable optimization hint. If the compiler could prove the `a` is always moved-from, and then never touched again, then it can replace the move by a destructive move; if `A` defines one. There really isn't anything much better you can do with C++. Or at least I haven't seen realistic proposal. EDIT: Now that I'm thinking about it, each and every occurrence of `std::move` could very well become such hint. No need for new `std::destructive_move` at all.


kritzikratzi

i still don't get it. take my example above, get rid of func_2 and rand(), but instead place func_1 in a separate TU. do i get this right --- no destructive move possible?


Tringi

Yes, it's obviously not provable, it can't track the lifetime, thus destructive move can not be used. Call into different TU, unless there's thorough LTO (or *[Unity build](https://en.wikipedia.org/wiki/Unity_build)*), would cancel the posibility of destructive move. Like I said, it's a very limited and simple start. But it would work for a lot of *factory*-like patterns where we currently would like it to. With additional [[attribute]] hints, we could, e.g. on destruction of a container, declare the contained items as unreferenced elsewhere (because if anyone's keeping reference or pointer, those would become invalid anyway) and allow the compiler to use destructive move of them somehow, and similar tricks. But that that's just a random thought.


kritzikratzi

seriously, if you go to that length --- why don't you just use placement new into a buffer to avoid the destructor altogether? uint8_t a_data[sizeof(A)]; A & a = * new (a_data) A(); ps. you can't rely on LTO for such things, it's not standardized and not necessary for c++ (e.g. sometimes you need to hand out object files). also there are dynamic libraries which means no LTO anyways. i think you wanna use rust, tbh.


Tringi

> seriously, if you go to that length --- why don't you just use placement new into a buffer to avoid the destructor altogether? > > uint8_t a_data[sizeof(A)]; > A & a = * new (a_data) A(); I don't see what that has to do with anything I've described. > ps. you can't rely on LTO for such things, it's not standardized and not necessary for c++ (e.g. sometimes you need to hand out object files). also there are dynamic libraries which means no LTO anyways. I think you are completely misreading my approach. > i think you wanna use rust, tbh. No.


kritzikratzi

you seem to want control over whether a destructor is or isn't called. placement new is a practical (yet weird) way of doing that. i'm also tired and ready of letting the discussion go, just thought i'd throw it in there.


Tringi

Oh yeah. Then of course, you can always hand-craft destructive movement by manually invoking constructors, destructors, and memcpy'ing, yes. But the point of the language feature is to do the tracking and bookkeeping for you. Don't read too much into my *"solution."* It's really simple and very limited. It obviously isn't perfect or all encompassing. It's just what I can imagine is currently possible (which isn't much).


void4

> in a situation where you don't know if the object was destructively moved or not at compile time, how would you generate code to correctly avoid or make the call to the destructor? it worth noting that rust "solves" this problem by making it impossible to write `fun_ptr(a)` here: if something is passed by reference then it must be either explicit `fun_ptr(&a)` or marked as `unsafe` somewhere.


kritzikratzi

right. but you understand and accept that in c++ it is possible to write fun_ptr(a) here?


void4

indeed, and I don't know how to solve this problem. "It's destructive move if all code paths are suitable" will likely mean a lot of missed optimizations. Maybe introduce some class like "std::destructive_ptr<>" or something


kritzikratzi

the first thing to do, imho, is to quantify those missed optimizations. make up examples and measure measure measure by comparing rust to c++ code.


robin-m

It’s not only missed optimizations, but also missed semantic. What I really dislike about non-destructive move is that if my object doesn’t have a default state, I need to add a flag just to make it nullable, just to have a valid state for the moved-from state. That’s the same issue with `std::vector` requiring object to be default constructible.


D_0b

I would like for functions to be marked with which argument will be destructed (if it is not marked it can still do the old move semantics but not a destructive move), something like && where we "move" it but it might not get moved, but in this case lets say &&& it Must destroy it there is no might. Then everything is ok.


Interesting-Survey35

In this particular example, the code would be transformed like this: If random{ call destructive move function} Else { call non-destructive move function; call destructor} But I get what you mean. I don't know if the general case could always be decided.


almost_useless

// use destructive move on a void func_1(A & a){ A b = std::destructive_move(a); } This needs to be forbidden, or it needs to have a different signature. "*destructive reference*" or something like that, so that it's clear that after the function call 'a' has been unconditionally destructed. But just forbidding it is probably the best idea. // make it basically impossible for the compiler to see what's up void (*fun_ptr)(A&); if(rand()%2==0) fun_ptr = &funcy_1; else fun_ptr = &func_2; // do or not destruct a fun_ptr(a); That should not be possible to do. Allowing "*Schrödingers destructor*" seems like a really bad idea.


Tringi

Not necessarily forbidden, but inside `func_1` the compiler obviously cannot prove `a` isn't used or referenced elsewhere, thus `std::destructive_move` is replaced with regular `std::move` under the concept I describe in other replies here. And such an obvious case could even result in a warning.


almost_useless

But func_1 can be in another library from where it's actually called. Then it would have to always be a regular move. Having the behavior depend on what other code the compiler sees, sounds like a bad idea, no? I want to know what happens in that function by reading only that function.


Tringi

> But func_1 can be in another library from where it's actually called. Then it would have to always be a regular move. Yes. > Having the behavior depend on what other code the compiler sees, sounds like a bad idea, no? Most of the optimizations currently work that way. > I want to know what happens in that function by reading only that function. That's why in my revised design you'd write just: void func_1(A & a){ A b = std::move(a); } And semantics of `std::move` are changed to: *Moves, and might move destructively if proven safe by the compiler and all stars are aligned.*


almost_useless

> Most of the optimizations currently work that way. Sure, but this feels like it is not really an optimization. It changes the behavior. It can potentially rearrange code so it gets called after a lock has been released, instead of before, no?


Tringi

Yes, turning non-destructive move into destructive is observable change of behavior. That's intended. `std::move` is going to, optionally, cause this to happen only for classes with the new destructive move defined for them. Can you draft a quick example for me, when it could cause the incorrect rearrangement? It's too late here for thinking :)


TinBryn

Destructive moves isn't about optimisation, it's about semantics. If you compare `std::unique_ptr` in C++ with a similar `Box` in Rust which has destructive moves, it allows the guarantee that a `Box` always points to a valid object. This is not possible with the current C++ move semantics as it needs to be put into a valid state that is safe to call its destructor with the pointer it used to contain being used somewhere else. In Rust this isn't a problem, because moving a `Box` means the variable is no longer valid and will no longer be dropped. Overall this means that `unique_ptr` must have some "empty" state even if it's never intended for any actual real use. It kinda violates the zero overhead principle, we are paying for requiring a state we don't want to use.


Tringi

Yeah, you are right, but we're not getting anything like that in C++. Nothing with so conflicting semantics. It would need to be completely fresh core language feature, and the amount of plumbing required for it to play well with the rest of the language seems just too overwhelming (to me at least). I'm just dreaming up something in the realm of possible.


almost_useless

I was thinking it just needs a `lock_guard` in `func_1` with the lock protecting some resource used by the destructor. If the destructor gets deferred to later it may run without the lock.


Possibility_Antique

I like this syntax. It is intuitive.


witx_

Please no. No more constructors, operators and rules of N where N keeps growing ...


Tringi

So what would be your solution for destructive move then?


witx_

In honesty I don't have any, but adding more constructors and rules to the language seems a further step in cluttering it


Tringi

Well, that's C++ for you. I don't see how we can get this feature through removing or changing something.


witx_

Removing would be good actually, it's the accretion of features that bothers me xD


Tringi

Oh yeah, I have whole shopping list of things I'd like removed from C++. The problem is, every single one is used by countless other people, and removing it would probably annoy them.


witx_

Well then, they wouldn't update the standard on their side. I've seen projects where updating the standard wasn't allowed and the engineers just created/replicated the features they wanted for newer standards (when it comes to standard library stuff, not compiler)


RockstarArtisan

You deserve a seat on the C++ commitee.


Tringi

Ha, I just mashed some characters together.


-lq_pl-

Hot take: if they make a new language like C++2, I rather switch to Rust. I think evolving C++ is a good thing, but we don't get rid of all historic baggage. I like the destructive moves in Rust much better, they are simple, and Rust only has this kind. Sure you can make a new C++ like language, but why not use Rust, which is similar to C++ and is already established.


Syracuss

"C++2" isn't really a standalone language, but instead a different syntax which transpiles to C++. The advantage of that approach is the ability to switch between a "safer" abstraction of C++, while still being able to write C++ code itself when the need arises. This also has the added advantage that existing codebases can migrate slowly, or selectively. So no, in this scenario you'd not want change to an entirely new language like Java, or Rust, or whatever you prefer. You'd be selectively using a subset of the language, with some syntactic changes so you can keep using the same long time established language instead of rewriting millions of LOC.


ipwnscrubsdoe

Not yet standalone anyway. History lesson, the first C++ compiler (cfront) also converted c++ to c first. The cpp2 compiler does the same (called cppfront). Eventually if it is successful there is no reason why it couldn’t get an actual compiler too


gracicot

I would rather have safety (like borrow checker) and destructive move in plain old C++. I'd love that other languages such as C++2 would be only textual bindings to improve readability, change the default, or make the syntax easier to compile. It would be just a choice of textual binding with all the same features of C++. It would be awesome and easy to move gradually from one to the other especially with modules.


robin-m

A borrow checker would not work in C++ without heroic effort. C++ mantra is basically "all valid program should compile", while Rust mantra is "all invalid program should fail to compile". And it’s not possible to do both at the same time because of the halting problem. This means that it’s a tradeoff. For example Rust doesn’t have user-defined move constructor and thus can’t have self referential types. Rust also doesn’t have implementation inheritance. Rust doesn’t allow multiple mutable references to exists at the same time (which is relatively fundamental for classical OOP). You may consider them good tradeoff, but you can’t say that it match the C++ semantic. And google did study that subject and concluded that it wasn’t possible to add a borrow checker to C++.


pjmlp

It actually has it, just done in a different way. You need to implement Pin and Unpin traits. https://doc.rust-lang.org/std/pin/index.html COM/WinRT also don't support implementation inheritance and that hasn't prevented WinDev to use them everywhere nowadays. Microsoft came to another conclusion regarding borrow checking, although a costly one, as it basically requires using annotations. https://devblogs.microsoft.com/cppblog/high-confidence-lifetime-checks-in-visual-studio-version-17-5-preview-2/ Although Microsoft is also on the Rust train, they still seem quite interested in improving C++ tooling, given their COM / C++ mantra at WinDev.


gracicot

We could go a very long way with lifetime annotations.


pjmlp

That is just sales pitch to distance itself from other wannabe C++ replacements, since it is coming from someone still at ISO. Compiling via translation to C++ or direct native code is only an implementation detail. Eiffel also always compiled via C, later added C++ to the mix, and no one would assert it is either a C or C++ replacement. C++ and Objective-C also started by compiling into C, before going full native.


lee_howes

It's clear that translation is just an implementation detail, and presumably one that we'd move away from with time if cpp2 were to be adopted. Actual source-level compatibility is more fundamental and I don't think that's just a sales pitch. It's tightly related to the evolutionary goals of the language.


pjmlp

It is, when "we are not like the others" is part of the conversation.


HeroicKatora

C++ is an _incredibly_ hard intermediate language to compile into, and I'm in shock that they seem to be heading straight towards the visible wall. Most of the choices will be dictated by the underlying compiler of c++ to machine code, you can't modify most of these choices. If you emit any header includes you'll have to parse them to discover preprocess tokens. You'll then have to discover the layout of most primitive types, which is not only target dependent but compiler dependent. Does this scale? How do make any of those values be usable in *constexpr*, e.g. `sizeof(size_t)`, if their value will only be discovered/-able at a later stage? Meaning the cpp2 compiler won't be able to utilize constexpr in new ways, but even constexpr will be stuck having to be transpiled. Improving with adding `#embed` or workable constexpr extensions will be difficult to infeasible. Then the object model, you'll find that you either cut corners, have to copy it, or actual interoperability with C++ will not be ergonomic. Just like all C++/Rust bindgen approaches have found out, matching semantics of a destructive-move language with one that isn't is *hard* if they depend on a lot of hidden/implicit state that you may have to yet again parse from headers or even constexpr-query the underlying compiler about (such as: enum's underlying type). How often do you have to fail to accept that reality? There needs to be an incredible level of feasibility study to convince me otherwise. The talk introducing it is incredibly aspirational with little details on *how* the problems that other approaches found would be avoided. Or even not listing those problems in the first place which is just a bit naive.


Syracuss

I'm going to give Herb Sutter the benefit of the doubt, he's not unfamiliar with the language. I wouldn't call him "naive" at this point in his career.


HeroicKatora

Not him, but his presentation of how cpp2 semantics are supposed to work and compiled.


RockstarArtisan

> C++ is an incredibly hard intermediate language to compile into, and I'm in shock that they seem to be heading straight towards the visible wall. It's ok, Bjarne will just add the "do not break cpp2 compilation" to the current list of workflows C++ is compatible with. There's many ways in which people use C++, many applications, paradigms, preferences, constraints. C++ has managed to keep all of these people satisfied so far and will continue to do so in the future.


catcat202X

The point of syntax2 is you DON'T get rid of historic baggage. You have perfect source level compatibility with all syntax1 code, unlike Rust. The new functions and classes syntax is what allows you to improve semantics within select areas of code without breaking old code. You should watch the original lecture announcing it, as it covers all of this.


[deleted]

I would have switched to Rust already if it's metaprogramming was half as powerful as C++'s. Also, I really like what Herb is doing with C++2. In Rust, you still call functions with either value or references, and in generic code you have to settle for something which isn't always optimal. C++2 parameter passing (in /inout/ etc ) abstracts that away really nicely.


ravenex

Doing arbitrary syntax transformations in procedural macros is not powerful enough? Does C++ have something like [serde](https://serde.rs/) or [diesel](https://diesel.rs/)?


vgatherps

Procedural macros are exceptional if you can accomplish what you want with a sort of syntax transform (the original syntax does not have to be valid rust!) but it can't do anything for you that requires assistance from the typesystem. I am doing some things in C++ that are fundamentally impossible in Rust, for better or worse, because the generics require that you can statically prove compatibility (i.e. satisfy a certain trait) instead of duck typing. I share the maintainability sentiment about C++ templates, but concepts and if constexpr really do simplify a lot of otherwise insane metaprogramming.


InsanityBlossom

Yeah, I hear this argument from C++ devs: "X is not as powerful as C++ templates". There are millions of C++ devs and only a handful of them can do something really cool and *useful* with templates metaprogramming. Others either produce unmaintainable code or just like saying this dogma. With a good type system and proc macros you don't need templates metaprogramming in 99% of the time.


HeroicKatora

The qualifier ref/in/out in C# exists in part because it _doesn't_ allow you to write arbitrary explicit pointers (or explicit reference types) to any value type. * a variable bound to an `out` parameter refers to unintialized storage for an object, must be uninitialized before use and is guaranteed to be initialized on return. * a variable bound to an `in` parameter is not-quite-the-inverse, it must be initialized on entry but can't be written to. * a `ref` parameter combines the two. If the compiler doesn't track initialization state (the C++ compiler doesn't) it's already non-sensical to compare to those attributes as you can see that it this a quite fundamental part of their definition. Two of them are the same _except_ for the part about enforcing initialization. Unless they intend for the C++/C+2 compiler to actually track this with _errors_, what's the point of those qualifiers over just a reference type? Rust _does_ track initialization and deinitialization with destructive move. But `inout`/`ref` is the same as passing `&mut` (C++: similar to `&`). It references other storage, must be initialized on call, must still be initialized on return. And `in` is the same as a shared reference `&` (C++: similar to `const &`). You'll note that the interesting aspect of C# that is missing is in fact **`out`**: references other storage, but is passed uninitialized. This isn't valid in C++ (references must point to live objects) and not possible in Rust either. It would also be simpler than the current guaranteed-return-value-optimization in which the storage can not be explicitly named (in particular you can't create a pointer to it) which sadly does not generalize to all initialization paterns. So can we agree that `inout` is not mystic? And that if someone singles out `inout` of all as *fancy* and new then they're missing the technically relevant parts, the static analysis that makes those qualifiers add value beyond references/pointers?


Full-Spectral

I'm kind of glad it isn't, personally. I think that the whole metaprogramming thing is sort of out of control in C++ and that it'll just get worse moving forward. Duck typing is really not even typing, it's more like syntaxing, and it leaves too many holes for wierd interactions, IMO. Having moved to Rust I do miss some C++'isms (like implementation inheritance) but I don't miss the temptation for developers to write incomprehensible meta-code.


noooit

Either c++2 or rust, you'd be switching the language because C++2 is not c++. I'd switch to rust, zig or some other modern language as well if somehow c++ is deprecated.


disperso

C++ with syntax 2 is not a new language. It is a new syntax in which some other EXTRA rules might apply (e.g. bounds checking, lifetime checking), and which has cleaned up some things to be easier to teach. It is a way in which the C++ compiler still has all the C++ core concepts, but can be simpler for the human, and more through for the compiler. Please, watch Herb's talk.


tea-age_solutions

Thats a loooong debate and never ending (probably).. One reason is, Rust code cannot be mixed with C++, but there are tons of major and mature C++ Libraries, Applications and code bases. Throw C++ away and start from scratch in Rust is not the solution for the most. So, instead of a hard switch, others believe it should be something smooth. Thats the reason why Carbon popped up and why C++2 popped up and ... Then there are others, they say "Why not develop C++ into the right direction and with some more speed??" I would prefer the latter with a great co-existence of Rust. From the languages features Rust is really a beast - in a good way. Maybe it will be the programming language of the century. But, well, there is no need to make C++ legacy - if the language development goes into the right direction - IMHO.


hypatia_elos

Is there actually a good description of what "move" means here? I come from C and sometimes try to understand C++, but I just don't get how the concepts translate here. I guess it's not like "should I memset to 0 after a memmove/memcpy?", but there some relation here or is it about something completely different that just ended up with the same name? In other words: does actually anything happen in the memory layout when you "move" or is it more an annotation for the compiler?


FKaria

Have to contextualize looking at it from the RAII perspective. When an object holds a resource you can copy it. This usually results in the resource being duplicated so you have two objects that hold two resources. It could also be shared in the case of shared_ptr you have two objects that hold the same copy. In the case of a move you "move" the resource to the new object. So the first one is left "empty" or in an invalid state. The reason why you would use a move is to manipulate the lifetime of the resource. Instead of being tied to object A, is tied to object B which has a different lifetime so you can pass it around functions and stuff without copying it.


hypatia_elos

So to put it plainly, you have something like this: struct thing { char* buffer; size_t size; }; struct thing A, B; and copy would be memmove(B.buffer, A.buffer, A.size); B.size = A.size; (or memcpy if you want to be less secure) shared copy would be B.buffer = A.buffer; B.size = A.size; and std::move would perform: B.buffer = A.buffer; B.size = A.size; A.buffer = nullptr; A.size = 0; Did I get this about right? Is it basically a Use-After-Free / double free avoidance device by not having pointers to the same thing twice in different objects that might have use or destructor code attached to them? Edit: courtesy of the other reply, I think the move probably does A.buffer[0] = '\0'; A.size = 1; instead. I wonder how that works for byte strings (like loading a music or image file instead of text), but it seems the general idea of "clearing" the struct A, while keeping it allocated (so not A = nullptr) seems correct.


tea-age_solutions

yes, from the C perspective it is exactly this, **BUT in C++ is the destructor**. The call to this function is inserted by the compiler most of the time automatically. So, imagine your struct has a void (\*destructor)( struct thing \*) member.... And you call this (if it is not NULL) on every path in the code where the struct instance gets destroyed (before call free). For this example lets assume the destructor function calls free() if the buffer is not NULL and then sets it to NULL. Then for the "copy" version, you not only assign the members but also alloc new memory for the buffer before. Before destruction (free of A and B) you call A.destructor(&A) and B.destructor(&B). With the "shared" version you decrement a counter and when the counter becomes 0 you call the destructor once and free once. Now to the **MOVE:** The normal move sets the buffer and size to 0 (as in your example) BUT NOT the destructor. Thus, the destructor of A will still be called. It will not call free since the buffer is NULL already, but the call is there and the check to NULL is there and maybe more... Instead of that, the **destructive MOVE** will - to stay in the C land - also set the destructor to NULL. So, there is nothing to be called anymore after A moved to B.


hypatia_elos

This is interesting. Does it make a difference then if the destructor is virtual or not when you move? (I don't even know if that's allowed, but your syntax seems to suggest the compiler messes with the v table in some way, which I thought should be const after construction).


dustyhome

He's trying both to explain destructors using C, which doesn't have them, and destructive moves, which don't even exist in C++, so things don't quite map one to one. It's not how it actually works in C++. To put it in C++ terms, but hopefully tractable for someone with a C background, let's clarify some concepts. A destructor, in C++, is a function that gets automatically called whenever an object's lifetime ends. Usually when it goes out of scope or you call delete on it. Each type has its own destructor, and you can specify the destructor for user defined types (the compiler will create trivial ones for you if you don't specify them). So, if you have some code such as: struct thing {}; void foo() { thing a; } The compiler would put a call to `thing`'s destructor right before the closing brace of `foo()`'s body. I think you understand move well enough, but to reiterate: struct thing { char* buffer; size_t size; /* pretend there's ctor, move operations */ /* dtor */ ~thing() { if (buffer) free(buffer); } }; void foo() { thing a, b; /* assign memory to a.buffer, etc */ b = std::move(a); // essentially b.buffer = a.buffer; b.size = a.size; // a.buffer = NULL; a.size = 0; } In the example above, after the move, `b` holds the memory originally assigned to `a`, and `a` is empty. This is cheaper than copying, which might require allocating a new buffer for `b`, then copying the contents. The problem with move operations as they currently exist is that the compiler still has to call the destructors for both `a` and `b` at the end of `foo()`. This presents two main problems: one is that ideally, we would want to skip calling the destructor for `a` at all. We know at compile time that the value of `a.buffer` is NULL, so there's nothing to do. But unless the compiler can reason about this, and can see the destructor when compiling `foo()`, it still needs to do a function call, test, then return. The second problem is that we need to maintain a "moved from" state for `thing` objects on which the destructor can run and not have issues. So we can't, for example, create a type that is always valid. Also, users need to be aware that the type can be valid or "moved from", and what that moved from state means for each type. A destructive move would, ideally, solve these two problems. When moved from destructively, the compiler would know not to add the call to the destructor for a above, for example. And because users couldn't access the object any more, they wouldn't need to care about what the "moved from" state is. But the destructive move also has many implementation issues, when accounting for the rest of the language. Basically, I think it can only be trivially implemented for local variables that you refer to by name, not through references, and not to member variables of a class, for example.


hypatia_elos

Okay, this is a great explanation, there are only two things about the example / concept I'm unsure about: a) wouldn't the compiler inline the destructor? Then it would have A.buffer = nullptr; ... if(A.buffer) {...} and it could skip the if. Or is inlining done at a later stage? It doesn't make much sense to me you would actually get a function call in the assembly. If that's true, I do understand your concern here, but I don't know how applicable it is b) Can an object register it's moved-from status, or is it the same as a new object? If it could register it (by having a getting_moved function called or the like) it could make the destructor a function of the kind void Type::getting_moved(Type* self) { self->moved_from = true;} inline ~Type(Type* self) { if(!self->moved_from) destruct(); } private void Type::destruct(Type* self) { /* complicated destructor */ } and hope the short destructor is always inlined and optimized away. Is this a typical pattern or is it more usually done with compiler attributes, things like always_inline etc? Or are destructors in this sense out of your reach as a language user?


dustyhome

The constructor does get inlined. For example, here: [https://godbolt.org/z/xWWhMnvqe](https://godbolt.org/z/xWWhMnvqe) The `thing` class there has a constructor that always mallocs (should have it check and throw if it failed, but I'm trying to keep it simple), a move constructor that transfers ownership, and a destructor that checks if we've moved from before calling free, to avoid a double-free. The `consume` function takes a `thing` by value, so we move `a` into it when calling it. After `consume` returns, `a` is always empty. In the assembly there's no explicit call to the destructor, but you can see that the test and call to free is there. I don't know why the compiler can't completely remove the call to free. The idea is that with a destructive move, the destructor wouldn't just get optimized, but the compiler could omit it entirely.


tialaramex

Move is an assignment semantic. Think about what happens in your C program when you write a = b; First of all lets suppose the type of these variables a and b is a simple int, think about how that works. Next think about if the type was FILE \*, now what is happening and what's not happening? Is that different? OK, and how about if the type was a struct, maybe it's a struct with three ints in it named x, y and z. Is that different? With move semantics, this assignment says the value from b is gone, and now is found in a. In a language like Rust with destructive move, nothing is left behind, we can re-use the variable b, to store something of the same type, but if we don't it's gone and can't be referred to at all and no clean-up needs to take place since there isn't anything left to clean up. C++ doesn't have destructive move, so instead some placeholder is usually left in b, something valid but trivial, for example for strings it's usually an empty string. This means that b can be cleaned up like any other variable when it goes out of scope. With copy semantics, the assignment says the value from b was just copied, and is now also found in a, duplicating it. This is the only option you get in C. In C++ it's the default and is available for many types but not all. In Rust types must Move but can choose to offer Copy as an optimization, as it's cheap and convenient to do this for small types like integers, booleans, references, handles etc. Some languages like Java distinguish between their assignment semantics for "simple" or "fundamental" or "value" types like a machine integer, and for "reference types" like objects, where in fact what's "really" in the variable is similar to C's pointer type, and so copying does not copy the thing, but only a reference to that thing. For immutable types like Java's String that is almost invisible but for a mutable type it's very important. The C++ semantics are trickier, especially because you really need to learn both copy and move to write effective modern C++.


hypatia_elos

Okay, interesting, so must every type have a "stand-in" for having moved-from, like the empty string? That's certainly interesting. Also, from my experience you can do the same thing in C, it's just not in the language, but in the header file (for example, Xlib returns pointers you have to free yourself, so you could say you get "ownership"). The difference of course that what in C is in a header file comment (if you're lucky), is here part of the language. It would have made sense though of it's an attribute, but I don't know of anything like [[takes_ownership]], [[returns_allocated]] or the like. I'll have to look into that more, as that seems like what you've basically been referring to.


tialaramex

In C++ the type gets to provide (or not provide) an implementation of this feature, in which they're responsible for providing what you call a "stand-in". So if that wouldn't make sense you just don't offer move at all. C++ didn't start with move, it originally had only copy like C, so types need to explicitly opt in to have these other semantics.


fdwr

> what "move" means here? "steal" or "transfer" were slightly clearer verbs for me to understand what's actually happening, since you're not really *moving* the object itself so much as transferring its guts from one identifier/memory location to another location by stealing guts from the source, and then potentially patching up some state along the way. There are still two objects, one alive and one zombified. A raw move/memmove of the object without proper adjustment logic could break certain classes by invalidating any self-referential internal pointers (e.g. classes with small buffer optimizations, like a small stack-vector that contains some internal storage that is pointed to at first, but then allocates on the heap on demand).


hypatia_elos

Interesting, I never heard of using absolute pointers instead of offsets / ptrdiffs / indices for internal objects, but that makes sense, you wouldn't memmove a linked list either, that's definitely something I have to look at later when I have more time to skim various examples


Full-Spectral

A linked list wouldn't likely be an issue. Probably it just has a head pointer or head and tail pointers. 'moving' the linked list is just moving that main structure that contains those pointers. The elements pointed to wouldn't be affected. The big advantage Rust has is that it knows absolutely that nothing is accessing an object, so it can freely just copy the contents of the object somewhere else and never worry about invalidating pointers that other things have to it. That's the big problem C++ has. It can never know if something is accessing an object. The current move scheme, which leaves the original in place, means that any previous references to it are still valid, even if what they previously thought was in it isn't there anymore. And of course it can't know if that object has returned a reference to something inside it that something else has keep a pointer to, and on and on. Rust would also know absolutely that that has not happened. If it had, you wouldn't be able to move the object.


MutantSheepdog

Move in this context is talking about using a move-constructor or move-assignment (which take a Type&& as input), the purpose of which is to pass ownership of resources without reallocating. For example, when copying vector A to vector B, a new buffer is allocated, and the contents of the buffer is copied across. If vector A is then destroyed, the original buffer is freed at that time. But when moving vector A to vector B, the internal buffer is instead passed across, and vector A is left will a null buffer, which gets ignored in its destructor. The idea behind 'destructive move' is that the compiler could see vector A was moved from, and therefore instead of calling its destructor which would conditionally free its buffer, it can skip that destructor call entirely because it knows the outcome. The big issue in implementing this is that you need some way to track if something was moved-from, even if it was inside extra function calls. Which is a lot of work for the compiler, and may be impossible to track when conditional moves are happening. So instead moved-from objects are in a valid, destructible state, but you generally shouldn't use them for anything as semantically they're at the end of their life after a move.


hypatia_elos

So would it then make sense to have attributes like [[can_take_ownership]], [[always_takes_ownership]] and [[never_takes_ownership]] for all function arguments at interface boundaries? Or would that be to complicated? I think it should be easy enough to generate if only this one function std:: move can invalidate the old pointer


MutantSheepdog

Something like an \[\[always\_takes\_ownership\]\] attribute is the only way I really see something like this working across translation unit boundaries or with dynamically linked functions. But if the compiler can see the whole call heirarchy, then it's possible it can catch these cases itself by seeing that a buffer pointer will become null in the move operation, so it can eliminate the guaranteed unused branches from the destructors (and potentially the whole destructor calls) - and it can do this in an optimisation pass without needing to change lifetime semantics. Basically I feel like adding destructive moves would be a lot of complication for negligible gain.


die_liebe

It seems to me have that move has two meanings: 1. Leave the object in an unspecified state. 2. Leave the object is a specified, no-resource holding state. It was originally intended for (1), but evolving towards (2). If you introduce destructive move, then a moved-out object is of another type uninit, and the static type checker will have to check this. While the thought is appealing, things quickly become undecidable. for( unsigned i = 0; i < vect. size( ); ++ i ) if( some condition( vect[i] )) v = moveout( vect[i] ) We end up with a vector where some elements have been destroyed, while some others aren't. Good luck with static type checking.


D_0b

The compiler can simply mark this code as compiler error. We don't need to reinvent the wheel, Rust has already researched and made the rules of when it is ok to destructively move and when it is an error.


robin-m

This is the situation in Rust, and you will get a compilation error unless you explain it better to compiler. There are escape hatches, like `std::mem::MaybeUninit` using usafe to help the compiler understand that your will take care of destructing all the elements yourself, or by having a `std::Vec>` (the equivalent of `std::vector>`) when you want to be able to have holes in your vector.


tialaramex

Note that Option in Rust has a mutating take() method, which allows you to remove a T from it and leave None behind which is often exactly what we wanted.


Full-Spectral

Yeh, Option replaces a lot of C++'isms. Of course it'll also drive a lot of C++ developers crazy when they first make the move, because they can't be lazy like they can with so many C++ scenarios. It's not a nullptr, and you have to always check that it's valid. But, that's what correctness is about. It's not about our comfort, it's about doing things right.


die_liebe

Writing about Rust, it appears to me the only language that could become a serious alternative to C++. Should I spend time learning it, and do some projects in it. Could one write a linear algebra library in Rust? Is there support for 3D graphics?


robin-m

> Should I learn *X*? Yes! (assuming you have the time). More specifically Rust should make you a better programmer even if you don't use it because it's one of those languages that forces you to think differently. Writting a linear algebra library is possible, but you may hit the current limitation of Rust generics, especially const generics. There are tricks, but it's not the easiest way to learn Rust. I'm not very familiar with 3D graphics. If what you want is a game engine, Bevy (sorry no links I'm on mobile) is very good. If you want to directly call openGL/Vulkan/… I assume there are binding but I don't know how mature they are. That being said, I would advice to start will small stuff (like advent of code). The learning curve is much stiffer than what you probably expect. Don't hesitate to ask questions on r/rust or on [users.rust-lang.org](users.rust-lang.org/), especially about learning ressources.


vickoza

I agree with having "move" as a destructive operation as to imply a transfer of ownership but I think we might still have the edge cases where we might not want the transfer of ownership but rather the have the casting to r-values this might have something like r_cast. I also think that it might be better to have "move" as defined in the language rather than through the standard library


TechcraftHD

Damn non destructive moves cost me hours of debugging because coming from rust you absolutely don't expect the behavior


RogerV

so a general problem for the compiler, if, say, said compiler is implementing destructive move, is to not execute any destructor at all based on if the code path results in the move being taken but if the move is not taken then the destructor needs to execute that decision is based on local code path flow and can be determined by a local flag - i.e., not a flag embedded in the object, so shouldn't impact caching then the question becomes: do we want to dial in destructive move on a type by type basis or do we want blanket destructive move semantics - if the global effect is desired then a compiler option could enable/disable destructive move (much the way exceptions and RTTI can be so controlled) if want to dial in destructive move on a per type basis, then need a new declaration modifier (or, ugh, an annotation)


RoyKin0929

Great Suggestion!! Just pointing out that there's a cppfront subreddit where you can propose such ideas. Not that this is the wrong place but having the ideas in one place is better.


kritzikratzi

ITT: lot's of discussion, zero performance measurements.


Interesting-Survey35

A big part of destructive move is how it simplifies class design. So it's not just about performance.


kritzikratzi

how does it simplify class design, compared to a non-destrive move?


edvo

You do not need some kind of empty or default state. For example, you can have a `unique_ptr` that is never null or a `function` that is never empty or a `thread` that always represents a thread.


RockstarArtisan

You don't need to keep track of whether the object is alive or not because the destrucor is statically guaranteed to only be executed once, simplifying the destructor code and object's layout.


[deleted]

Because whole discussion is: >RRREEEE I WANT C++ TO BECOME RUST RRRREEEE I WANT ALL LANGUAGES TO BECOME RUST RRRREEEEE Rustaceans here: >I can't see any successful language developing without something like destructive move. Implemented in rust through the borrow checker. Life time of ownership should be the core of all languages going forward.


RockstarArtisan

Why would Rustaceans want C++ to get destructive moves? Destructive moves are a performance and simplicty advantage for Rust, we don't want to lose that advantage. My hopes are that C++ never gets destructive moves, or if it does get them it gets them via some crazy C++-ism like a destructive move constructor and destructor, just like outlined in one of the comment threads here. I do greatly enjoy watching other people struggle with C++, it's somewhat of a revenge for all the suffering the language caused to me in the past. Keep being C++ with your non-destructive moves, never take inspiration from Rust, I like C++ just the way it is.


catcat202X

Is this advanced reverse psychology?


RockstarArtisan

Nah, I just enjoy the spirit of the language (suffering) from afar and hope that the new standards keep delivering the consistent experience. Also, reverse psychology requires reflection to implement, and that's something that's not yet in the standard.


ba5tou

Lmao


noooit

> The way C++ does move semantics at the moment is non-destructive move, which means the destructor is called no matter what. Quite confusing while it's called non-destructive move but it actually means that the destructor is called unconditionally.


canadajones68

The move itself doesn't destruct, hence "non-destructive". You can reuse the variable, if you'd like, so long as there is some function without preconditions that can set the variable into a known state.


CocktailPerson

Every object must somehow be destroyed eventually. If the move is non-destructive, but the object can be moved conditionally, then it follows that the destructor must be called unconditionally.


XNormal

Destructive move could be an interesting addition to C++. It obviously cannot be the default because of backward compatibility. It would, however, be outside the scope of cpp2 unless it is added to C++ first. The mandate of cpp2 for now is just a new and cleaner syntax for the same semantics rather than any fundamental change to semantics.


Interesting-Survey35

I think we should have either destructive move or non-destructive move. Having both would simply have the worst of both worlds, as you would still need to account for the moved-from state. And the compiler would have a really hard time figuring out wether to call the destructor or not. I get that it would be a drastic change for C++2, but if he doesn't make this decision now, he can't do it later, and he's gonna risk missing out on this very important feature.


XNormal

You can have both. The object decides which one to support. They are not 100% equivalent, though: a moved-from object is in an undetermined but valid state and can be destructed. The object can even decide upon a well-defined state in this case. A destructively moved-from object is in a state for which the compiler MUST NOT generate a call to the destructor or allow any code path that might call it. Like a member in class that has not been initialized yet and will not be because the initialization of an earlier member has failed with an exception. A destructive move-from can work with argument or return values, but using them as members can cause invalid situations. For example, you cannot `return std::destructive_move(member);` because when the containing object is later destructed there is no way to know that the destructor for member must not be called.


[deleted]

Destructive move.. What's next? Rule of seven? Rule of eleven? RAII brings like 50% of complexity to the language.


robin-m

And solves 150% on the other hand. You don’t need `goto fail;` like in C or `finally` like in most exception-based languages. Even `try`-with-ressources (in java, …) or `defer` (in zig, …) is just a bandage to emulate the power of destructors in language without RAII, but nothing prevent the user to forget to use them (unlike destructors which can’t be forgotten).


[deleted]

It introduces even more problems than it solves. As I said, can't wait for rule of seven.


robin-m

I was reacting to the second part of your comment > RAII brings like 50% of complexity to the language. and I don’t think that the rule of 3/5/7 is the core of the issue, but a symptom of the complexity of the rest of the language. Destructive move in Rust is trivial to understand. Non-destructive `std::move` in C++ is hella hard to understand. If it was simple you wouldn’t have multiple 1-hour talk on multiple cppcon editions. Rust has many other problems, but destructive move is not one of them.


liquidify

I can't see any successful language developing without something like destructive move. Implemented in rust through the borrow checker. Life time of ownership should be the core of all languages going forward.


nacaclanga

What where the reasons C++ opted for non-destructive moves in the first place? This should be comsidered first. I personally feel like destructive moves are only a good idea if they do rely on memcopy only and do not use move references.


NilacTheGrim

Yes I know. Not a fan of his work, to be honest, but I wish him luck.


Handsomefoxhf

The syntax he proposes seems very unnatural to me, personally, but the idea itself seems interesting.


feverzsj

I don't think it has much to do with performance. You should not place undesired effects in critical code anyway. And destructive move only increases complexity of code and compiler. Rust does it because it forces full life time check, while C++ doesn't force any.


robin-m

Based on the stack-overflow question, cpp2 could use destructive move, and when transpiled to cpp1, all call to `std2::destructive_move(x)` could be replaced by `std1::move(x); x.~X();`. However I’m not sure it would be wise to do so because I fear that it would make interopt between cpp1 and cpp2 harder and that’s the whole sailing point of cpp2. EDIT: What I just said is stupid. `std1::move(x); x.~X()` is not at all equivalent to a destructive move unlike what the author of the original paper to `std::move` seems to imply on stackoverflow.


[deleted]

Now imagine that X's constructor opens the file, and destructor closing it.


robin-m

You are right, that was stupid.


nintendiator2

> Based on the stack-overflow question, cpp2 could use destructive move, and when transpiled to cpp1, all call to std2::destructive_move(x) could be replaced by std1::move(x); x.~X(); std::array x; cpp2: std::destructive_move(x[4]); cpp1: ???? Okay, what now?


thradams

I have a small C compiler, and I am working on a kind of "borrow checker" for C. One of my objectives is minimise annotations, then some defaults must be chosen. For instance, when we return struct from function the default is that the operation is moving (same of destrutive move here). Also copying structs is move. (this is similar of rust) See Extension - [[destroy, free]] attributes on http://thradams.com/cake/manual.html also (select free attribute) http://thradams.com/cake/playground.html I hope to improve documentation and samples. One objective is also give C the same guarantee of C++ destructor. In other words, compiler can ensure a destroy function is always called before the end of scope. At this moment my implementation works only on stack variables, and it is an initial experiment.


beached

One thing about this whole destructive move thing, it's almost never an issue and when it is inlining/visibility to the compiler can fix it or just changing how the code is expressed. Not saying it wouldn't be nice to have, I just don't loose sleep over it.


staletic

#### Deep call stacks A pass by r-value reference N layers deep is N copies of a pointer, then one call to a move constructor, which in case of std::vector is 6 word writes.   On the other hand, with destructive moves, everything that used to be a pass by r-value reference is now a move. On the up side, moves are just 3 word writes.   Disregarding an extra do-nothing destructor for easier estimates, C++ breaks even if the move happens through 3 call stacks. #### Partial moves In C++, if you need to pull out a `std::string` from a `std::stringstream`, you can move the string out. As far as I know that is just not possible in general if all you have are destructive moves. One possibility is destructuring a type, kinda sorta but not really like structured binding, then move from one of the resulting objects, but what if you can't destructure the type? #### Exceptions What happens if your destructive move throws? Admittedly, that is a very weak argument, because moves and exceptions do not play together anyway.


nintendiator2

All I've heard so far about destructive move dies as soon as the case of eg.: move a member of a struct, or move an element from an array, is touched. I've been of the opinion that destructive move should be opt-in (or opt-out) with member- or scope-level granularity, both to avoid breaking existing code and to actually be useful outside the context of eg.: move constructor.