0

I'm running into an issue where under a specific set of conditions, a local variable being returning by a function is copied instead of moved. So far, it seems like it needs to meet the following:

  • The returned variable has some usage in the function. I'm assuming otherwise the whole copy/move is ellided.
  • The returned type is using a perfect forwarding-style constructor. From this answer (Usage of std::forward vs std::move) I learned that this style of constructor has some different deduction rules.
  • Be compiled in gcc before 8.0. Compiling under clang (and apparently gcc 8.0+, thanks PaulMcKenzie for the comment) produces the results I expect, however I'm not free to change the compiler being used in the larger project.

Here's some minimum reproduction:

#include <iostream>
#include <type_traits>

// Test class to print copies vs. moves.
class Value
{
public:
    Value() : x(0) {}

    Value(Value&& other) : x(other.x)
    {
        std::cout << "value move" << std::endl;
    }

    Value(const Value& other) : x(other.x)
    {
        std::cout << "value copy" << std::endl;
    }

    int x;
};

// A container class using a separate lvalue and rvalue conversion constructor.
template<typename T>
class A
{
public:
    A(const T& v) : data_(v)
    {
        std::cout << "lvalue conversion" << std::endl;
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    A(T&& v) : data_(std::move(v))
    {
        std::cout << "rvalue conversion" << std::endl;
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    T data_;
};

// A container class using a single perfect forwarding constructor.
template<typename T>
class B
{
public:
    template <typename U>
    B(U&& v) : data_(std::forward<U>(v))
    {
        std::cout << "template conversion" << std::endl;
        std::cout << __PRETTY_FUNCTION__ << std::endl;
    }

    T data_;
};

// Get a Value rvalue.
Value get_v()
{
    Value v;
    v.x = 10;  // Without this things get ellided.
    return v;
}

// Get an A<Value> rvalue.
A<Value> get_a()
{
    Value v;
    v.x = 10;  // Without this things get ellided.
    return v;
}

// Get a B<Value> rvalue.
B<Value> get_b()
{
    Value v;
    v.x = 10;  // Without this things get ellided.
    return v;
}

int main()
{
    Value v = Value();

    std::cout << "--------\nA" << std::endl;
    std::cout << "lvalue" << std::endl;
    A<Value> a0(v);
    std::cout << a0.data_.x << std::endl;

    std::cout << "rvalue" << std::endl;
    A<Value> a1(get_v());
    std::cout << a1.data_.x << std::endl;

    std::cout << "get_a()" << std::endl;
    std::cout << get_a().data_.x << std::endl;

    std::cout << "--------\nB" << std::endl;
    std::cout << "lvalue" << std::endl;
    B<Value> b0(v);
    std::cout << b0.data_.x << std::endl;

    std::cout << "rvalue" << std::endl;
    B<Value> b1(get_v());
    std::cout << b1.data_.x << std::endl;

    std::cout << "get_b()" << std::endl;
    std::cout << get_b().data_.x << std::endl;

    return 0;
}

Under gcc this produces:

--------
A
lvalue
value copy
lvalue conversion
A<T>::A(const T&) [with T = Value]
0
rvalue
value move
rvalue conversion
A<T>::A(T&&) [with T = Value]
10
get_a()
value move  <---- Works with separate constructors.
rvalue conversion
A<T>::A(T&&) [with T = Value]
10
--------
B
lvalue
value copy
template conversion
B<T>::B(U&&) [with U = Value&; T = Value]
0
rvalue
value move
template conversion
B<T>::B(U&&) [with U = Value; T = Value]
10
get_b()
value copy  <---- Not what I expect!
template conversion
B<T>::B(U&&) [with U = Value&; T = Value]
10

For completeness, clang gives:

--------
A
lvalue
value copy
lvalue conversion
A<Value>::A(const T &) [T = Value]
0
rvalue
value move
rvalue conversion
A<Value>::A(T &&) [T = Value]
10
get_a()
value move
rvalue conversion
A<Value>::A(T &&) [T = Value]
10
--------
B
lvalue
value copy
template conversion
B<Value>::B(U &&) [T = Value, U = Value &]
0
rvalue
value move
template conversion
B<Value>::B(U &&) [T = Value, U = Value]
10
get_b()
value move  <---- Like this!
template conversion
B<Value>::B(U &&) [T = Value, U = Value]
10

I have two questions:

  • Is this allowed behavior by gcc?
  • Is there any way to force move behavior here through changing the implementation of A/B? It seems any time you have a templated function parameter being taken as && it will trigger the special rules for perfect forwarding, so if I try to provide two constructors as in the A example, one taking const U& and one taking U&&, it won't avoid the problem as long as they have other templating in place.
  • Yes, it is allowed. NVRO is optional, not mandatory, copy elision. – Sam Varshavchik May 22 '21 at 20:19
  • @SamVarshavchik I was under the impression that NRVO was dealing with constructing the object in its final location to avoid a move or a copy. Here gcc is choosing to do a copy instead of a move. Does that fall under the same umbrella? – Carter De Leo May 22 '21 at 20:30
  • No, the move-instead-of-copy-in-return is strictly defined by the standard. – HolyBlackCat May 22 '21 at 20:38
  • @CarterDeLeo What version of gcc are you using? This does not occur with g++ 8.0 or higher. [See this](https://godbolt.org/z/476oh9GGq) – PaulMcKenzie May 22 '21 at 20:46
  • @HolyBlackCat are you saying that this _isnt_ allowed behavior for gcc? – Carter De Leo May 22 '21 at 20:46
  • @CarterDeLeo No, I'm only saying compilers have no freedom of choice here. Whatever the correct behavior is, it's strictly defined. – HolyBlackCat May 22 '21 at 20:47
  • @PaulMcKenzie Unfortunately, 5.4.0. That's a super nifty tool, though. Thanks for the link. – Carter De Leo May 22 '21 at 20:50
  • @CarterDeLeo Why not use the newer version of the compiler? You're using a version that is now six generations away from the upcoming version 11.0 -- you see that later versions of the compiler ironed out the issue that you're concerned with. Don't be surprised if there are other issues with the compiler not meeting standards. I know you say you can't change compiler, but this is a version change, not a compiler change. – PaulMcKenzie May 22 '21 at 20:57
  • @PaulMcKenzie I hear you. Part of the question was trying to figure out if this was a suboptimal but legal choice or a bug in gcc, so that I could go have that discussion. A bug would give better ammunition. The other part of the question was whether there was a way to implement this that would force the behavior I wanted, in case I get a negative answer back. – Carter De Leo May 22 '21 at 21:01
  • @CarterDeLeo Well I would think there are several bugs in that version of the compiler, not just the (potential) one you're seeing now. Again, you see this issue today, and tomorrow or next week, you discover something else that isn't up to standard, or is a bug. You will be playing whack-a-mole with such an old compiler, and the engineers that put together g++ can't help you (except to tell you to get a later version). BTW, Visual C++ 2019 does the same thing clang does and g++ 8.x. – PaulMcKenzie May 22 '21 at 21:10
  • What C++ version is this? If I understand correctly, C++20 mandates a move (for both `get_a` and `get_b`), but previous versions mandate a copy (for both). NRVO is irrelevant because the returned expression and returned object are of different types. (And in particular, "NRVO is optional" is not salient point.) Does GCC 8 even claim to support C++20? [Previous answer of mine with relevant quotes](https://stackoverflow.com/a/65279831/5684257) – HTNW May 22 '21 at 21:20
  • @HTNW The project uses C++14. You are thinking what I'm seeing is the other way around, that the move is aberrant and the copy expected? – Carter De Leo May 22 '21 at 21:27
  • @CarterDeLeo Hm, C++14 `[class.copy]/32` actually seems to have changed since C++11 to mandate a move in both cases: it reads basically "... or when the expression in a `return` statement is a ... *id-expression* ... overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue". Then GCC 8 is in fact wrong. – HTNW May 22 '21 at 21:45

0 Answers0