Why isn't move semantic called?

Hi everyone,

I was trying to implement by myself an analogous of the std::pair, which I called ppair. I wrote the following class (since everything is public), which is really simple.

I wrote using vecint = typename std::vector<int,int> in the main to be more coincise.


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
#include <iostream>
#include <utility>
#include <vector>


template<typename T, typename U>
class ppair{

public:
    T first;
    U second;

    ppair(): first{}, second{}{};
    ppair(T& _first, U& _second): first{_first}, second{_second}{}
    ppair(T&& _first, U&& _second): first{std::move(_first)}, second{std::move(_second)}{}

    /*move semantic*/

    //move cstr
    ppair(ppair&& p): first{std::move(p.first)},second{std::move(p.second)}{
        std::cout << "move cstr"<<"\n";
    }
    
//    move assign
    ppair& operator=(ppair&& p){
        std::cout <<"move assignment" <<"\n";
        first=std::move(p.first);
        second=std::move(p.second);
        return *this;
    }
    

};





int main(){
    
    using vecint = typename std::vector<int>;
    vecint v1{1,2,3};
    vecint v2{4,5,6};
    ppair<vecint, vecint> p1{v1,v2}; //custom cstr
    ppair<vecint, vecint> p2{std::move(p1)}; //move cstr
    std::cout << p1.first.size() << " " <<p1.second.size()<< "\t" <<p2.first.size() << " "<< p2.second.size()<<"\n";
    
    ppair<vecint, vecint> p3{}; //def cstr
    p3 = std::move(p2); //move assignment
    std::cout <<p2.first.size()<< " "<<p2.second.size() << "\t" << p3.first.size()<<" " <<p3.first.size()<<"\n";
    

    //WHY move assignment is not called here?
    ppair<vecint, vecint> p4{ppair<vecint, vecint>(vecint{5}, vecint{5})};
    return 0;
}


All the tests for the move semantics work, except for the last one, when I have that for

ppair<vecint, vecint> p4{ppair<vecint, vecint>(vecint{5}, vecint{5})};

only the default constructor is called, but I expected ppair<vecint, vecint>(vecint{5}, vecint{5}) to be considered as an r-value, so that the move semantics is used. My professor told me that I can informally think an r-value as something that cannot be on the left hand side of an assignment. But this is not the case here, because the move constructor is not called.

Why is only the custom constructor invoked, instead of the custom and move constructor?


Actually, I've seen that if I write ppair<vecint, vecint>(vecint{5}, vecint{5}); as a single statement, I don't have any compiler error. Why is that?
Last edited on
Presumably it's just being constructed in place so there's no need to move it.
ppair(T&& _first, U&& _second) is definitely being called, though.
Yes, that r-valued version of the custom constructor is called. So the only way to let it be an r-value reference is to cast it to an r-value by using std::move(). In this case, indeed, first the custom constructor, and then the move constructor, are called.


However, there's still something that I am missing: if I define the following function outside the class

1
2
3
4
template<typename T, typename U>
void foo(ppair<T,U>&& p){
    std::cout << "r-value reference is called" <<"\n";
}


and in the main I call foo(ppair<int, int>(2,3)); it turns that now ppair<int, int>(2,3) is seen as a r-value reference, as can be tested.

Why is this the case now?
Last edited on
You're missing the point entirely.
I'm not saying that it isn't an "r-value reference" in the first case.
Just that there's no reason to call the constructor at all since the object can be "constructed in place" and so there's no need to move it at all.
I'm not good at explaining this kind of thing, and maybe I'm wrong.
Hopefully someone else can explain it.

What I'm talking about is either exactly this or at least related to it:
https://en.cppreference.com/w/cpp/language/copy_elision
Last edited on
Oh, it's my fault because I wasn't aware of "in place" construction. So also in
foo(ppair<int, int>(2,3)); the pair with 2 and 3 is constructed "in place", but since it's a r-value, then foo takes it and doesn't complain, right?
I don't know about that case. I usually think of something being "constructed in place" when there's a non-reference object to construct it into. In that case a temporary object would be created.
https://en.cppreference.com/w/cpp/language/implicit_conversion#Temporary_materialization
> but I expected ppair<vecint, vecint>(vecint{5}, vecint{5}) to be considered as an r-value,
> so that the move semantics is used

In ppair<vecint, vecint> p4{ppair<vecint, vecint>(vecint{5}, vecint{5})};,
the initialiser expression is a prvalue of the same class type as the type of the object being initialised. Because of copy elision (mandatory in C++17, permissible even if there are observable side-effects to move/copy in C++11), the objects are constructed directly without any copy or move.

The details: https://en.cppreference.com/w/cpp/language/copy_elision
@JLBorges thanks, I didn't know it was a rule of the language, and indeed I was really confused.

Btw, I have a side question: do you think is a okay/good-practice to have two custom constructors in this case, i.e. one taking l-value references

ppair(T& _first, U& _second): first{_first}, second{_second}{}

and another one taking r-value references

ppair(T&& _first, U&& _second): first{std::move(_first)}, second{std::move(_second)}{}


?
Last edited on
> do you think is a okay/good-practice to have two custom constructors in this case,
> i.e. one taking l-value references
> ppair(T& _first, U& _second): first{_first}, second{_second}{}
> and another one taking r-value references
> ppair(T&& _first, U&& _second): first{std::move(_first)}, second{std::move(_second)}{}

These two by themselves don't cover all bases; there are four possibilities:
1. lvalue, lvalue
2. lvalue, rvalue
3. rvalue, lvalue
4. rvalue, rvalue

Tip: have a look at constructor (3) of std::pair (it uses perfect forwarding)
https://en.cppreference.com/w/cpp/utility/pair/pair

Also, the constructor taking two lvalue references is not const-correct.
You're perfectly right about the missing cases.

> Also, the constructor taking two lvalue references is not const-correct.

About const-correctness, I should write

ppair(const T& _first,const U& _second): first{_first}, second{_second}{}

so that in acquiring the resources I can't change _first and _second, since it makes no sense to modify them inside a constructor, is this what you meant?
> is this what you meant?

Yes.
Topic archived. No new replies allowed.