Why does NRVO not work on a structured binding?

102 views Asked by At

The Y part.

Consider these examples:

#include <iostream>

struct movable
{
    movable() {
        std::cout << "movable()\n";
    }

    movable(movable&&) noexcept {
        std::cout << "movable(&&)\n";
    }

    movable(const movable&) noexcept {
        std::cout << "movable(const &)\n";
    }

    ~movable() {
        std::cout << "~movable()\n";
    }
};

movable rvo()
{
    return {};
}

movable nrvo()
{
    movable m;
    return m;
}

movable cnrvo()
{
    const movable m;
    return m;
}

movable binding_nrvo()
{
    struct binding { movable m; };
    auto [m] = binding{};
    // explicit move is required
    // return std::move(m);

    // otherwise a copy would be made
    return m;
}

int main()
{
    {
        std::cout << "rvo:\n";
        movable m = rvo();
    }

    {
        std::cout << "\nnrvo:\n";
        movable m = nrvo();
    }

    {
        std::cout << "\ncnrvo:\n";
        movable m = cnrvo();
    }

    {
        std::cout << "\nbinding_nrvo:\n";
        movable m = binding_nrvo();
    }

    return 0;
}

The output with -std=c++17 -O3 is the followng:

rvo:
movable()
~movable()

nrvo:
movable()
~movable()

cnrvo:
movable()
~movable()

binding_nrvo:
movable()
movable(const &)
~movable()
~movable()

I know that NRVO would not work for a "wrapped" value, for example you have to explicitly use std::move like return std::move(std::get<0>(tuple)); in order to prevent the copy.

But here the binding happens "by value": auto [m] = binding;. Hence, I beleive that m is a standalone object. Why doesn't NRVO happen here?

The X part

The question is not about "X", but for the context:
I was experimenting with structured bindings to adopt them for a "go-like" control flow to return errors. The ideal way would be to have something like:

const auto [value, err] = getSome();
if (err)
  return err;

Without an expected-like return type this appears to me to be the cleanest control flow without resorting to exceptions. I expected this to perform NRVO on the const err (like in my example with cnrvo()), but it doesn't happen.
And sinse you have to use std::move, there's no way to have value as const either.
At the end it becomes something like

auto [value, errGetSome] = getSome(); // can't have several `err` in the scope
if (!errGetSome)
  return std::move(errGetSome);

// just promise not to modify `value`
const auto& actualValue = value;

... which defeats the whole point.
Anyways, I would like an answer to the whY part.

1

There are 1 answers

5
HolyBlackCat On BEST ANSWER

A structured binding doesn't create N separate objects. It creates a single unnamed variable, plus N (somewhat magical) references to its members.

When the caller allocates the receiving object for NRVO, it can't be expected to allocate excessive space to fit the entire unnamed variable from the sturctured binding, and it might be impossible in the general case.

There's no particular reason why it wouldn't be possible for a simple single-element structured binding like yours, I guess nobody bothered to optimize this special case.


Regarding the X part, I don't see a good solution. Don't use structured bindings if you're going to return the whole pair, then you can NRVO the pair itself.