Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return a named object of a class from a function (by value ) and implicit move rule?

Tags:

c++

I have a problem with understanding what happens when you return an object of a class ( Not a specific class ) form a function ( pass by value ) in this code : EXAMPLE 1

#include<iostream>
#include<vector>
#include<string>
using namespace std;
class test {
public:
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( const test& z) {
        printf(" test( const test&z)\n");
    }
    test(test&& s)noexcept{
            printf(" test(test&& s)\n");          
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
    Some_thing();
    return 0;
}

The Output :

 test()
 test(test&& s)

The previous Output makes me understand that in the function ( Some_thing ( ) ) Scope there are two objects are created . the first one is an lvalue object which we create it in the first line in the function ( Some_thing ( ) ) and we give it a name ( i ) So the constructor test ( ) is called. And the second one is an rvalue object So the constructor test ( test&& s ) is called.

But when i deleted this constructor test(test&& s)noexcept and changed this constructor

test( const test& z)

into

test( test& z)

and run the code again :

EXAMPLE 2

#include<iostream>
#include<vector>
#include<string>
using namespace std;
class test {
public:
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
    Some_thing();
    return 0;
}

The Output :

 test()
 test( test&z)

While I expected that this code will not compile because there is no constructor takes test&& or const test& as a parameter

and when i tried to add one line to the previous code which is test(test&& z) = delete

EXAMPLE 3

#include<iostream>
#include<vector>
#include<string>
using namespace std;
class test {
public:
    test(test&& z) = delete;
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( const test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
  Some_thing();
    return 0;
}

I tried to compile it but it does not compile and it does not run

So how does EXAMPLE 2 compile and run ?????? and how can the constructor test( test&z) be used instead of test(test&& z) ??????

( I mean test( test&z) is not test( const test&z) So test( test&z) can not be used instead of test(test&& z) )

edit : this code compiles and runs : EXAMPLE 4

#include<iostream>
#include<vector>
#include<string>
using namespace std;
class test {
public:
    test(test&& z) = delete;
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test(const test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};

int main()
{
    test u;
    test r(u);
    return 0;
}

The Output :

 test()
 test( test&z)

1 Answers

tldr;

i is move-eligible and so considered an xvalue as per prim.id.unequal. Thus, the move ctor can be used here.

The expression is an xvalue if it is move-eligible (see below); an lvalue if the entity is a function, variable, structured binding, data member, or template parameter object; and a prvalue otherwise ([basic.lval]); it is a bit-field if the identifier designates a bit-field.


The same can be seen from Automatic move from local variables and parameters:

If expression is a (possibly parenthesized) id-expression that names a variable whose type is either

  • a non-volatile object type or

  • a non-volatile rvalue reference to object type (since C++20)

and that variable is declared

  • in the body or

  • as a parameter of

    the innermost enclosing function or lambda expression,

then overload resolution to select the constructor to use for initialization of the returned value or, for co_return, to select the overload of promise.return_value() (since C++20) is performed twice:

  • first as if expression were an rvalue expression (thus it may select the move constructor), and
  • if the first overload resolution failed or
  • it succeeded, but did not select the move constructor (formally, the first parameter of the selected constructor was not an rvalue reference to the (possibly cv-qualified) type of expression) (until C++20)
  • then overload resolution is performed as usual, with expression considered as an lvalue (so it may select the copy constructor).

Now, lets apply this to your code snippet on case by case basis.

Example 1

In this case, as the move ctor is available and viable, the condition "first as if expression were an rvalue expression" is satisfied and hence the move ctor is selected we get the mentioned output.

class test {
public:
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( const test& z) {
        printf(" test( const test&z)\n");
    }
    test(test&& s)noexcept{
            printf(" test(test&& s)\n");          
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
    Some_thing();
    return 0;
}

Example 2

In this case, since you've provided the copy ctor test::test( test&), the compiler will not synthesize a move ctor for us. Note that not having a synthesized move ctor is different from having a deleted move ctor. Thus the condition "if the first overload resolution failed" is satisfied(because there is no move ctor) and the overload resolution is then performed for the second time which will now select the provided copy ctor and hence the mentioned output.

class test {
public:
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
    Some_thing();
    return 0;
}

Example 3

In this case, you've explicitly deleted the move ctor. That is, your intent is that if someone tried to use the move ctor, then it should fail. So here, when the overload resolution happens for the first time, the move ctor is selected but since you've explicitly marked it as deleted fails immediately and hence the error.

class test {
public:
    test(test&& z) = delete;
    test(int y) {
        printf(" test(int y)\n");
    }
    test() {
        printf(" test()\n");
    }
    test( const test& z) {
        printf(" test( test&z)\n");
    }
    test& operator=(test e) {
        printf(" test& operator=( test e)\n");
        return *this;
    }
};
test Some_thing() {
    test i;
    return i;
}
int main()
{
  Some_thing();
    return 0;
}
like image 79
Anoop Rana Avatar answered Oct 19 '25 21:10

Anoop Rana



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!