【问题标题】:Is it a good idea to use varargs in a C API to set key value pairs?在 C API 中使用可变参数来设置键值对是个好主意吗?
【发布时间】:2009-05-14 22:47:17
【问题描述】:

我正在编写一个更新结构中许多不同字段的 API。

我可以通过使更新函数可变参数来帮助添加未来字段:

update(FIELD_NAME1, 10, FIELD_NAME2, 20);

然后在不更改任何现有调用的情况下添加FIELD_NAME3

update(FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);

请说些智慧的话?

【问题讨论】:

    标签: c api


    【解决方案1】:

    一般不会。

    Varargs 抛出了很多类型安全问题——你可以传递指针、浮点数等,而不是 int,它会毫无问题地编译。滥用可变参数(例如省略参数)可能会由于堆栈损坏或读取无效指针而导致奇怪的崩溃。

    例如,以下调用将编译并导致崩溃或其他奇怪的行为:

    UpdateField(6, "Field1", 7, "Field2", "Foo");
    

    最初的 6 是期望的参数数量。它将字符串指针 "Foo" 转换为 int 以放入 Field2,并尝试读取和解释其他两个不存在的参数,这可能会因取消引用堆栈噪声而导致崩溃。

    我相信在 C 中实现 varargs 是一个错误(考虑到今天的环境 - 它在 1972 年可能非常有意义。)实现是你在堆栈上传递一堆值,然后被调用者将遍历堆栈拾取参数,基于它对一些初始控制参数的解释。这种类型的实现基本上会让你以一种很难诊断的方式犯错误。 C# 对此的实现,在方法上传递具有属性的对象数组,只是必须更明智,尽管不能直接映射到 C 语言中。

    【讨论】:

    • 然后以已知格式传递一个大字符串。字符串对序列化更友好并且非常便携。您只需要定义一种格式来传递它们 - XMl 就是其中之一(尽管是重量级的),或者只是一个简单的 csv 就可以了。
    • +1 到 gbjbaanb - 看看 (x)printf - 已知的最漂亮的 varags 实现 :)
    【解决方案2】:

    我倾向于避免使用可变参数,除非在一种非常有用的特定情况下。变量参数并没有真正提供超出单个函数调用可以完成的所有好处,尤其是在您的情况下。

    就可读性而言(除了非常特殊的情况,我通常更喜欢原始速度),以下两个选项之间没有真正的区别(我在可变参数版本中添加了一个计数,因为您需要一个计数或哨兵来检测数据的结束):

    update(2, FIELD_NAME1, 10, FIELD_NAME2, 20);
    update(3, FIELD_NAME3, 10, FIELD_NAME4, 20, FIELD_NAME5, 30);
    /* ========== */
    update(FIELD_NAME1, 10);
    update(FIELD_NAME2, 20);
    update(FIELD_NAME3, 10);
    update(FIELD_NAME4, 20);
    update(FIELD_NAME5, 30);
    

    事实上,随着 varargs 版本变长,无论如何您都需要将其拆分,以进行格式化:

    update(5,
        FIELD_NAME1, 10,
        FIELD_NAME2, 20,
        FIELD_NAME3, 10,
        FIELD_NAME4, 20,
        FIELD_NAME5, 30);
    

    以“每个字段名称一次调用”的方式执行此操作会导致函数本身的代码更简单,并且不会降低调用的可读性。此外,它还允许编译器正确检测它无法对可变参数执行的某些错误,例如不正确的类型或用户提供的计数与实际计数不匹配。

    如果您真的必须能够调用单个函数来执行此操作,我会选择:

    void update (char *k1, int v1) {
        ...
    }
    void update2 (char *k1, int v1, char *k2, int v2) {
        update (k1, v1);
        update (k2, v2);
    }
    void update3 (char *k1, int v1, char *k2, int v2, char *k3, int v3) {
        update (k1, v1); /* or these two could be a single */
        update (k2, v2); /*   update2 (k1, v1, k2, v2);    */
        update (k3, v3);
    }
    /* and so on. */
    

    如果您愿意,您甚至可以将高级函数作为宏执行,而不会失去类型安全性。

    我倾向于使用 varargs 函数的唯一地方是提供与 printf() 相同的功能时 - 例如,我有时不得不编写带有 logPrintf() 等函数的日志库,以提供相同的功能。在我需要使用它的漫长(我的意思是,很长 :-) 的时间里,我想不出任何其他时间。

    顺便说一句,如果您决定使用可变参数,我倾向于使用哨兵而不是计数,因为这样可以防止在添加字段时出现不匹配。您很容易忘记调整计数并最终得到:

    update (2, k1, v1, k2, v2, k3, v3);
    

    添加时,这是阴险的,因为它会默默地跳过 k3/v3,或者:

    update (3, k1, v1, k2, v2);
    

    删除时,这对你的程序的成功运行几乎肯定是致命的。

    有一个哨兵可以防止这种情况发生(当然,只要你不忘记哨兵):

    update (k1, v1, k2, v2, k3, v3, NULL);
    

    【讨论】:

      【解决方案3】:

      C 中可变参数的一个问题是您不知道传递了多少个参数,因此您需要将其作为另一个参数:

      update(2, FIELD_NAME1, 10, FIELD_NAME2, 20);
      
      update(3, FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);
      

      【讨论】:

      • 为什么-1这个?这是对这个问题的完全有效(和正确)的回答。 OP 无法知道要传递多少个参数......这是使用可变参数的一个很好的例子。
      • 将 NULL 作为最后一个参数传递可能是更好的方法update(FIELD_NAME1, 10, FIELD_NAME2, 20, NULL);,因为您不需要编辑第一个参数来添加另一个字段
      • @MustafaSerdarŞanlı - 假设NULL 是有效的FIELD_NAME。我希望FIELD_NAMEs 是enum 值,所以你需要一个特殊的END_FIELD 值。在合适的时候使用NULL 很好,但并不总是合适。
      【解决方案4】:

      为什么没有一个参数,一个数组。更好的是,指向数组的指针。

      struct field {
        int val;
        char* name;
      };
      

      甚至……

      union datatype {
        int a;
        char b;
        double c;
        float f;
      // etc;
      }; 
      

      然后

      struct field {
        datatype val;
        char* name;
      };
      
      union (struct* field_name_val_pairs, int len);
      

      好的 2 个参数。我撒了谎,并认为长度参数会很好。

      【讨论】:

      • 我认为这是一个很好的设计,除了我会避免联合(只是给 val 一个固定的类型)。这样,编译器可以帮助您强制执行类型安全。当然,长度还是要正确的。
      • 这很好,如果有时有点尴尬, update_ary(int n, char *names, datatype vals) 也是可能的。
      • 使用数组代替可变参数可能是一个不错的决定,但是当值的类型是可变的时,它也会变得非常讨厌。想想你的代码充满了void** 变量......
      【解决方案5】:

      我会仔细研究任何设计用于外部(甚至内部)使用的“更新”功能,它使用相同的功能来更新结构中的许多不同字段。是否有特定原因导致您不能使用离散功能来更新字段?

      【讨论】:

        【解决方案6】:

        到目前为止,避免使用可变参数的理由都很好。让我添加另一个尚未给出的,因为它不太重要,但可以遇到。可变参数要求参数在堆栈上传递,从而减慢函数调用。在某些架构上,差异可能是显着的。在 x86 上它不是很重要,因为它缺少寄存器,例如,在 SPARC 上,它可能很重要。最多可以在寄存器上传递 5 个参数,如果您的函数使用的局部变量很少,则不会进行堆栈调整。如果您的函数是叶函数(即不调用另一个函数),则也没有窗口调整。因此,通话费用非常小。使用可变参数,在堆栈上传递参数、堆栈调整和窗口管理的正常顺序是正常的,否则您的函数将无法获取参数。这会显着增加通话费用。

        【讨论】:

          【解决方案7】:

          这里的很多人都建议传递参数#,但其他人正确地指出,这会导致隐蔽的错误,即字段# 发生变化,但传递给可变参数函数的计数却没有。我在产品中通过使用空终止来解决这个问题:

          send_info(INFO_NUMBER,
                    Some_Field,       23,
                    Some_other_Field, "more data",
                    NULL);
          

          这样,当复制和粘贴程序员不可避免地复制它时,他们不太可能搞砸。更重要的是,我不太可能把事情搞砸。

          回顾最初的问题,你有一个函数必须更新一个包含很多字段的结构,并且结构会增长。将此类数据传递给函数的常用方法(在 Win32 和 MacOS 经典 API 中)是通过传递另一个结构(甚至可以是与您正在更新的结构相同的结构),即:

          无效更新(UPDATESTRUCTURE *update_info);

          要使用它,您将填充字段:

          UPDATESTRUCTURE my_update = {
              UPDATESTRUCTURE_V1,
              field_1,
              field_2
          };
          update( &my_update );
          

          稍后当您添加新字段时,您可以更新 UPDATESTRUCTURE 定义并重新编译。通过输入版本号,您可以支持不使用新字段的旧代码。

          主题的一个变体是为您不想更新的字段设置一个值,例如 KEEP_OLD_VALUE(理想情况下为 0)或 NULL。

          UPDATESTRUCTURE my_update = {
              field_1,
              NULL,
              field_3
          };
          update( &my_update);
          

          我没有包含版本,因为我利用了这样一个事实:当我们增加 UPDATESTRUCTURE 中的字段数时,额外的字段将被初始化为 0 或 KEEP_OLD_VALUE。

          【讨论】:

          • 这样的设计不是超级安全,而且你的例子包含潜在的错误。由于 NULL 可能仅定义为 int 类型的“0”,并且 int 的大小可能与指针的大小不同,因此可能存在不匹配。因此 ALWAYS 强制转换为 NULL 参数的指针。见lysator.liu.se/c/c-faq/c-1.html
          • 这很好:在可变参数示例中,我没有指定 Some_Field 和 Some_other_Field 的类型。如果它们没有被实现为整数,那么将 NULL 转换为相同的类型是明智的。
          猜你喜欢
          • 2010-11-05
          • 2012-07-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-10-15
          • 1970-01-01
          • 2011-08-14
          • 1970-01-01
          相关资源
          最近更新 更多