在我的项目中,我正在使C类层次结构与ansi C中的Axel-Tobias Schreiner面向对象编程相似,但不相同,请参见https://www.cs.rit.edu/~ats/books/ooc.pdf。例如。
我正在初始化对象,与Axel有点不同。在多个初始化函数之间传递va_list对象时,我遇到了麻烦。假设我有一个对象两个,它是从对象一个派生的。然后,在初始化 two 对象时,我需要先初始化 one 部分,然后再初始化 two 部分。 因此,我有1个init函数,该函数使用用于初始化 two 对象的 one 部分的参数和仅用于初始化 two < / strong>扩展一个对象的部分。
我正在创建的库很大,但是我从其中提取了一个演示相同问题的迷你项目:
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;
}
}
现在我遇到的问题是,这段代码的行为恰好是我在Linux上使用gcc或clang进行调试和发布时所期望的。不幸的是,代码在带有Visual Studio 17的Windows上失败。
因此,程序的输出应类似于:
a = 1 msg =你好,世界pi = 6.283000 e2.712800
这正是我在Linux上使用gcc(5.4.0-6)所获得的
在Windows上,我得到:
a = 1 msg =你好,世界pi =这里是吉布利语e2 =这里是吉布利语。
函数init_two
确实返回该函数在Linux上成功,而在Windows上不成功。我也可以看到两个的one_t部分的一部分已成功初始化,而two_t部分没有被初始化。
有人指出问题出在哪里,我将不胜感激。 va_list是在Linux上通过引用传递的,而va_list是在Windows上通过值传递的吗?枚举值是否在Linux上提升为int,而在Windows上却作为char传递?
最后,我将问题标记为C和C ++,因为我知道演示的代码是C,但是我也希望它也可以与C ++编译器一起使用。
答案 0 :(得分:5)
va_list
的实现在编译器之间可能有很大的不同。
例如,gcc可能将其实现为指针,因此按值将其传递给另一个函数仍会修改基础结构,以使被调用函数中的更改在调用函数中可见,而MSVC将其实现为结构因此调用方函数中的更改在调用方中不可见。
您可以通过将指向初始va_list
实例的指针传递给所有需要它的函数来解决此问题。这样内部状态在所有功能中都是一致的。
// use pointer to va_list
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;
}
// use pointer to va_list
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); // pass pointer
va_end(list);
return ret;
}
C standard的7.16p3节中明确提到了这种用法:
声明的类型为
va_list
是适合保存的完整对象类型 宏
va_start
,va_arg
,va_end
和va_copy
。如果需要访问各种参数,则 被调用函数应声明一个对象(通常称为 类型为ap
的{{1}}。 对象va_list
可以作为参数传递给另一个 功能;如果该函数使用以下命令调用ap
宏 参数va_arg
,调用函数中ap
的值是 不确定,并且应在传递给ap
宏之前 对va_end
的任何进一步引用。 253)253)允许创建指向
ap
的指针,并且 将该指针传递给另一个函数,在这种情况下 原功能可能会在 其他函数返回。
答案 1 :(得分:3)
这适用于任何编译器:
va_list
无论如何,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;
}
增加两个不同函数的设计看起来非常脆弱。我不会那样做。
API设计还要求编译器无法识别特定且复杂的参数序列。这是如此容易出错,以至于我不会在代码审查中通过。
因为它看起来像XY problem。我建议创建另一个有关您的X问题的问题(如何设计所需的API)。
答案 2 :(得分:1)
据我了解,问题围绕着这样一个事实,即您的继承实现需要子类的初始化函数才能调用其超类的初始化函数。结合以下事实:将面向用户的初始化函数声明为varargs函数。为了使这项工作有效,varargs函数充当内部函数的前端,这些内部函数通过类型va_list
的单个参数接受变量参数。
这可以在标准C中起作用,但是您必须遵守一些限制。其中:
您的varargs函数必须分别通过va_list
宏初始化va_start
,一次。
您的函数可以将类型为va_list
的对象传递给其他函数,但是调用者之后不得再使用该对象,除非他们确信被调用的函数既未使用va_arg
也未使用va_end
,或者除非他们自己先使用va_end
,然后再使用va_start
。
每次使用va_start
或va_copy
必须与在同一va_end
va_list
配对
当然,您需要确保为va_arg
的每次使用指定正确的数据类型。
您通过将va_list
从一个内部初始化函数传递给另一个内部初始化函数,然后再使用它来违反第二个要求。
您应该能够通过将 pointers 传递到va_list
并间接访问它来解决此问题,以便所有涉及的功能都使用同一对象。这将使va_list
的状态得以共享,例如,最顶层类的初始化函数可以有效地使用为其指定的参数,而其子类无需知道其编号或类型。格式如下:
static int priv_init1(one_t* one, va_list *list) {
while ((selector = va_arg((*list), int)) != INIT_ONE_FINISHED) {
// ...
}
// ...
return ret;
}
static int priv_init2(two_t* two, va_list *list) {
int ret = priv_init1((one_t*)two, list);
// ...
while ((selector = va_arg((*list), int)) != INIT_TWO_FINISHED) {
// ...
}
// ...
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;
}