In my project I'm making a C class hierarchy similar to, but not the same as Axel-Tobias Schreiner Object-oriented Programming in ansi C see https://www.cs.rit.edu/~ats/books/ooc.pdf. for example.
I'm initializing my object a little bit different than Axel is. I'm running into troubles when I'm passing va_list objects between multiple initializing functions. Say I've an object two, that derives from object one. Then while initializing a two object, I'll need to initialize the one part first, and subsequently initialize the two part. So I'm having 1 init function that calls the public initializer function with arguments that initialize the one part of the two object and arguments that only initializes the two part that extends one object.
The library I'm creating is quite large, but I've extracted a mini project from it that demonstrates the same issues:
CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project (var_args
VERSION "0.0"
LANGUAGES C CXX
)
set(HEADERS "init.h")
set(SOURCES init.c program.c)
add_executable(program ${SOURCES} ${HEADERS})
if (NOT MSVC)
target_compile_options(program PRIVATE -W -Wall -Wextra -pedantic)
else()
target_compile_options(program PRIVATE /W4)
endif()
init.h:
typedef struct _one {
int a;
const char* msg;
} one_t;
/* this struct "derives" from struct _one */
typedef struct _two {
one_t parent;
double pi;
double e;
}two_t;
enum status_t {
STATUS_SUCCES,
STATUS_INVALID_ARGUMENT,
STATUS_ERROR
};
enum init_one_flags {
INIT_ONE_A, // 2nd argument should be of type int
INIT_ONE_MSG, // 3rd argument should be const char*
INIT_ONE_FINISHED, // takes no arugment, but rather tell init1 should be finished.
INIT_ONE_SENTINAL // Keep at the last position.
};
enum init_two_flags {
INIT_TWO_PI = INIT_ONE_SENTINAL, // 2nd arugument should be of type double.
INIT_TWO_E, // 2nd argument shoudl be double.
INIT_TWO_FINISHED, // takes no arugment, but rather tell init2 should be finished.
INIT_TWO_SENTINAL, // for init3...
};
#ifdef __cplusplus
extern "C" {
#endif
int init_two(two_t* two, ...);
//void init_one(one_t* one, ...);
#ifdef __cplusplus
}
#endif
init.c:
#include <stdarg.h>
#include "init.h"
static int priv_init1(one_t* one, va_list list)
{
// use default values;
int a = 0;
const char* msg = "";
int selector, ret = STATUS_SUCCES;
while ((selector = va_arg(list, int)) != INIT_ONE_FINISHED) {
switch (selector) {
case INIT_ONE_A:
a = va_arg(list, int);
break;
case INIT_ONE_MSG:
msg = va_arg(list, const char*);
break;
default:
// unknown argument
return STATUS_INVALID_ARGUMENT;
}
}
one->a = a;
one->msg = msg;
return ret;
}
static int priv_init2(two_t* two, va_list list)
{
double pi = 3.1415, e=2.7128;
int selector, ret = STATUS_SUCCES;
ret = priv_init1((one_t*)two, list);
if (ret)
return ret;
while ((selector = va_arg(list, int)) != INIT_TWO_FINISHED) {
switch (selector) {
case INIT_TWO_PI:
pi = va_arg(list, double);
break;
case INIT_TWO_E:
pi = va_arg(list, double);
break;
default:
return STATUS_INVALID_ARGUMENT;
}
}
two->pi = pi;
two->e = e;
return STATUS_SUCCES;
}
int init_two(two_t* two, ...)
{
int ret;
va_list list;
va_start(list, two);
ret = priv_init2(two, list);
va_end(list);
return ret;
}
program.c:
#include <stdio.h>
#include "init.h"
int main() {
int ret;
two_t two;
ret = init_two(
&two,
INIT_ONE_A, 1,
INIT_ONE_MSG, "Hello, world",
INIT_ONE_FINISHED,
INIT_TWO_PI, 2 * 3.1415,
INIT_TWO_FINISHED
);
if (ret) {
fprintf(stderr, "unable to init two...\n");
printf("a=%d\tmsg=%s\tpi=%lf\te%lf\n",
two.parent.a,
two.parent.msg,
two.pi,
two.e
);
return 1;
}
else {
printf("a=%d\tmsg=%s\tpi=%lf\te%lf\n",
two.parent.a,
two.parent.msg,
two.pi,
two.e
);
return 0;
}
}
Now the problem I'm running into is that the behavior of this code is exactly what I expect on Linux with gcc or clang in debug and release builds. Unfortunately the code fails on Windows with visual studio 17.
So the output of the program should be someting like:
a=1 msg=Hello, world pi=6.283000 e2.712800
And that is exactly what I obtain on Linux with gcc (5.4.0-6)
On windows I get:
a=1 msg=Hello, world pi=jiberish here e2=jiberish here.
And the function init_two
does return that the function was successful on Linux and that is is not successful on windows. Also I can see that the part of one_t part of two is initialized successfully, whereas the two_t part is not.
I would be very grateful if someone would point out the issue what is going wrong. Is the va_list is passed by reference on Linux, whereas the va_list is passed by value on windows? Are the enum values promoted to int on Linux, whereas they are passed as char on windows?
Finally, I tagged the question as C and C++ because I know the code I demonstrate is C, but I would like it to work with a C++ compiler as well.