1

My code is like the below. My idea is that we have a P which is movable but not copyable. And we have a OP which is an optional wrapper of P. A gen_p will generates a std::pair<int, OP>, and its returned value will be "patten matched" by structured binding. Then I think the returned value is decomposited into i and pp. I want pp being returned by move or RVO, at least not by copy.

#include <iostream>
#include <vector>
#include <optional>
#include <map>

struct P {
    int i;
    std::vector<int> v;
    P(int ii, std::vector<int>&& vv): i(ii), v(std::move(vv)) {}
    P(P&& other) {
        i = other.i;
        v = std::move(other.v);
    }
    P & operator=(P && other) {
        i = other.i;
        v = std::move(other.v);
        return *this;
    }
    P(const P & other) = delete;
    P & operator=(const P &) = delete;
};

typedef std::optional<P> OP;

OP move_out_p() {
    auto gen_p = [&](){
        P pp1 {1, std::vector<int>{2,3,4}};
        std::optional<P> op = std::make_optional(std::move(pp1));
        return std::make_pair(1, std::move(op));
    };
    auto&& [i, pp2] = gen_p();
    // It compiles with std::move(pp2)
    return pp2;
}

static_assert(std::is_move_constructible_v<P>);

void test_move_part() {
    auto v = move_out_p();
    printf("v size %lu\n", v.value().v.size());
}

int main() {
    test_move_part();
}

And I get error

tm.cpp:87:12: error: call to implicitly-deleted copy constructor of 'OP' (aka 'optional<P>')
    return pp2;

I think this error is strange. Here is my reasoning: It says that

In these initializer expressions, e is an lvalue if the type of the entity e is an lvalue reference (this only happens if the ref-qualifier is & or if it is && and the initializer expression is an lvalue) and an xvalue otherwise (this effectively performs a kind of perfect forwarding)".

Since my ref-qualifier is &&, and gen_p() is a not a lvalue(it is actually a prvalue), so in my case it is a perfect forwarding.

I don't know what I will get in this perfect forwarding case. However, if pp2 is a lvalue, then NRVO will happen. I am sure that NRVO is enabled in my compiler, since we need to specify -fno-elide-constructors to disable that. If pp2 is a xvalue, it will compiles. Since std::move(pp) creates a xvalue, and it compiles. If pp2 is a prvalue, I don't know, but I find it meaningless to require a copy constructor of a prvalue. So I think there is no case that prevents a RVO.

Also, if pp2 is a lvalue, it should be RVO-ed. I don't understand why an RVO is not happen in this case. It is recommended that we don't return a std::move(pp2) in our program. I don't know why we break this rule here. The compiled code looks strange to me.

So I am lost here? Could you explain this error to me?

