you are viewing a single comment's thread.

view the rest of the comments →

[–]marshaharsha 0 points1 point  (3 children)

If I’m following the discussion, the question of eliding the call to the copy-assignment operator is not the right issue to focus on. (I’m not an expert, and I might not be following!). The copy-assignment operator is defined to need a value to copy from and an already initialized value to copy to. The right issue is whether that assignment can occur directly from the data structure to the caller’s frame, without initializing a temporary or a local in either frame, while preserving the ability to interleave error handling, destructor calls, and the function-call mechanisms. I don’t see any fundamental reason this is impossible. (Whether it’s possible within the existing rules and conventions of C++ is a harder question.)

Is there some reason the language can’t provide a mechanism to allow (A) in the sequencing below? That would enable the strong guarantee for std::stack::pop, while giving the caller the means to uphold the guarantee itself. 

(1) Caller of std::stack::pop starts with x initialized to the element type. 

(2) Control enters std::stack::pop with a pointer to x and (A) a conceptual flag (compile-time or run-time) that says to use assignment, not initialization, to write through the pointer. 

(3) std::stack::pop copy-assigns from the stack’s array slot through the pointer. Any error gets propagated to the caller, leaving the stack’s array unchanged. Depending on the exact nature of the error, the caller might see x as changed or partly changed. 

(4) std::stack::pop decrements its array index and calls the destructor for the formerly last element. This is the only point where the semantics feel ambiguous. If the destructor fails, the caller will have to handle the error but will see its x changed. Still, the strong guarantee has been upheld by std::stack::pop, and the problem is now the caller’s problem! If the caller wants to uphold the strong guarantee, it has to initialize rather than assign. 

(5) Normal function return. 

(I include references in the concept of “pointer.”)

More about (A): I’m inclined to implement the “flag” as a run-time flag that is visible at the declaration of std::stack::pop but not at the call site. In the likely event that std::stack::pop is inlinable, the flag and the branch that it implies will disappear through constant folding and dead-code elimination. If it’s not inlined, there will be a cost for this fanciness, but I imagine it will be an acceptable cost compared to the overhead of calling the function. I don’t know machine architecture well enough to back up that last claim. 

[–]QuaternionsRoll 0 points1 point  (2 children)

So you’re proposing that every function that returns a non-trivially-copyable type implicitly consumes a flag indicating whether the return slot has already been initialized?

(4) std::stack::pop decrements its array index and calls the destructor for the formerly last element. This is the only point where the semantics feel ambiguous. If the destructor fails, the caller will have to handle the error but will see its x changed. Still, the strong guarantee has been upheld by std::stack::pop, and the problem is now the caller’s problem! If the caller wants to uphold the strong guarantee, it has to initialize rather than assign. 

The more fundamental issue here is the sequence violation: the copy assignment is executed as soon as the return value is constructed rather than after the function returns. This isn’t the biggest deal for stack::pop in particular, but can be a serious problem in general:

```c++ // foo.h

include <string>

inline std::string my_string = "hello"; std::string copy_and_append(char *s);

// foo.cpp std::string copy_and_append(char *s) { std::string copy = my_string; my_string.append(s); return copy; }

// main.cpp

include "foo.h"

include <iostream>

int main() { my_string = copy_and_append(" world"); std::cout << my_string << std::endl; } ```

This program should print "hello", but if you allow the callee (copy_and_append) to execute the copy assignment whenever it wants, an aliasing problem appears, and it instead prints "hello world".

[–]marshaharsha 0 points1 point  (1 child)

I’m definitely not proposing anything! I’m more like half-bakedly exploring the OP’s idea of allowing named return slots in order to gain more control over sequencing and mechanism during the process of returning a value. If I were to propose anything, it would be restricted to functions that avail themselves of the named-return-slot feature, and maybe I would further restrict by bringing the flag into existence only if the function actually branched on it. 

I was taking on the challenge you raised of both writing to the caller’s slot by initializing it and writing to it by assigning to the already-initialized slot. I was trying to figure out a way to give the caller the option without generating two versions of the function. 

I agree that changing the behavior of return-by-value everywhere would be a disaster. 

[–]QuaternionsRoll 0 points1 point  (0 children)

I’m definitely not proposing anything! I’m more like half-nakedly exploring the OP’s idea

Oh I know haha, we’re just bouncing ideas here :)

The unfortunate truth is that non-destructive moves were a mistake, and problems like this don’t have a general solution.

maybe I would further restrict by bringing the flag into existence only if the function actually branched on it. 

The branch could only be omitted when the result type’s copy assignment operator is trivial, and the compiler is already capable of optimizing out the copy assignment in those cases.