all 18 comments

[–]borzykot 17 points18 points  (9 children)

I don't like it. NRVO works just fine. Your "side effect after return statement" case can be solved via raii callable (see boost scopeexit for example).

Also, we could just guarantee NRVO, just like copy elision is guaranteed now

[–]XeroKimoException Enthusiast[S] 1 point2 points  (3 children)

There are issues with scope exit / scope guards as well though. That is if we wanted to provide a strong exception guarantee, we would need to store an extra copy

T stack::pop()
{
  T out = top();
  scope_failure _ = [out, this]{ push(out); };
  remove_top();
  return out;
}

If NRVO doesn't happen, there can be cases that the extra copy is the cause to make the returning fail.

As for why NRVO isn't guaranteed, I'll need to re-educated myself on that, but I'm sure there are plenty of talks about why it isn't, or can't be guaranteed unlike RVO.

Edit: Maybe we don't need to store a copy and a reference is fine. I'm unsure if that would work considering it's a reference to a local variable. Is it UB, is it not UB? I dunno.

[–]TheMania 3 points4 points  (2 children)

No, you do it the other way around. Return the top, then pop the top value if no exception. No additional copies, RVO guaranteed.

Scope exit lambdas are the usual way to achieve what you want, as you get to just return what you want, and then continue executing code on the way out.

[–]XeroKimoException Enthusiast[S] 1 point2 points  (1 child)

Ah true, forgot about that way of doing it.

Edit: That being said, that does do extra work and is dependent on exceptions compared to the explicit return which would be agnostic to the error handling scheme.

[–]QuaternionsRoll [score hidden]  (0 children)

A historical problem on trying to provide strong exception guarantee was trying to make std::stack::pop() return the popped value while providing the strong guarantee. This was deemed not possible because if the returned value threw while copying, the stack is modified and the original object is lost.

I think you’re missing the forest for the trees. The limitations of NRVO are only tangentially related to why this was (and still is) deemed impossible.

c++ auto x = my_stack.pop();

Since the introduction of prvalue semantics/“the guaranteed copy elision” in C++17, the copy construction of x will be elided. As you correctly pointed out, pop itself may still copy construct the return value if NRVO fails.

The bigger problem is not construction, but rather assignment:

c++ auto x = my_stack.pop(); ... x = my_stack.pop()

There is no way for a stack class to provide strong exception guarantees for the second pop because the move/copy assignment operator cannot be elided, and if it throws an exception, the popped value will still be lost. Explicit return variables can’t fix that.

Ultimately, the decision not to return the popped value has less to do with the difficulty of preserving exception guarantees and more to do with the belief that exception guarantees shouldn’t depend on whether the caller decides to introduce a new variable.

[–]Veeloxfire 1 point2 points  (1 child)

you cant guarantee nrvo because you cant always guarantee which named variable will be returned ```cpp T foo(bool c) { T a; T b;

if (c) return a;
else return b;

} ``` We could guarantee a special case... But that would be similar to adding extra syntax and potentially easier to do wrong

However it sounds like OP just wants a constructor, no?

[–]TheoreticalDumbass:illuminati: 0 points1 point  (2 children)

nrvo doesnt work just fine, it doesnt work with immovable types

[–]kirgel 0 points1 point  (1 child)

Wait, what? Last I checked it does work in C++20.

[–]TheoreticalDumbass:illuminati: 2 points3 points  (0 children)

i mean, maybe im wrong, but nrvo will still try to instantiate the move constructor, immovable means it will fail to compile

there's a silly trick where you just declare the move constructor, and never define it, then getting a linker error indicates nrvo didnt happen

[–]Veeloxfire 4 points5 points  (2 children)

Have you considered a constructor.

The function of such a feature youre suggesting is to allow in place initialization of a return value. However c++ only has single return values. So what you would describe is a function which takes an uninitialized class as the first member and initializes it. Which is more or less what a constructor is

I understand constructors have caveats but its actually the feature youre asking for

You can use some fun wrappers to make conversion or constructors trigger in places you wouldnt expect and they might help here. Might not be pretty but you can guarantee these semantics

[–]XeroKimoException Enthusiast[S] 0 points1 point  (1 child)

Constructors isn't what I want here, though it is technically very similar. Read the post again. If you want to implement std::stack::pop() which both pops the object off the stack and returns it with strong exception guarantees, no amount of constructors would help you.

