Can C++ aggregate initialization be used to construct an instance of a class which implements an interface?
Asked Answered
O

2

10

I am hoping someone can give me the technical details of why the following will not compile, and if possible, a work around.

I have an existing struct called Foo, and code which uses initializer lists to create instances of Foo. This code compiles and works:

struct Foo {
    int id1;
    int id2;
};

int main()
{
    Foo f({1,2});

    return f.id1;
}

I would like Foo to implement an interface going forward:

struct Interface {
    // All pure virtual methods, but this won't compile even if empty
};

struct Foo : public Interface{
    int id1;
    int id2;
};

int main()
{
    Foo f({1,2});

    return f.id1;
}

This code no longer compiles, with errors in the vein of

cannot convert argument 1 from 'initializer list' to 'const _Ty &'

(Error changes depending on your exact compiler.)

I have found this section of the standard relating to aggregate initialization:

[dcl.init.aggr]/1 An aggregate is an array or a class (Clause 12) with 1.1 no user-provided, explicit, or inherited constructors (15.1), 1.2 no private or protected non-static data members (Clause 14), 1.3 no virtual functions (13.3), and 1.4 no virtual, private, or protected base classes (13.1).

Though I am not actually sure if aggregate initialization is what's occurring here. Can someone explain the error that's occurring, and if possible, offer changes I could make to the interface? I have several existing structs which need this interface, and lots of existing code which uses this form of initialization, and I'd like to rewrite as little of it as possible. Thank you!

Obstreperous answered 8/12, 2018 at 0:51 Comment(0)
D
11

You need to initialize the base class even though it is empty:

Foo f({{},1,2});

see it live on godbolt

Further down in the standard in the section you are referring to we can see an example of this in [dcl.init.aggr]p4.2:

struct base1 { int b1, b2 = 42; };
struct base2 {
  base2() {
   b3 = 42;
 }
 int b3;
};

struct derived : base1, base2 {
 int d;
};

derived d1{{1, 2}, {}, 4};
derived d2{{}, {}, 4};

initializes d1.b1 with 1, d1.b2 with 2, d1.b3 with 42, d1.d with 4, and d2.b1 with 0, d2.b2 with 42, d2.b3 with 42, d2.d with 4. —end example]

Also see [dcl.init.aggr]p2 which explains what the elements of an aggregate are:

The elements of an aggregate are:

-for an array, the array elements in increasing subscript order, or
-for a class, the direct base classes in declaration order, followed by the direct non-static data members ([class.mem]) that are not members of an anonymous union, in declaration order.

and [dcl.init.aggr]p3 says:

When an aggregate is initialized by an initializer list as specified in [dcl.init.list], the elements of the initializer list are taken as initializers for the elements of the aggregate. ...

Note, the answer assumes C++17 or greater since before C++17 an aggregate was not allowed to have a base class.

Depressed answered 8/12, 2018 at 1:1 Comment(4)
+1 Would it be ok to dodge the bullet with struct Interface {}; struct Foo : public Interface { int id1; int id2; }; int main() { Foo f = { .id1 = 1, .id2 = 2 }; return f.id1; } and claim it's for C++20? :-) `Bobbiebobbin
@TedLyngmo I don't know if I would recommend that, both gcc and clang support C99 designated initializers as an C99 extension but clang reports their C++20 support for designated initializers as partialDepressed
@NicolBolas I think the prevailing thought it that C++ tag means prevailing C++ and the quote the OP uses is C++17 or greater since C++14 does not allow this but I can definitely add a note wrt.Depressed
@ShafikYaghmour I wouldn't recommend it either, but it is(/will perhaps become) valid C++20, isn't it (except for the uninlitialized base)?Bobbiebobbin
S
1

@ShafikYaghmour explained why when Interface is empty, aggregate initialization can not be done as it used to be.

But if Interface has virtual functions, as suggested in the question, the derived class from Interface will not be an aggregate. So the class that implements Interface and holds data members as Foo must implement a constructor. The simplest way I see (which, depending on the "triviality" of the data members, might not be the most efficient in term of speed) is this:

struct Interface {
   // All pure virtual methods, but this won't compile even if empty
   virtual void bar() =0;
   };

struct Foo_data{ //change the name of the aggregate
  int id1;
  int id2;
  };

struct Foo
  :Interface  //Foo has virtual function => Foo is not an aggregate
  ,Foo_data
  {
  Foo() =default;
  Foo(Foo_data data):Foo_data(std::move(data)){}//a constructor must be provided
  void bar() override {}
  };

int main(){
  Foo f({1,2});
  return f.id1;
  }
Spano answered 8/12, 2018 at 10:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.