c语言模拟Python的命名参数
最近在书里看到的,让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的命名参数的更多相关文章
- Python: 收集所有命名参数
有时候把Python函数调用的命名参数都收集到一个dict中可以更方便地做参数检查,或者直接由参数创建attribute等.更简单的理解就是def foo(*args, **kwargs): pass ...
- Python 必选参数,默认参数,可变参数,关键字参数和命名关键字参数
Py的参数还真是多,用起来还是很方便的,这么多参数种类可见它在工程上的实用性还是非常广泛的. 挺有意思的,本文主要参照Liaoxuefeng的Python教程. #必选参数 def quadratic ...
- 使用C语言扩展Python
开发环境:Ubuntu9.10,python2.6,gcc4.4.1 1,ubuntu下的python运行包和开发包是分开的,因此需要在新利得里面安装python-all-dev,从而可以在代码中引用 ...
- Python变量命名规范
模块名: 小写字母,单词之间用_分割 ad_stats.py 包名: 和模块名一样 类名: 单词首字母大写 AdStats ConfigUtil 全局变量名(类变量,在java中相当于static变量 ...
- C语言扩展Python模块
1. 先创建一个PythonDemo.cpp文件: //c/c++中调用python脚本,配置步骤参见上一篇:C/C++与python交互 \ C/C++中调用python文件. #include ...
- windows 下 使用codeblocks 实现C语言对python的扩展
本人比较懒就粘一下别人的配置方案了 从这开始到代码 摘自http://blog.csdn.net/yueguanghaidao/article/details/11538433 一直对Python扩展 ...
- Python学习笔记(四)Python函数的参数
Python的函数除了正常使用的必选参数外,还可以使用默认参数.可变参数和关键字参数. 默认参数 基本使用 默认参数就是可以给特定的参数设置一个默认值,调用函数时,有默认值得参数可以不进行赋值,如: ...
- 可选参数、命名参数、.NET的特殊类型、特性
1.可选参数和命名参数 1.1可选参数 语法: [修饰符] 返回类型 方法名(必选参数n,可选参数n) 注意: 1.必选参 ...
- C# 4.0 新特性dynamic、可选参数、命名参数等
1.dynamic ExpandoObject熟悉js的朋友都知道js可以这么写 : 1 var t = new Object(); 2 t.Abc = ‘something’; 3 t.Valu ...
- Ruby 2.x 命名参数特性简介
我以前曾有一个梦想,就是我的爹是李嘉诚-,那个-,不是啦,我的梦想是ruby像ObjC,或是现在的swift那样给方法提供命名参数. 之前的ruby只能用hash来模拟这个行为,不过你没法很容易的定义 ...
随机推荐
- itest(爱测试)开源接口测试&敏捷测试&极简项目管理 6.6.6 发布,新增接口mock
(一)itest 简介及更新说明 itest 开源敏捷测试管理,testOps 践行者,极简的任务管理,测试管理,缺陷管理,测试环境管理,接口测试,接口Mock 6合1,又有丰富的统计分析.可按测试包 ...
- 算法学习笔记(39): 2-SAT
SAT 问题,也就是可满足性问题 Boolean Satisfiability Problem,是第一个被证明的 NPC 问题. 但是特殊的 2-SAT 我们可以通过图论的知识在线性复杂度内求解,构造 ...
- Vue学习:13.生命周期综合
0基础如何进入IT行业? 简介:对于没有任何相关背景知识的人来说,如何才能成功进入IT行业?是否有一些特定的方法或技巧可以帮助他们实现这一目标? 方向一:学习路径 明确兴趣和目标:首先确定你对IT领域 ...
- 解析Html Canvas的卓越性能与高效渲染策略
一.什么是Canvas 想必学习前端的同学们对Canvas 都不陌生,它是 HTML5 新增的"画布"元素,可以使用JavaScript来绘制图形. Canvas元素是在HTML5 ...
- WebApi 接口参数不再困惑
从网上看了WEBAPI理解感觉不错分享一下 前言:还记得刚使用WebApi那会儿,被它的传参机制折腾了好久,查阅了半天资料.如今,使用WebApi也有段时间了,今天就记录下API接口传参的一些方式方法 ...
- DotNet Web应用单文件部署系列
目录 一. pubxml文件配置 二. 打包wwwroot文件夹 三. 混淆dll文件 四. csproj文件配置 五. 批处理 六. Windows服务安装 七. ...
- 获取ImageView的触摸点所对应的UIImage的坐标
获取ImageView的触摸点所对应的UIImage的坐标 功能描述 实现前分析 注意事项 代码 求打赏 功能描述 在imageview上触摸图片,求对应UIImage的触摸点. 实现前分析 从ima ...
- 掉了两根头发后,我悟了!vue3的scoped原来是这样避免样式污染(上)
前言 众所周知,在vue中使用scoped可以避免父组件的样式渗透到子组件中.使用了scoped后会给html增加自定义属性data-v-x,同时会给组件内CSS选择器添加对应的属性选择器[data- ...
- 3.8折年终钜惠,RK3568J国产工业评估板
3.8折年终钜惠,RK3568J国产工业评估板活动火热进行中,错过等一年! -核心板国产化率100%,提供报告-瑞芯微四核ARM Cortex-A55@1.8GHz-4K视频解码.1080P视频编码. ...
- HIVE从入门到精通------(1)hive的基本操作
1.开启hive 1.首先在master的/usr/local/soft/下启动hadoop: master : start-all.sh start-all.sh 2.在另一个master(2)上监 ...