1

I need to initilise an member array of a class with a non-default constructor and without using the copy constructor.

I have the following two classes:

class MemberClass
{
  public:
    MemberClass(int id) {  /* Do stuff */ }; // Define non-default ctor
    MemberClass(const MemberClass& other) = delete; // Delete copy ctor
    ~MemberClass() { /* Do stuff */ }; // Overide default dtor
};

class ContainerClass
{
  private:
    MemberClass mem[2];

  public:
    ContainerClass(int id)
        : mem { {id} , {id} }
          {}
};

which upon compiling gives the following error:

error: use of deleted function ‘MemberClass::MemberClass(const MemberClass&)’
         : mem { {id} , {id} }

but I cannot figure out a way to initialise the mem array without defining a copy constructor. I've found answers from here and here explaining that copy-elision is occuring and a copy-ctor is needed to compile but should be removed by the compiler. The MemberClass should never be copied, so defining a copy-ctor just for this initialisation seems very awkward and prone to more difficult debugging elsewhere.

If the MemberClass only has a default constructor then there is no issue given by the compiler. Nor is there any issue given if mem is not an array and is just a single MemberClass object. My only issue is with initialising this array with the non-default ctor, and without using copy-ctor.

Weirdly, if I do not define a destructor I don't get any compilation error, which seems like a clue.

Is there a "correct" way to do this sort of initialisation?

1 Answers1

0

I think this is a bug in gcc.

As specified by C++17 [dcl.init.aggr]/3 (this text was substantially the same in C++14):

When an aggregate is initialized by an initializer list as specified in 11.6.4, the elements of the initializer list are taken as initializers for the elements of the aggregate, in order. Each element is copy-initialized from the corresponding initializer-clause. [...] If an initializer-clause is itself an initializer list, the member is list-initialized,

I'll analyze three different cases here:

  • Case 1: mem { id, id }
  • Case 2: mem { MemberClass{id}, MemberClass{id} }
  • Case 3: mem { {id}, {id} }

In Case 1, mem[0] is copy-initialized by the expression id. This is the same sort of initialization as MemberClass x = id;. It is covered by dcl.init/17.6.2 (copy-initialization from expression of a different type).

The behaviour is that the initializer is converted to a prvalue (i.e. MemberClass{id}) which then direct-initializes the target. In C++14 this was ill-formed: although it is a copy elision context, a valid copy constructor must still exist. In C++17 it is well-formed: initializing an object from a prvalue of the same type is the same as initializing the object using the constructor specified by the prvalue (so-called "guaranteed copy elision").

Case 2 is similar to case 1: it uses 17.6.1 (initialization from expression of the same type) and the same analysis for Case 1 applies.

However, Case 3 is different. As per the last bold quote from dcl.init.aggr/3, mem[0] is list-initialized by {id}, i.e. the code should behave the same as MemberClass z {id};. There is no temporary or copy operation even in C++14.

So the correct behaviour is:

  • C++14 - Case 3 is correct, Case 1 and 2 ill-formed.
  • C++17 - All cases correct.

The error messages emitted by gcc suggest that it in C++14 mode is treating Case 3, your code, the same as the other two cases. And in C++17 mode it never got the memo about guaranteed copy elision.

M.M
  • 138,810
  • 21
  • 208
  • 365