最近在书里看到的,让c语言去模拟其他语言里有的命名函数参数。觉得比较有意思所以记录一下。

目标

众所周知c语言里是没有命名函数参数这种东西的,形式参数虽然有自己的名字,但传递的时候并不能通过这个名字来指定参数的值。

而支持命名参数的语言,比如python里,我们能让代码达到这种效果:

def k_func(a, b, c):
print(f'{a=}, {b=}, {c=}') k_func(1, 2, 3) # Output: a=1, b=2, c=3
k_func(c=1, b=3, a=2) # Output: a=2, b=3, c=1

我们想要的类似于k_func(c=1, b=3, a=2)的效果,至少表现形式上要接近。虽说c的语法并不支持这样的表达式,但我们有办法模拟。

实现

我们假设有这样一个c语言的函数,现在我们想模拟命名参数传递:

int func(const char *text, unsigned int length, int width, int height, double weight)
{
int printed = 0;
printed += printf("text: %s\n", text);
printed += printf("length: %d\n", length);
printed += printf("width X height: %d X %d\n", width, height);
printed += printf("weight: %g\n", weight);
return printed;
}

模拟的关键在于如何完成名字到参数的映射。而且我们函数的五个参数有四种不同的类型,所以这个映射还得是异构的。

在不借助第三方库的情况下,第一个能想到的应该是enum加void*数组。enum可以完成名字到数组索引的映射,void*可以保存异构的数据。

这个方案的缺点很多:如果想要在length和width之间加个参数,我们很可能就需要修改所有的映射代码;比如void*可以接受任何数据类型的指针,所以我们几乎没有办法确保类型安全,想象一下如果有人给text传了个int的指针会发生什么。所以这个方案并不推荐。

能容纳异构的数据,同时还能给这些数据名字的东西,实际上在c里非常常见,那就是结构体。我们要选择的就是基于结构体的方案。

首先我们来定义这个结构体:

typedef struct func_arguments {
const char *text;
unsigned int length;
int width;
int height;
double weight;
} func_arguments;

字段的顺序是无所谓的,你可以根据情况来任意调整。现在我们可以根据字段名来设置值了。现在还缺一环,只有结构体是没用的,我们需要把结构体的字段传递给函数才行。所以我们要写一个帮助函数:

int func_wrapper(func_arguments fa)
{
// 根据需要还可以加入参数校验等逻辑
return func(fa.text, fa.length, fa.width, fa.height, fa.weight);
}

我们需要的工具基本上都在这了,然而现在和命名参数传递还有不少差距,因为我们需要这样写代码:

func_arguments fa;
fa.text = "text";
fa.length = 4;
fa.width = fa.height = 8;
fa.weight = 10.0;
func_wrapper(fa);

不仅形式上差远了,代码还很繁琐,所以我们还得借助一下c99的复合字面量+指定初始化器的新语法来模拟命名参数传递:

func_wrapper((func_arguments){ .text = "text", .length = 4, .width = 8, .height = 8, .weight = 10.0 });

c99允许在初始化时使用.字段名的形式给字段设置值,没有指定的字段则初始化成零值,c99还允许符合要求的字面量类型转换成数组/结构体。利用新语法我们就能写出上面的代码了。

现在形式上确实很接近了,但还是显得有点啰嗦。这时候就得依赖宏了。c的宏可以实现文本替换和简单的类型分发,所以可以用它来把一些看起来不合法的表达式转换成正常的c语言代码。

首先声明,不要滥用宏,尤其是像下面那样,这里只是充当一下记录而不是教你生产实践。

用宏可以这样写:

#define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })

这里用了另一个c99的新语法,可变长参数宏,三个点意味着宏可以接受用逗号分隔的任意个参数,而__VA_ARGS__会原样替换成这些参数。因此我们只需要让宏的参数里是正确的指定初始化器就行了:

k_func(.text="text", .length=4);
// 宏完成替换后等价于 func_wrapper((func_arguments){ .text = "text", .length = 4 });

是不是很神奇?

有人可能会担心我们参数传递时复制了整个结构体,这会不会带来效率问题?通常这不会带来什么问题,现在编译器一般都能做到省略大部分不必要的复制,另外如果对象比较小的话复制通常也不会带来太大的开销,什么叫小很难定义,以我个人的经验来看尺寸比两个cacheline小的通常都可以算是“小”。

