2

e.g.

#include <iostream>
using namespace std;


struct Point {
    int x;
    int y;
};

int main() {
    Point p1 {1};
    Point p2 {.y=2};

    cout << p1.x << ", " << p1.y << endl;
    cout << p2.x << ", " << p2.y << endl;
    return 0;
}

https://ideone.com/DlJRfU

Is there any way to force the caller to initialize all struct members when using initializer lists? I want to ensure both x and y are set by the caller in this example.

I also want to allow designated initializers. Many of our structs are very long and that syntax adds much-needed clarity.

mpen
  • 272,448
  • 266
  • 850
  • 1,236

5 Answers5

5

Something like what you want is possible in practice by making the aggregate meaninglessly templated:

template<class Bad>
struct PointImpl {
  int x=Bad(),y=Bad();
};
using Point=PointImpl<void>;

void f() {
  Point p0;             // error
  Point p1{1};          // error
  Point p2{.y=2};       // error
  Point p3{.x=1};       // error
  Point p4{.x=1,.y=2};  // OK
}

I won’t say I morally agree with this sort of “defeat the client” approach, and it’s not even clear from the standard that default member initializers should work like default arguments this way ([temp.inst]/3, /11, and /13 don’t mention them at all), but implementations agree that they do.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • Woah, neat trick! Why does this work? I can't quite figure it out. – Catskul May 12 '22 at 20:09
  • 1
    @Catskul: If you fail to supply an initializer for any member, it attempts to use the default member initializer, but that's ill-formed because it tries to initialize from `void()`. – Davis Herring May 12 '22 at 23:13
  • So the ill-formed-ness is kind of using SFINEA-like pattern, but it's just kind of hard to see since it's happening behind the scenes. – Catskul May 13 '22 at 17:57
4

You could add a 2-parameter constructor. That would force initializing with both parameters.

Point(int x, int y) : x(x), y(y) {}
Point() = default; // to maintain behaviour of default constructor

then

Point p1{1, 2}; // OK
Point p1 {1}; // error: no matching constructor for initialization of 'Point'
Point p2 {.y=2}; // error: no matching constructor for initialization of 'Point'

Note that this means Point is no longer an aggregate, which means you can no longer use designated initializers.

juanchopanza
  • 223,364
  • 34
  • 402
  • 480
3

Sure, set them yourself. With

struct Point {
    int x = 0;
    int y = 0;
};

x and y will always be initialized. If you do not provide a value for any of the members, the default value (0 in this case) will be used.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 1
    The key to the question being "force the **caller**". I don't want default values. Default values cause bugs when new members are added. – mpen May 26 '20 at 19:49
  • 1
    @mpen If you want to keep the class as an aggregate, AFAIK this is your only option. – NathanOliver May 26 '20 at 19:51
2

You declared an aggregate

struct Point {
    int x;
    int y;
};

If a member of an aggregate is not initialized explicitly using an initializer list, it is initialized as an empty pair of braces is used to initialize it. That is, this initialization:

Point p1 {1};

is equivalent to:

Point p1 {1, {}};

that means that the data member y will be zero-initialized.

If you want to provide that data members of an aggregate would be initialized even when the user did not use a list initialization, you can use default initializers, for example:

struct Point {
    int x = 0;
    int y = 0;
};
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Vlad from Moscow
  • 301,070
  • 26
  • 186
  • 335
1

What you want is impossible in the language. In C++, a type cannot simultaneously force an initialization invariant on the user (like 'you must explicitly initialize all members') and be an aggregate (a type whose component objects can be initialized with any legal value of that type). If you want a type to be an aggregate (and therefore permit aggregate initialization), then by definition you are OK with a user implicitly value initializing some of the subobjects of that type. If you want a type to be initialized in a certain, very specific way, then it can't be an aggregate.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982