a little confusion about copy return value.

came across this code on cprogramming.com about move semantics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  #include <iostream>
 
using namespace std;
 
vector<int> doubleValues (const vector<int>& v)
{
    vector<int> new_values;
    new_values.reserve(v.size());
    for (auto itr = v.begin(), end_itr = v.end(); itr != end_itr; ++itr )
    {
        new_values.push_back( 2 * *itr );
    }
    return new_values;
}
 
int main()
{
    vector<int> v;
    for ( int i = 0; i < 100; i++ )
    {
        v.push_back( i );
    }
    v = doubleValues( v );
}


author claimed new_values will have to be copied twice, what I understand is for first time, new_values gets copied to the temporary for return value, and then this temporary value gets copied a second time to v, thus twice, do i get it right or am I missing something?
The vector will not get copied. It will just get moved once or twice.

On line 23 when v is assigned the return value the move assignment operator will be used. This means that the vector elements will be transferred from the returned vector to v without being copied.

To construct the temporary return value the move constructor is used because you are returning a local variable (and no type conversion is necessary). The compiler is allowed to optimize away this move by constructing new_values directly in place where the return value is expected. In this simple case, where you only ever return the same local variable, all major compilers will be able to do this optimization.
Last edited on
I think your deduction about what the author means is correct, but a few years ago new functionalities have been added to C++, notably move semantic and copy elision.
‘Copy elision’ in short means the compiler is authorised not to carry out any copy, but move the object instead, if it reckons it can be done safely.

I think in a code like that, a modern compiler would choose moving rather than copying.
Here is what my compiler does:
example 1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include <utility>
#include <vector>

template <typename T>
class LoudVector : public std::vector<T> { // not a good idea
public:
    LoudVector()
        : std::vector<T>()
    {
        std::cout << "No-argument constructor called\n";
    }

    LoudVector(std::size_t size)
        : std::vector<T>(size)
    {
        std::cout << "one-argument constructor called\n";
    }

    LoudVector(const LoudVector & lv)
        : std::vector<T>(lv)
    {
        std::cout << "LoudVector: copy constructor called\n";
    }
    
    LoudVector & operator=(const LoudVector & lv)
    {
        std::vector<T>::operator=(lv);
        std::cout << "LoudVector: copy assignment operatot called\n";
        return *this;
    }

    LoudVector(const LoudVector && lv)
        : std::vector<T>(std::move(lv))
    {
        std::cout << "LoudVector: move constructor called\n";
    }

    LoudVector & operator=(LoudVector && lv)
    {
        std::vector<T>::operator=(std::move(lv));
        std::cout << "LoudVector: move assignment operatot called\n";
        return *this;
    }
};
 
LoudVector<int> doubleValues (const LoudVector<int>& v)
{
    LoudVector<int> new_values;
    new_values.reserve(v.size());
    for (auto itr = v.begin(), end_itr = v.end(); itr != end_itr; ++itr )
    {
        new_values.push_back( 2 * *itr );
    }
    std::cout << "I'm about to return new_values...\n";
    return new_values;
}
 
int main()
{
    LoudVector<int> v;
    for ( int i = 0; i < 100; i++ )
    {
        v.push_back( i );
    }
    v = doubleValues( v );
    std::cout << "I've just executed \"v = doubleValues( v );\".\n";
}


My output:
No-argument constructor called
No-argument constructor called
I'm about to return new_values...
LoudVector: move assignment operatot called
I've just executed "v = doubleValues( v );".


example 2 (perhaps clearer):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <utility>

struct Test {
    int myint;
    Test()
        : myint { 13 }
    {
        std::cout << "No-argument constructor called\n";
    }

    Test(int myint_arg)
        : myint { myint_arg }
    {
        std::cout << "One-argument constructor called\n";
    }

    Test ( const Test & t)
    {
        myint = t.myint;
        std::cout << "Copy constructor called\n";
    }

    // Trivial example, no copy&swap syntax:
    Test & operator=(const Test & t)
    {
        myint = t.myint;
        std::cout << "Copy assignment operator called\n";
        return *this;
    }

    Test ( Test && t)
    {
        myint = std::move(t.myint); // nonsense
        std::cout << "Move constructor called\n";
    }

    Test & operator=(Test && t)
    {
        myint = std::move(t.myint); // nonsense
        std::cout << "Move assignment operator called\n";
        return *this;
    }

    ~Test() {};

    friend std::ostream & operator<<(std::ostream & os, const Test & t)
    {
        return (os << t.myint);
    }
};

Test doubleTestVal(const Test& t);

int main()
{
    std::cout << "main(): Test t1 { 666 } --> ";
    Test t1 { 666 };
    std::cout << "main(): Test t2 { t1 } --> ";
    Test t2 { t1 };
    std::cout << "main(): Test t3 --> ";
    Test t3;
    std::cout << "main(): t3 = t2 --> ";
    t3 = t2;
    std::cout << "t3: " << t3 << '\n';
    std::cout << "main(): t3 = doubleTestVal(t3)\n";
    t3 = doubleTestVal(t3);
    std::cout << "t3: " << t3 << '\n';
}

Test doubleTestVal(const Test& t)
{
    std::cout << "\ndoubleTestVal(): Test t2 --> ";
    Test t2;
    std::cout << "doubleTestVal(): t2 = t --> ";
    t2 = t;
    t2.myint *= 2;
    std::cout << "doubleTestVal(): return t2 --> ";
    return t2;
}


My output:
main(): Test t1 { 666 } --> One-argument constructor called
main(): Test t2 { t1 } --> Copy constructor called
main(): Test t3 --> No-argument constructor called
main(): t3 = t2 --> Copy assignment operator called
t3: 666
main(): t3 = doubleTestVal(t3)

doubleTestVal(): Test t2 --> No-argument constructor called
doubleTestVal(): t2 = t --> Copy assignment operator called
doubleTestVal(): return t2 --> Move assignment operator called
t3: 1332


I don’t know if what above may be helpful or confusing…
Enoizat wrote:
a few years ago new functionalities have been added to C++, notably move semantic and copy elision.
For copy elision, "a few decades" is more like it (it became popular in 1992-1994, and C++98 just recorded this common practice)
Enoizat wrote:
‘Copy elision’ in short means the compiler is authorised not to carry out any copy, but move the object instead, if it reckons it can be done safely.
no, "copy elision" means no move takes place (and no copy, naturally). There are cases where moves replace copies (as of C++11), but for that to happen, copy elision has to first fail.
Last edited on
thanks for all the replies, so you would say even the code doesn't have any explicit move constructor, any modem compiler would be still able to optimize it away by actual moving the object on the fly? it's just not guaranteed, you just can't make any assumption by leaving move constructor out? for this code, even you don't have move constructor written, new_value will be actually move once or twice to v?
weee wrote:
for this code, even you don't have move constructor written, new_value will be actually move once or twice to v?
new_value is an std::vector<int>, which has a move constructor.
Last edited on
std::vector has a move constructor.

The vector is guaranteed to be moved. Copying the vector is not allowed in this particular situation. What is not guaranteed is the number of moves.

Note that if you used the returned vector to construct a new vector ...
 
vector<int> v2 = doubleValues( v );
... the compiler would probably optimize this so that it doesn't move at all (it's not guaranteed though).

It might be tempting to use std::move explicitly ...
 
return std::move(new_values);
... but this unfortunately disables copy elision so that you only get a guaranteed move.

Note that implicit move/copy elision only works because you are returning a local variable. If you want to return a non-local variable using move you have to use std::move.
Topic archived. No new replies allowed.