如果还是不放心,也可以简单得把参数类型改成结构体指针:

int func_wrapper(const func_arguments *fa)
{
return func(fa->text, fa->length, fa->width, fa->height, fa->weight);
} #define k_func(...) func_wrapper(&(func_arguments){ __VA_ARGS__ })

使用方法是一样的。注意宏里的&,这会分配一个auto生命周期的func_arguments变量(通常在栈上)然后再取它的指针。现在你可以不用担心了。不过我一般不推荐这么写,除非你经过性能测试后发现参数复制真的导致了性能问题。

缺陷

奇迹和魔法都不是免费的,所以上面像变魔术的代码也是有代价的。

第一个缺陷比较小,那就是字段名字前必须加上点。如果这么写:printf("%d\n", k_func(.text="text", length=4)),注意length前我们不小心把点给漏了。编译器会爆出一个不明所以的报错:

test.c: In function ‘main’:
test.c:31:45: error: ‘length’ undeclared (first use in this function)
31 | printf("%d\n", k_func(.text="text", length=4));
| ^~~~~~
test.c:27:52: note: in definition of macro ‘k_func’
27 | #define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
| ^~~~~~~~~~~
test.c:31:45: note: each undeclared identifier is reported only once for each function it appears in
31 | printf("%d\n", k_func(.text="text", length=4));
| ^~~~~~
test.c:27:52: note: in definition of macro ‘k_func’
27 | #define k_func(...) func_wrapper((func_arguments){ __VA_ARGS__ })
| ^~~~~~~~~~~

它会告诉你length这个名字从来没被定义过而不是告诉你漏了个.。平时看东西不认真的人就要受罪了,因为要找这个点得花上一番功夫。

第二个缺陷是没有语法提示和自动补全。毕竟宏可以把任何符合规则的文本替换进去,至于替换完怎么样就管不到了,所以想要对k_func的参数进行提示和补全是不太容易的,而且实验下来目前没有ide和编辑器能帮我把字段名自动补全。不过问题倒也不大,因为万一写错了的话编译器会给出准确的报错,只不过开发效率会降低一些。

第三个缺陷在于可以写出这样的东西:printf("%d\n", k_func(.length=10, .text="text", .length=4))。我们指定了两次length字段的值,语法上这是允许的,length的值会被最右边的那个覆盖掉。但这显然不符合我们的要求,而且前面说了因为没有自动补全和语法提示,我们不小心把height写成了weight也很难察觉到。更糟糕的是这种覆盖在gcc下需要指定-Wextra才能看到一个不痛不痒的警告。同样的情况在python下会直接收到一个语法错误的异常keyword argument repeated

前两个缺陷还有办法克服,最后一个是没有任何办法的,只能靠更高级别的警告设置和人力检查了。

总结

正常来说我们做到func_wrapper那步就足够,后面的宏没啥意义纯粹是为了在形式上模拟python的命名参数而做的。

除了用结构体包装,写一个包装函数并调换参数的顺序或者给出默认值也是常见的做法,但这种做法很快会让接口的数量失控,大量相似的接口会让代码变得臭不可闻,所以我更推荐用结构体。

最后学习新语法还是很有用的,因为很多新语法好好利用的话可以有效提升扩展性和你的开发效率。

