Playing with the idea of creating new types in C using a struct, I wondered about assignment.
I was surprised that the following C program compiles and runs producing the expected output.
I had never considered that it was possible to use a cast on a struct
initializer list to assign a value to a struct
variable as in x = (struct JJ){ 5 };
or x = (struct JJ){ jjR1, x.jj2, x.jj3, {llR1, x.kk.ll2} };
.
Why does this work and when was it allowed?
After a series of modifications and embellishments, I arrived at the following C source which much to my delighted surprise, compiles with Visual Studio 2019 with Properties -> General -> C Language Standard set to Default (Legacy MSVC)
.
Edited with new source example that expands and extends the original posted source.
I received an answer to my original posted source but in a fit of playful curiosity as to how far this can be pushed, while discovering how much I've missed working for a couple of decades in a very old C source code base from before C99, here a more complex exploration.
Perhaps there are a few bits here that may help others.
#include <stdio.h>
#include <memory.h>
// first a couple of helper functions to try something interesting though not very useful.
// bitwise and the two memory areas.
void* memand(void* dest, void* src, size_t leng)
{
unsigned char* pDest = dest;
unsigned char* pSrc = src;
if (leng > 0) for (size_t j = 0; j < leng; j++) *(pDest++) &= *(pSrc++);
return dest; // return address of the destination to allow chaining of functions.
}
// bitwise or the two memory areas.
void* memor(void* dest, void* src, size_t leng)
{
unsigned char* pDest = dest;
unsigned char* pSrc = src;
if (leng > 0) for (size_t j = 0; j < leng; j++) *(pDest++) |= *(pSrc++);
return dest; // return address of the destination to allow chaining of functions.
}
// bitwise not the memory area, toggling the bits from zero to one and one to zero.
void* memnot(void* dest, size_t leng)
{
unsigned char* pDest = dest;
if (leng > 0) for (size_t j = 0; j < leng; j++, pDest++) (*pDest) = ~*pDest;
return dest; // return address of the destination to allow chaining of functions.
}
int main()
{
#define charbufsize 256 // char buffer size
#define intaraysize 10 // int array size
char myaray[charbufsize] = "this is a string"; // large char buffer initialized with a text string
int iaray[intaraysize] = { 1,2,3,4,5,6,7,8,9,0 }; // integer array initialized with values.
struct JJ {
int jj1;
int jj2;
int jj3;
struct KK {
long ll1;
long ll2;
} kk;
};
int jjR1 = 21; // used as a replacement value for member jj1 in struct JJ above.
long llR1 = 401; // used as a replacement value for member ll1 in struct KK of struct JJ above.
struct AA {
char myaray[charbufsize]; // same size as the char buffer myaray defined above.
};
struct BB {
int iaray[intaraysize]; // same size as the integer array iaray defined above.
};
struct AA aatest = { "this another string aatest" };
struct KK kkR1 = { 501, 502 };
struct JJ x = { 0, 1, 2, {3, 4} };
printf("modifying struct variable with constants\n");
printf(" x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
x = (struct JJ){ 5 };
printf(" x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
x = (struct JJ){ 5, 0, 23, {101, 102} };
printf(" x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
x = (struct JJ){ jjR1, x.jj2, x.jj3, {llR1, x.kk.ll2} };
printf(" x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
x = (struct JJ){ 67, 3, x.jj3, kkR1 };
printf(" x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
printf("\nusing memcpy, memand, memor, and memnot functions\n");
// assign literal to part while keeping part two different ways, first works and second does not.
((struct JJ*)memcpy(&x, &(struct JJ) {.jj2 = 122, .jj3 = 123}, sizeof(struct JJ)))->kk = ((struct JJ){ .kk = x.kk }).kk; // assign with snapshot before memcpy()?
printf(" memcpy assign 1: x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
((struct JJ*)memcpy(&x, &(struct JJ) {.jj2 = 222, .jj3 = 223}, sizeof(struct JJ)))->kk = x.kk; // assign after memcpy().
printf(" memcpy assign 2: x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
// obfuscated assignment of literal to change only member x.jj2. Ordinarily you'd just use x.jj2 = 24;
// this creates a struct jj set to zero except for member variable .jj2 which is all ones then does a bitwise not to flip the bits.
// then we and the result with our target to clear only member .jj2 then we bitwise or in the .jj2 value we want.
x.jj1 = 1001; // ensure first member is nonzero so we can test that it stays this value.
x.kk.ll1 = 2001; x.kk.ll2 = 2002; // ditto
memor(memand(&x, memnot(&(struct JJ) { .jj2 = 0xffffffff }, sizeof(struct JJ)), sizeof(x)), &(struct JJ) { .jj2 = 24 }, sizeof(struct JJ));
printf(" x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
#define MACJJ(a,var,m,val) *(a *)memor(memand(&(var), memnot(&(a) { m = 0xffffffff }, sizeof(a)), sizeof(var)), &(a) { m = val }, sizeof(a))
#define MACJJ2(a) (a)
x = MACJJ(struct JJ, x, .jj3, 799);
printf(" using MACJJ: x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
// using initializer list with a defined macro requires additional parenthesis to force the definition into a single macro variable.
x = MACJJ2(((struct JJ) { 101, 102, 103 })); // member kk is set to zero since it is not included in the initializer list.
printf(" using MACJJ2: x.jj1 = %d x.jj2 = %d x.jj3 = %d x.kk.ll1 = %d x.kk.ll2 = %d\n", x.jj1, x.jj2, x.jj3, x.kk.ll1, x.kk.ll2);
printf("\nmodifying entire arrays\n");
// this is using char arrays for holding a string. the char array is a specific size. not a std::string replacement!
printf(" myaray before: \"%s\"\n", myaray);
*(struct AA *)myaray = (struct AA){ "this array 2" }; // modify the array using a struct with an array of the same size.
printf(" myaray after: \"%s\"\n", myaray);
// can we use this construct with a struct defined on the fly at the point we need it? It appears so!
// however it does now define a struct that is visible within the current scope and whose identifier can't be reused.
*(struct t1 { char j[5]; } *) myaray = (struct t1) {"jjj"}; // locally defined struct t1 not visable outside this function.
printf(" myaray using struct t1: \"%s\"\n", myaray);
printf(" iaray before: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d\n", iaray[0], iaray[1], iaray[2], iaray[3], iaray[4], iaray[5], iaray[6], iaray[7], iaray[8], iaray[9]);
*(struct BB*)iaray = (struct BB){ 21,22,23,24,25,26,27,28,29,30 }; // modify the array using a struct with an array of the same size.
printf(" iaray after: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d\n", iaray[0], iaray[1], iaray[2], iaray[3], iaray[4], iaray[5], iaray[6], iaray[7], iaray[8], iaray[9]);
*(struct t2 {int ii[3];} *)iaray = (struct t2){ 0 }; // locally defined struct t2 not visable outside this function.
printf(" iaray zero first 3 with struct t2: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d\n", iaray[0], iaray[1], iaray[2], iaray[3], iaray[4], iaray[5], iaray[6], iaray[7], iaray[8], iaray[9]);
*(struct t2 *)(iaray + 7) = (struct t2){ 0 };
printf(" iaray zero last 3 with struct t2: %d, %d, %d, %d, %d, %d, %d, %d, %d, %d\n", iaray[0], iaray[1], iaray[2], iaray[3], iaray[4], iaray[5], iaray[6], iaray[7], iaray[8], iaray[9]);
return 0;
}
The output of this no longer simple test is:
modifying struct variable with constants
x.jj1 = 0 x.jj2 = 1 x.jj3 = 2 x.kk.ll1 = 3 x.kk.ll2 = 4
x.jj1 = 5 x.jj2 = 0 x.jj3 = 0 x.kk.ll1 = 0 x.kk.ll2 = 0
x.jj1 = 5 x.jj2 = 0 x.jj3 = 23 x.kk.ll1 = 101 x.kk.ll2 = 102
x.jj1 = 21 x.jj2 = 0 x.jj3 = 23 x.kk.ll1 = 401 x.kk.ll2 = 102
x.jj1 = 67 x.jj2 = 3 x.jj3 = 23 x.kk.ll1 = 501 x.kk.ll2 = 502
using memcpy, memand, memor, and memnot functions
memcpy assign 1: x.jj1 = 0 x.jj2 = 122 x.jj3 = 123 x.kk.ll1 = 501 x.kk.ll2 = 502
memcpy assign 2: x.jj1 = 0 x.jj2 = 222 x.jj3 = 223 x.kk.ll1 = 0 x.kk.ll2 = 0
x.jj1 = 1001 x.jj2 = 24 x.jj3 = 223 x.kk.ll1 = 2001 x.kk.ll2 = 2002
using MACJJ: x.jj1 = 1001 x.jj2 = 24 x.jj3 = 799 x.kk.ll1 = 2001 x.kk.ll2 = 2002
using MACJJ2: x.jj1 = 101 x.jj2 = 102 x.jj3 = 103 x.kk.ll1 = 0 x.kk.ll2 = 0
modifying entire arrays
myaray before: "this is a string"
myaray after: "this array 2"
myaray using struct t1: "jjj"
iaray before: 1, 2, 3, 4, 5, 6, 7, 8, 9, 0
iaray after: 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
iaray zero first 3 with struct t2: 0, 0, 0, 24, 25, 26, 27, 28, 29, 30
iaray zero last 3 with struct t2: 0, 0, 0, 24, 25, 26, 27, 0, 0, 0
Function Return
In some cases I've found that returning a small struct
containing both an error status and a value as a tuple handy. In the following I use the Compound Literal to create a tuple of values and return them in a struct
.
Here is the struct
that is used for returning the tuple.
// Return value struct that returns both an error code as well as some data.
// the Mnemonic struct is used to allow assignment of the char array since
// C does not allow assigning an array to another array.
typedef struct {
char x[20];
} Mnemonic;
typedef struct {
int iret; // return code indicating error or not
Mnemonic ray; // an array of data wrapped in a struct
long total; // more data because less ain't more, more is more.
} RetVal;
And here is an example function where the return value is assembled in the return
statement. As a side note, I thought to test out a static assert hack that is needed before C11 introduced the _Static_assert()
.
I used a work around to the C restriction on assigning arrays by wrapping the fixed length char
array that contains a string into a struct
. The assignment requires casting the string array to the wrapping struct
in order to perform the assignment.
RetVal myRetValTest(long total)
{
int iret = 0;
char raygun[16] = { 0 }; // use a deliberately smaller value than Mnemonic to test return.
total += 5;
(total < 12) ? iret = 3 : (iret = 0, total = 0); // make the function change its return value sometimes
strcpy_s(raygun, sizeof(raygun)/sizeof(raygun[0]), "raygun rocks"); // copy a string into the mnemonic area
// static assert to cause compile error if char raygun[] is too large.
// compiler should compile the constants and expression down to a 0 or 1. Visual Studio 2019 handles this just fine.
// compiler error of "error C2196: case value '0' already used" if raygun[] too large.
// NOTE: C11 offers _Static_assert() to replace this hack.
switch (0) { case 0: case sizeof(raygun) <= sizeof(Mnemonic) : ; }
return (RetVal) { iret, * (Mnemonic *) raygun, total }; // create the return value. cast raygun to bypass array asignment not allowed in C restriction.
}
I tried a short test, adding a bit of a twist by inserting the return value into a struct
that contains not only the return value but management data as well though in this case the management data is simply the loop index.
long total = 5L;
for (int i = 0; i < 5; i++) {
struct retvalPlus {
int i; // loop index value
RetVal v; // return value from the function
} myRet = (struct retvalPlus){ i, myRetValTest(total) };
if (myRet.v.iret > 2) {
printf(" myRetValTest: i=%d iret=%d ray = \"%s\" total = %ld\n", myRet.i, myRet.v.iret, myRet.v.ray.x, myRet.v.total);
}
else {
printf(" myRetValTest: i=%d iret=%d\n", myRet.i, myRet.v.iret);
}
total = myRet.v.total;
}
which produced the following output.
myRetValTest: i=0 iret=3 ray = "raygun rocks" total = 10
myRetValTest: i=1 iret=0
myRetValTest: i=2 iret=3 ray = "raygun rocks" total = 5
myRetValTest: i=3 iret=3 ray = "raygun rocks" total = 10
myRetValTest: i=4 iret=0
The syntax you're using is known as a compound literal in C.
in C, a compound literal is an unnamed object defined by a parenthesized type-name followed by an initializer list. Its general form is:
(type_name) { initializer_list }
For example:
struct JJ {
int jj1;
int jj2;
};
struct JJ x;
x = (struct JJ){ 1, 2 };
Compound literals were introduced as part of the C99 standard (ISO/IEC 9899:1999), approved in 1999. Before this, the C89/C90 standards did not define such a construct.
Your example compiles because your compiler (Visual Studio 2019) supports certain C99 features even in legacy or default mode. Even though Microsoft Visual Studio historically did not fully support the C99 standard, later versions (such as Visual Studio 2019 and later) did add incremental support for some parts of C99, including compound literals.