calvin
  • 2,125
  • 2
  • 21
  • 38
  • 1
    `gen_p()` is not an lvalue, but you aren't returning `gen_p()`. `pp` is an lvalue. – Nathan Pierson Jun 20 '23 at 17:19
  • @NathanPierson Hi, I found I have two `pp`s in my code, so I don't know which `pp` you mean. I renamed them to `pp1` and `pp2`, could you specify a little bit? – calvin Jun 20 '23 at 17:22
  • Can you edit the question so that the error code matches the revised code? I meant what you now call `pp2`, and I believe your compiler also meant that. – Nathan Pierson Jun 20 '23 at 17:26
  • @NathanPierson Yes, my compiler complains about `pp2`. My question is even if `pp2` is a lvalue, it should be RVO-ed. I don't understand why this is not happen in this case. It is [recommened](https://stackoverflow.com/questions/14856344/when-should-stdmove-be-used-on-a-function-return-value) that we don't return a `std::move(pp2)` in our program. I don't know why we break this rule here. – calvin Jun 20 '23 at 17:30
  • 1
    RVO is an optimization that is applied after the fact. Your code has to obey the rules of the language first before the optimization is applied. If it is an l-value it must be copy constructable to return as a value, once the compiler knows this then it can apply RVO to remove the copy construction. – Martin York Jun 20 '23 at 17:33
  • It could just be compiler discretion about whether to apply NRVO. See [here](https://godbolt.org/z/Mae8o17Th) where even at O0, gcc is fine with it in C++20 but not C++17. – Nathan Pierson Jun 20 '23 at 17:33
  • @MartinYork Do you mean that the compiler don't actually know it can NRVO `pp2` even if it actually can, so we need a `std::move` here? So if a compiler don't support RVO, then many C++ codes can't compile, if they tend to rely on RVO without a copy-constructor? – calvin Jun 20 '23 at 17:37
  • @calvin: No. The code must support the operation before any optimization is done. Technically your code requires a copy so you get an error. Only after your code is semantically correct can it then compiler apply the RVO optimization. – Martin York Jun 20 '23 at 17:45
  • @MartinYork So the point is if RVO could happen, then the class I defined must have sopy constructor. If I only defines a move constructor, then I can't assume RVO can happen. In some cases, the compile can RVO for classes which only have move-constructors, but they make no promise for that? – calvin Jun 20 '23 at 17:53
  • @MartinYork Counterexample: https://godbolt.org/z/sWzEbcshx This is returning a `std::optional

    ` by value yet there is no compiler error/warning.

    – Matt Jun 20 '23 at 18:35
  • @Matt In that case the compiler is fine using a move constructor. The case above the compiler is saying it needs a copy constructor. These two are not identical. The RVO part is an optimization but it will only be applied when the compiler determins the code is symtantically correct. – Martin York Jun 20 '23 at 20:57
  • @calvin Both Move/Copy construction can (sometime have to be optimized away). **BUT** the underlying code must be valid first. If you needed a copy/move constructor for the operation, then those must be usable before they can be optimized away. If they are not available, then your code is not semantically valid. – Martin York Jun 20 '23 at 21:15
  • you can guarantee copy elision by returning the prvalue. change the return to "return gen_p().second" – Yusuf Khan Jun 20 '23 at 22:17

1 Answers1

3

First, some terminology.

RVO, or NRVO, refers to a specific instance of copy elision. In C++, copy elision is an optimiztaion where there is a situation that the language rules say that a copy/move will happen, but a compiler can choose not to have that copy/move happen. However, in all such cases, even if a copy/move will be elided, it must still be possible in accord with the rules of the language.

Note that C++17's "guaranteed elision" is not really a form of "elision" per se; it redefines certain C++ concepts such that there is no copy/move. But since your case involves a named object, guaranteed elision does not apply.

So it doesn't matter if you allow the compiler to elide something; it still must be a thing that can happen for that object. If return pp2; means to copy from pp2, elisions settings on the compiler are irrelevant. Whatever pp2 is, it must be copyable or a compile error results.

So the only question is this: why does return pp2; try to copy from pp2 instead of moving from it? Normally, return identifier; will move from identifier. But there are rules for that.

In particular:

An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type.

So, what are the names of structured bindings? Well, for structured bindings whose elements are enumerated through get<i> calls, they are references. But they are only rvalue references if the initializer used to initialize the reference is a prvalue.

The initializer used is get<i>(e), where e is the value in question. And get<i> for pair returns either an lvalue reference or an rvalue reference. It never returns a prvalue (as that would be a copy of the object).

Therefore, the names of a structured binding of a pair will never be rvalue references, and therefore they cannot be implicitly moved from in a return statement. return pp2; will try to copy from it.

It should be noted that the other two forms of structured binding (arrays and publicly-accessible struct members) also will never allow elements of the structured binding to be implicitly moved from. For structs and array elements, the names refer to subobjects of some object. They aren't references, but they aren't objects either. So neither qualifies for implicit moves.

In short, you should not expect the elements of a structured binding to be implicitly moved from.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • So the correct reasoning is that: According to [Mandatory elision of copy/move operations](https://en.cppreference.com/w/cpp/language/copy_elision), compilers are only promised to do elisions when we are returning a prvalue. However, names of structured bindings are references, and references are lvalues, so the can't be promised to do elision. – calvin Jun 20 '23 at 18:14
  • If the above reasoning is correct, then I have another question that why using `std::move(pp2)` is ok? Since from [value_category](https://en.cppreference.com/w/cpp/language/value_category), I find `std::move(pp2)` will return a xvalue rather than a prvalue, so it is also not promised to elide? If it is true, then by no means can we reduce a copy here? – calvin Jun 20 '23 at 18:15
  • @calvin: You don't seem to have understood the first part of my post, as you are continuing to use "elision" incorrectly. Implicitly or explicitly moving something is not "elision". `return pp2;` causing a move is not "elision". Elision *does not apply* to your question. – Nicol Bolas Jun 20 '23 at 18:31
  • Alright, it the following true? We can't elide a copy here, since P is not defined to be copied. We can't elide a move here, since `i` and `pp2` are not rvalue reference. If there is no rvalue reference here, then we can't implicitly move. If we can't move, then there will be no move elision. – calvin Jun 20 '23 at 18:36
  • @calvin: "*If there is no rvalue reference here, then we can't implicitly move.*" If `pp2` were the name of an automatic object in the local scope (rather than a reference to something), then implicit moves would happen. "*If we can't move, then there will be no move elision.*" It would be more accurate to say that if an operation is illegal, it cannot be optimized away. That is, if your syntax doesn't allow a move, then it will be copied. But copying is not allowed, therefore the code is broken. Elision *only applies* to legal operations. That's why it doesn't matter here. – Nicol Bolas Jun 20 '23 at 18:41
  • Ok, that really helps. However, could you explain a bit more about the [" Mandatory elision of copy/move operations"](https://en.cppreference.com/w/cpp/language/copy_elision)? It says elisions are promised to be elided only when they are prvalues. However, I find "std::move" will generate a xvalue rather than a prvalue. So is elide guaranteed here if we use "std::move"? – calvin Jun 20 '23 at 18:47
  • @calvin: Why do you insist on talking about elision? As I have repeatedly said, elision *does not matter* to your question. And, as I said in my answer, "guaranteed elision" is not really the same thing as "copy elision". You are confusing yourself by continuing to talk about something that doesn't matter. – Nicol Bolas Jun 20 '23 at 18:53
  • Ok, I get that, `std::move` compiles, but still not because of elision. It can compile, just because it is move constructed? – calvin Jun 20 '23 at 19:02
  • And yet one more question about details of your question... "the names of a structured binding of a pair will never be rvalue references". Are they actually reference to a rvalue reference, which makes them lvalue references? – calvin Jun 20 '23 at 19:14
  • @calvin: "*Are they actually reference to a rvalue reference*" There's no such thing; you cannot have a reference to a reference. You only have a reference to an object. And the standard says that they are only rvalue references if the initializer is a prvalue. – Nicol Bolas Jun 20 '23 at 20:17