c语言模拟Python的命名参数的更多相关文章

  1. Python: 收集所有命名参数

    有时候把Python函数调用的命名参数都收集到一个dict中可以更方便地做参数检查,或者直接由参数创建attribute等.更简单的理解就是def foo(*args, **kwargs): pass ...

  2. Python 必选参数,默认参数,可变参数,关键字参数和命名关键字参数

    Py的参数还真是多,用起来还是很方便的,这么多参数种类可见它在工程上的实用性还是非常广泛的. 挺有意思的,本文主要参照Liaoxuefeng的Python教程. #必选参数 def quadratic ...

  3. 使用C语言扩展Python

    开发环境:Ubuntu9.10,python2.6,gcc4.4.1 1,ubuntu下的python运行包和开发包是分开的,因此需要在新利得里面安装python-all-dev,从而可以在代码中引用 ...

  4. Python变量命名规范

    模块名: 小写字母,单词之间用_分割 ad_stats.py 包名: 和模块名一样 类名: 单词首字母大写 AdStats ConfigUtil 全局变量名(类变量,在java中相当于static变量 ...

  5. C语言扩展Python模块

    1. 先创建一个PythonDemo.cpp文件: //c/c++中调用python脚本,配置步骤参见上一篇:C/C++与python交互 \  C/C++中调用python文件. #include ...

  6. windows 下 使用codeblocks 实现C语言对python的扩展

    本人比较懒就粘一下别人的配置方案了 从这开始到代码 摘自http://blog.csdn.net/yueguanghaidao/article/details/11538433 一直对Python扩展 ...

  7. Python学习笔记(四)Python函数的参数

    Python的函数除了正常使用的必选参数外,还可以使用默认参数.可变参数和关键字参数. 默认参数 基本使用 默认参数就是可以给特定的参数设置一个默认值,调用函数时,有默认值得参数可以不进行赋值,如: ...

  8. 可选参数、命名参数、.NET的特殊类型、特性

    1.可选参数和命名参数    1.1可选参数        语法:            [修饰符] 返回类型 方法名(必选参数n,可选参数n)        注意:            1.必选参 ...

  9. C# 4.0 新特性dynamic、可选参数、命名参数等

    1.dynamic ExpandoObject熟悉js的朋友都知道js可以这么写 :   1 var t = new Object(); 2 t.Abc = ‘something’; 3 t.Valu ...

  10. Ruby 2.x 命名参数特性简介

    我以前曾有一个梦想,就是我的爹是李嘉诚-,那个-,不是啦,我的梦想是ruby像ObjC,或是现在的swift那样给方法提供命名参数. 之前的ruby只能用hash来模拟这个行为,不过你没法很容易的定义 ...

随机推荐

  1. golang开发 gorilla websocket的使用

    很多APP都需要主动向用户推送消息,这就需要用到长连接的服务,即我们通常提到的websocket,同样也是使用socket服务,通信协议是基本类似的,在go中用的最多的.也是最简单的socket服务就 ...

  2. 7.11考试总结(NOIP模拟11)[math·biology·english]

    吾于冥河沉浮,受尽命运捉弄,纵然汝将忘吾,吾亦伴汝身旁. 前言 考试的时候本来一看 T2 一见如故,决定 231 开题,然后瞅了一眼 T3 的题面,似曾相识. 仔细看了一眼,这,这不是差异吗,然后果断 ...

  3. H5图片预览

    官方链接下载示例项目需要注册账号,似乎有点不友好,不想注册账号的可以去gitee上下载示例项目 如果你上来就是把previewImg.js 放在head中可能会出现意想不到的错误,比如下面这样子,遇到 ...

  4. Web 网页性能及性能优化

    Web 网页性能及性能优化 一.Web 性能 Web 性能是 Web 开发的一个重要方面,侧重于网页加载速度以及对用户输入的响应速度 通过优化网站来改善性能,可以在为用户提供更好的体验 网页性能既广泛 ...

  5. jq data方法

    data() 是 jQuery 的方法之一,用于在元素上存储和获取数据.它允许你将任意类型的数据附加到一个或多个元素上,并且可以通过选择器或元素对象来访问和操作这些数据. 代码中,_t.selectB ...

  6. Vue Router 4与路由管理实战

    title: Vue Router 4与路由管理实战 date: 2024/6/7 updated: 2024/6/7 excerpt: 这篇文章介绍了如何在Vue.js应用中利用Vue Router ...

  7. 使用edge浏览器时,怎么让alt+tab不切换他的子标签页而只在程序间切换?

    使用搜索按钮(WIN+Q),搜索"多任务设置",在弹出的窗口中看到"alt+tab"相关设置.选择"仅打开的窗口",ok搞定.

  8. redis高可用哨兵篇

    https://redis.io/docs/manual/sentinel/#sentinels-and-replicas-auto-discovery 官网资料 在上文主从复制的基础上,如果注节点出 ...

  9. redis主从复制篇

    我们知道要避免单点故障,即保证高可用,便需要冗余(副本)方式提供集群服务. 而Redis 提供了主从库模式,以保证数据副本的一致,主从库之间采用的是读写分离的方式. 主从复制概述 主从复制,是指将一台 ...

  10. ftl生成模板并从前台下载

    1.生成模板的工具类 package com.jesims.busfundcallnew.util; import freemarker.template.Configuration; import ...