Here follows a more detailed answer for those who like a bit more insight in the language mechanics related to array/struct initialization.
Initialization versus assignment
First of all it is important to know the difference between initialization and assignment. Both are done using one of the assignment operators, most commonly the simple assignment operator =, or otherwise one of the compound assignment operators (*= /= %= += -= <<= >>= &= ^= |=). The difference between initialization and assignment:
- Initialization is when you give initial value(s) to an object at the point where the variable is defined, using an initializer list
{ ... }.
- Assignment is when you give values to an object later on.
Initialization may happen at compile-time or during program launch, whereas assignment always happens in run-time. This difference matters in many scenarios, for example if we do this:
static int x = 1;
static int y = x;
Then the initialization of y is invalid since initialization of static storage duration objects must be done using constant expressions. However, would we just write y=x; later: then that is assignment, not initialization, and that's perfectly fine.
(The difference between initialization and assignment is even more important in C++ where you have constructors and overloaded assignment operators.)
Going back to the question, MY_TYPE a; ... a = { true, 15, 0.123 } is invalid C because a is first defined but not initialized. Then later an attempt to assign a value to a occurs, but that isn't even valid C syntax. This is because the { ... } is an initializer list, which may only be present during initialization, not during assignment.
This is particularly true for arrays where the C language simply does not allow assignment from arrays to arrays. As shown in some other answers, struct has a work-around for run-time assignment, namely by creating a temporary object known as a compound literal, which is written using the syntax
(type){initializer_list}.
MY_TYPE a;
a = (MY_TYPE){true, 15, 0.123};
Here (MY_TYPE){true, 15, 0.123} is a compound literal, an anonymous temporary object. That object is initialized using the provided initializer list. Then the temporary object is assigned to another struct object a of the same type, which is fine for structs specifically, but won't work for arrays.
The initialization rules of C
Whenever initializing an array or struct, this must be done using an initializer list with the curly braces {}, aka "brace-enclosed list".
The initializer rules work so that the compiler looks at the array or struct being initialized and starts with the first item. In case of arrays that is the object at index [0]. In case of structs it is the first member in the list: flag in the example struct from the question.
This first object is then set as the current object during initialization (C23 6.7.11). The compiler looks at the first item in the initializer list and then attempts to give that one to the current object. This is done as per "the rules of assignment" (C23 6.5.17), which I won't go into here, but come with a little bit of type safety. After that, the current object to be initialized becomes the next item in the array or the next member in the struct, which will get set to the next item present in the initializer list. The program will keep doing this until it runs out of objects to initialize in the array/struct.
If the amount of items in the array/struct matches the amount of items in the initializer list, then all is well.
If the amount of items in the array/struct are fewer than the amount of items in the initializer list, then that's invalid C.
It is "a constraint violation". C23 6.7.11: "No initializer shall attempt to provide a value for an object not contained within the entity being initialized. The compiler will give a message about that, maybe something slightly cryptic like "excess elements in struct initializer".
If the amount of items in the array/struct are more than the amount of items in the initializer list, but there is at least one initializer present, then we have a partial initialization. In this case, the items that had no matching initializer are initialized to zero/NULL depending on their type.
(Older C standards call this "initialization as if it had static storage duration", C23 calls it "default initialization" but it works the same.)
In practice, initialization might happen all at once, the mechanics with "the current object" is just the theory describing how the compiler should act when pairing together items in the array/struct with the initializer list.
Arrays/structs within arrays/structs
Looking into more advanced rules, we may have initializer lists within the initializer list - for initializing structs within structs etc. For example:
typedef struct {
int x;
int y;
} inner;
typedef struct MY_TYPE {
inner in;
bool flag;
short int value;
double stuff;
} MY_TYPE;
MY_TYPE a = { {1,2}, true, 15, 0.123 };
Here it will first set in as the current object, then (recursively) within that one pick the first item in.x as the current object. It is good practice to provide the inner brace-enclosed initializer list as above. But it is actually not mandatory! If we skip braces then the compiler will attempt to set the current object the best it can given the order of the items, so we could create an initializer list like this:
MY_TYPE a = { 1,2, true, 15, 0.123 };
This is bad practice and the compiler might warn, but it is valid C. The list is iterated through as before, by shifting current object and then picking the next item in the initializer list to initialize the object with - regardless if there are braces present or not.
This is convenient in one way, because we may write an initializer list like MY_TYPE a = {0};. What happens here is that the first struct member is initialized to zero. Then the initializer list ends - as we learned before this is a partial initialization and the remaining objects will then also get set to 0/NULL. And that's why we can initialize a whole array or struct to zero like this, regardless of any number of sub-objects present. But it won't work with MY_TYPE a = {1};, here the first object gets set to 1 and the rest to zero, not all of them to 1 as was perhaps the intention.
But obviously leaving out braces is also dangerous, if we forget one object or mix up initializers. In my example with the inner struct, suppose we forget to set the inner struct's member y in the initializer list:
MY_TYPE a = { 1, true, 15, 0.123 };. That will unfortunately compile, in.y will get initialized to true instead, then the rest of the items get the wrong initializer too, until the last item stuff which now suddenly isn't initialized explicitly, it gets set to zero since it becomes a partial initialization. Making slips and bugs like that is easy when the initializer lists are long and complex.
Initialization in modern C
With the C99 version of C, an alternative way of initialization was added for clarity. Instead of manually keeping track of which array/struct item that matches with which initializer in the initializer list, we can state it explicitly with a feature called designated initializers. For arrays it may look like this, with an inner [] naming the object index to initialize:
int arr[3] = { [0]=1, [1]=2, [2]=3 };
And for structs, it is done by writing a . dot, then the member's name:
MY_TYPE a = { .flag=true, .value=15, .stuff=0.123 };
This is much more explicit and clear, particularly when it comes to structs. Bugs related to miscounting the amount of members/initializers can be eliminated and we create a direct relation between the object to initialize and the initializer instead of relying on the old mechanism with implicit setting of next current object. It is really a great feature, to the point where using the older form without designated initializers for structs becomes questionable, since it has a much higher potential for accidental bugs.
Designated initializers do use the same initialization concept of setting the current object, but instead of automatically picking the next member in the struct as the current object, it jumps to the one we requested and then makes than the current object and initializes that one.
It is not entirely unproblematic though. We can combine designated initializers with the older form of letting the next current object determine initialization:
int arr[3] = { [1]=2, 3 };
Here, only items at index 1 and 2 are initialized explicitly. First we requested index 1 explicitly with a designed initializer, then the next current object is picked automatically as the next one after index 1, so index 2. Whereas index 0 was not initialized explicitly here - we get partial initialization and it gets initialized to zero.
Obviously this can become unreadable and dangerous, and with designated initializers we might even end up initializing an item twice. Best practice is therefore to always initialize all objects present, particularly whenever designated initializers are used.
Initialization in even more modern C (C23)
C23 did some mostly cosmetic changes to terminology with the introduction of default initialization, which happens to objects that were not initialized explicitly during partial initialization.
But C23 also allows an empty initializer MY_TYPE a = {}; which essentially works just like {0} did previously, with the slight cosmetic difference that instead of setting the first object to zero and the rest as if it had static storage duration, all objects are now initialized according to default initialization.
Before C23, some compilers like gcc supported non-standard extensions for an empty initializer lists.
Order of evaluation during initialization/assignment
Note that the order in which an array/struct is initialized is a separate matter from the order of evaluation of items in the initializer list. The items in the initializer list are "indeterminately sequenced" (C23 6.7.11). Meaning that in a case like int arr[] = { f1(), f2(), f3() };, we can't know the order in which the functions are executed - likely all of them are before initialization even begins, in an unspecified order. Similarly, something like int arr[] = {i, i++}; invokes undefined behavior.
Best practice here is to never put any expressions with side effects (assignments, ++ operators etc) inside an initializer list.