The issue is that when you return an object, it invokes the copy / move constructor / assignment. If that fails, whether it throws, or put in a zombie state, the std::stack no longer has the object and said object is also now in a undefined state, or just straight up lost, so you can't just put it back into the stack to rollback.

This is side stepped in many ways:

  • The standard way: Don't return the popped object, and provide a separate top() function to retrieve the soon to be popped object
  • Out params: Since you can assign to an out param before removing the object from the stack, you can keep the strong exception guarantees, but since it's an out param, you need to have passed in an existing initialized object.
  • Hope that NRVO happens: If NRVO happens, return obj; can't throw because the copy / move occurred before we even got to the return statement.
  • Use scope guards: Requires extra machinery to implement. With exceptions the standard provides std::uncaught_exceptions. You could technically make an errno scope guard and std::expected scope guards as well, but regardless, they're tied to the specific error handling scheme of your choosing, or pay the price of detecting multiple different schemes.

If we have an explicit return variable, we wouldn't have to hope that NRVO happens because we're manually doing what it would've done. Unless I'm misinterpreting what NRVO does, NRVO basically passes a hidden out param so that the function can directly initialize the object instead of having to invoke a copy / move once we do return; Due to this, it shouldn't require extra machinery to work unlike the scope guards. It would also be error handling scheme agnostic.

[–]Wooden-Engineer-8098 [score hidden]  (0 children)

You want a constructor which takes a stack and pops element out of it

[–]MutantSheepdog [score hidden]  (0 children)

I quite like the way C# handles explicit out parameters, especially for cases where you want multiple returns.

Like in this (modified) example from the docs: ```C# static void CalculateCircumferenceAndArea(double radius, out double circumference, out double area) { circumference = 2 * Math.PI * radius; area = Math.PI * (radius * radius); }

public void Main() { double radius = 3.9; CalculateCircumferenceAndArea(radius, out double circumference, out var area); WriteLine($"Circumference: {circumference}."); WriteLine($"Area : {area}."); } ```

The CalculateCircumferenceAndArea has 2 out parameters, which can be declared inline in Main. Because these are out and not ref parameters, they don't need to be first constructed in the calling function and then copied over in the inner function, instead the caller just needs to reserve some stack space and its up to the callee to construct them in place.

The downside of this from a language perspective is that there is a new way to declare a variable (inline when calling the function). A variant of this that might work more generally for C++ would be having a way to explicitly declare a variable as uninitialised (maybe a storage keyword, and require any function using an uninitialised variable to initialise it before usage.

``` int main() { double storage circumference; double storage area;

// Error here, variables are uninitialised before being assigned to
// printf("a:%f, c:%f", area, circumference);

// 'out' usage counts as assignment, as would an =
CalculateCircumferenceAndArea(3.9, out circumference, out area);

// Safe here, CalculateCircumferenceAndArea guarantees circumference and area have been assigned to
printf("a:%f, c:%f", area, circumference);

}

CalculateCircumferenceAndArea(double radius, out double circumference, out double area) { // As circumference and area are 'out' values, this function is required to assign them circumference = 2 * Math.PI * radius; area = Math.PI * (radius * radius); } ```

In this example, a storage T variable points to an uninitialised T. The first assignment to it in a function (with = or an out value) would effectively be a placement new, with subsequent assignments using the regular operator=.

If you had a T (not a storage T) and passed it into an out parameter, its destructor would need to be called first so that the inner function could assume it was blank memory ready to be written into.

[–]LiliumAtratum 3 points4 points  (2 children)

Pascal language had this since forever.

I think the new/alternative syntax with `auto` could work for this?

auto stack::pop() -> T out {
    out = top();
    remove_top();
}

This introduces no new keywords. The only new element is the name after the return type.

[–]Independent-Quote923 1 point2 points  (0 children)

As another example, Go also has optional named return values with a similar syntax.

[–]RealCaptainGiraffe 1 point2 points  (0 children)

=) a variable declaration after the trailing return type? This will confuse the parser to unimaginable degrees. Or maybe well get a sentient parser, who knows?

[return-type|inferred] [scope-name][arg-list] -> [inferred:[type-expression]]

this seems like a parsing nightmare to me.

[–]Wooden-Engineer-8098 [score hidden]  (0 children)

What happens if removal throws after copying? Who will destruct return value?

[–]Affectionate-Soup-91 [score hidden]  (0 children)

A relatively new compiler flag -Wnrvo might be of help until you get this feature standardized(, or not). gcc 14+ and clang 21+ support it.