On Thu, May 23, 2024 at 10:28 AM Maciej Cencora via Std-Proposals <std-proposals@lists.isocpp.org> wrote:
What kind of semantic ambiguities are you talking about?
I've provided you with a specific example where without NRVO you cannot construct and return big arrays or structs thereof with a guarantee that it won't blow up the stack.
Using std::array doesn't solve the issue (just gets rid of one out of three problems there).

This is another case of leading with complications ("can't return C array from function") that distract from the example. :)
Here's a simpler case:
https://godbolt.org/z/e9vYxTr8E

constexpr int N = 1'000'000'000;

std::array<long, N> getFooedArray(int *p) {
std::array<long, N> r = {};
for (int i=0; i < N; i += 1'000'000) { r[i] = 1; }
return std::move(r);
}

std::array<long, N> *allocateFooedArray(int *p) {
return new auto(getFooedArray(p));
}

With `return std::move(r)`, this forces `r` to be stack-allocated, which then refuses to compile because it would totally blow your stack segment.
But change it to `return r;` and everyone's happy, because `r` no longer requires its own stack-allocation; now it's RVO'ed.

But using raw `std::array` is an antipattern; let's wrap it in a strong class type of our own.
https://godbolt.org/z/K5514n1hE
And then, since `Widget` is designed never to be copied or moved, let's enforce that.
In fact — reintroducing threading to the mix — we can go ahead and place a std::mutex at the head of the class; this is fine, since (again) `Widget` is immobile.

And then boom goes the dynamite: despite the programmer wanting to enforce that `Widget` is immobile — never moved or copied — it turns out that we must provide a dummy declaration for our copy (or move) constructor, and rely on the linker to catch any attempt by the client programmer to call that dummy constructor. "Dummy declaration in place of =delete" is C++98-era technology! We ought to be able to do better in the core language.
https://godbolt.org/z/czej7exGs

#include <mutex>

constexpr int N = 1'000'000'000;

struct Widget {
explicit Widget() = default;
void foo() { for (int i=0; i < N; i += 1'000'000) data_[i] = 1; }

// Never copy or move a Widget -- it's too expensive!
Widget(const Widget&); // cannot =delete, but this member doesn't really exist;
// trying to call it will give you a linker error
Widget& operator=(const Widget&) = delete;

private:
std::mutex m_;
long data_[N] = {};
};

Widget getFooedWidget(int *p) {
Widget r;
r.foo();
return r;
}

Widget *allocateFooedWidget(int *p) {
return new Widget(getFooedWidget(p));
}

int main() {
Widget *p = allocateFooedWidget(nullptr);
delete p;
}

–Arthur