Friday, 5 May 2017

c++ - Why does adding a destructor change the copy constructor behavior of this struct?




I have a bit of code that I find confusing. In particular, when I try to add something to a list as an initializer-list - it works until I add a destructor - then it starts trying to find a copy constructor.



This doesn't seem to be completely consistent behavior. Take this minimal example:



#include 
int main()
{
class MemberType

{
public:
MemberType() {}
MemberType(MemberType&& copy) { }
};
struct ListItemType
{
MemberType x;
~ListItemType() {}
};

std::list myList;
myList.push_back({MemberType()});
return 0;
}


This fails to compile in GCC and VS2015 because the push_back attempts to access the ListItemType copy constructor:



main()::ListItemType::ListItemType(const main()::ListItemType&)



(as per my understanding). This seems to make some sense as the list push_back will make a copy (since there is no move constructor), except this is not the behavior if you remove the destructor. Comment out the destructor and the compilation succeeds as expected.



That said, the following works fine even with the destructor - no copy or move constructors are needed to satisfy it. This seems like the same behavior to me though.



ListItemType foo = { MemberType() };


Finally, if you delete or comment out the move constructor of MemberType - the compilation again succeeds - meaning the following will compile.




#include 
int main()
{
class MemberType
{
public:
MemberType() {}
};
struct ListItemType
{

MemberType x;
~ListItemType() {}
};
std::list myList;
myList.push_back({MemberType()});
return 0;
}


Can someone please explain the behavior here? Why does the push_back try to access the copy constructor of ListItemType - but only if ListItemType has a destructor and MemberType has a move constructor?



Answer



The behaviour you are observing is generated by the rules that govern whether implicit copy or move constructors are generated by the compiler:



Implicit Move



If not defined, a move constructor is implicitly declared for a class if:




  • the class has no user defined copy constructors; and

  • the class has no user defined copy assignment or move assignment operators; and


  • the class has no user defined destructor.



Implicit Copy



If not defined, a copy constructor is implicitly deleted for a class if:




  • the class has a user defined move constructor; or

  • other reasons which are not relevant here ...







In your question you have several cases:



Case 1




  • ListItemType has a destructor


  • MemberType has a move constructor



The implicit move constructor for ListItemType has been deleted due to the existence of the destructor. Thus push_back must use a copy constructor to place the ListItemType into the list.



In that case, a copy constructor for ListItemType cannot be implicitly declared as one of its data members (MemberType) contains a move constructor, which prevents an implicit copy constructor for MemberType being generated.



Case 2





  • ListItemType has no destructor

  • MemberType has a move constructor



A move constructor can be implicitly generated for ListItemType and used to move the value into the list.



Case 3




  • ListItemType has a destructor


  • MemberType has no move constructor



An implicit copy constructor for ListItemType and MemberType can be generated and used to copy the value into the list.



Finally, the expression ListItemType foo = { MemberType() }; is aggregate initialization and follows different rules. In either case MemberType will have either a move or copy constructor that is sufficient for aggregate initialization.


No comments:

Post a Comment

c++ - Does curly brackets matter for empty constructor?

Those brackets declare an empty, inline constructor. In that case, with them, the constructor does exist, it merely does nothing more than t...