4

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)
Jason
  • 36,170
  • 5
  • 26
  • 60
f877576
  • 447
  • 2
  • 7
  • Object slicing happens. Never do this. – user207421 Apr 17 '22 at 03:24
  • What is Object slicing ????? and when does it happen in my examples ???? @user207421 – f877576 Apr 17 '22 at 03:27
  • 2
    It depends. In older C++ standards (before C++17) the semantics of returning by value was returning a copy to the caller. BUT, those standards *also* explicitly permitted (but did not require) the compiler to elide (omit) the copies of objects in some cases (e.g. if the only way to check a copy had occurred was by tracking calls of constructors and destructors) and some compilers (with relevant optimisation settings) implemented a return value optimisation to elide copies in some situations and some didn't. From C++17, copy elision became mandatory in several circumstances. – Peter Apr 17 '22 at 03:27
  • 1
    @user207421 How is object slicing happening? There's no inheritance here. – Nathan Pierson Apr 17 '22 at 04:01
  • I think the key misunderstanding for example 2 is that you think a copy constructor must use `const`, but that is not true. The "class.copy" section of the C++ standard explicitly says a constructor like `test(test&)` is a copy constructor. – David Grayson Apr 20 '22 at 05:48
  • my question is : How can a value be taken as an rvlaue (test&& ) and an lvalue ( test& ) ????? @David Grayson – f877576 Apr 20 '22 at 05:57
  • Since this is the `return` statement, the object in question is about to be destroyed, so it's fine to just treat it as an rvalue, since it won't be used later. Are you looking for a specific reference ot the C++ standard? I think Anoop Rana's answer covers that. – David Grayson Apr 20 '22 at 06:02
  • Okay how can an rvalue be taken by a constructor which takes an lvalue ? @David Grayson – f877576 Apr 20 '22 at 06:05
  • The object can be treated as an rvalue or an lvalue so it can go into either constructor, as explained in Anoop's answer. Remember the object is about to be destroyed, so it's OK if we move it (potentially putting it into a different state) or just copy it. Either way it will get destroyed later. Is there a specific thing you're worried about? – David Grayson Apr 20 '22 at 06:07
  • when you say (potentially putting it into a different state) you mean make it rvalue and copy it as rvalue , right? and when you say ( or just copy it. ) you mean copy it as it is ( lvalue ) , right ? @ David Grayson – f877576 Apr 20 '22 at 06:12
  • Yes, that is right. – David Grayson Apr 20 '22 at 06:13
  • the explanations of the two sentences ( (potentially putting it into a different state) , ( or just copy it. ) ) is right ? @David Grayson – f877576 Apr 20 '22 at 06:17
  • Yes, that is right, and I already said it is right. – David Grayson Apr 20 '22 at 06:20

1 Answers1

3

The behavior of your program can be understood with the help of 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;
}
Jason
  • 36,170
  • 5
  • 26
  • 60
  • first thank you very much for answering my question . and I have one more question when I try to compile the code ( EXAMPLE 1 ) using a different compiler ( a C++14 compiler ) and ` test(test&& s)` was not appeared . That is due to Copy elision , right ? ( I used in my question a C++98 compiler ) @Anoop Rana – f877576 Apr 20 '22 at 05:58
  • @f877576 Yes, it is due to [copy elison](https://en.cppreference.com/w/cpp/language/copy_elision). You can even confirm this by providing the `-fno-elide-constructors` flag to the compiler. For example when you provide this `-fno-elide-constructors` flag you'll get the output `test() test(test&& s)`. [Demo with flag](https://wandbox.org/permlink/cHBbd3bhOPK9hrTp). And if you don't provide this flag, the compilers are allowed to elide copy/move construction. [Demo without flag](https://wandbox.org/permlink/kt1YIAGl1DJi9Pjg). And you're welcome. – Jason Apr 20 '22 at 06:06
  • Is ( ctor ) an abbreviation of the word ( constructor ) ? @Anoop Rana – f877576 Apr 20 '22 at 07:26
  • @f877576 Yes "ctor" stands for constructor while "dtor" stands for destructor. – Jason Apr 20 '22 at 07:50
  • What is the meaning of ( a synthesize move constructor ) ? @Anoop Rana – f877576 Apr 21 '22 at 09:35
  • @f877576 Under some conditions, the compiler will automatically/implictly declare the move ctor for us. This compiler generated move ctor is called *synthesized move ctor*. See [Implicitly-declared move constructor](https://en.cppreference.com/w/cpp/language/move_constructor#Implicitly-declared_move_constructor) for more details. For example, you might already know that under certain conditions the compiler will automatically generate a default ctor. In that case, we say that compiler generated default ctor as *synthesized default ctor*. – Jason Apr 21 '22 at 09:42
  • @f877576 Also refer to [what is a synthesized constructor](https://stackoverflow.com/questions/9322728/what-is-a-synthesized-constructor). – Jason Apr 21 '22 at 09:52
  • I am sorry but I did not know that @Anoop Rana – f877576 Apr 22 '22 at 04:13
  • @f877576 It's okay. I suspected that you weren't aware of this feature of Stackoverflow:) – Jason Apr 22 '22 at 04:15
  • So any constructor I do not write it ( the compiler automatically/implictly declares it ) is a synthesize constructor ???? @Anoop Rana – f877576 Apr 22 '22 at 05:06
  • @f877576 The compiler will generate a particular ctor under some condition(specified by the standard c++). Also, the word "synthesized" in the context that we're talking means "automatically created". So whatever ctor is automatically generated by the compiler we call that ctor a *"synthesized ctor"*. – Jason Apr 22 '22 at 05:12