使用 C、gcc、C99 和 Macros 优化微控制器的简约 OOP
Posted
技术标签:
【中文标题】使用 C、gcc、C99 和 Macros 优化微控制器的简约 OOP【英文标题】:Improving a minimalistic OOP for microcontrollers using C, gcc, C99, and Macros with optimization 【发布时间】:2015-07-06 17:28:19 【问题描述】:我经常不得不用 C 语言编写微控制器,因为 C++ 编译器通常不可用,或者由于各种错误而无法编写极小的代码。但通常,OOP“语法糖”在使硬件程序更清晰封装以便于维护时非常方便;所以我想知道是否有一种方法可以在 C 中执行 OOP 语法,其中尽可能多的 OOP 开销(当不需要时)可以以一种可移植的方式进行优化。例如:这将使用针对不同微控制器的 gcc 进行优化,或者如果 gcc 不适用于该微控制器,则可能使用 gcc 的预处理器和通用 ANSI-C 编译器。
我发现只有这样的线程,Elegant way to emulate 'this' pointer when doing OOP in C? 通常通过将指针嵌入到结构中来执行 OOP,但这并不总是我想要的,因为当我对虚拟方法或类似的东西不感兴趣时,它会浪费内存。我总是可以在需要这些功能的链接中遵循编码风格,但我想开发不需要它们的技术;例如我只是希望能够使用 OOP 范式进行编程,使用简单易懂的代码(不一定是 C++,尽管我喜欢 C++),并且在不使用某些 OOP 范式时仍然能够实现最小的 C 程序内存使用。
所以,我尝试使用 gcc 和 C99,因为通常 gcc 3.2 或更高版本可用于大多数平台;并意识到我可以使用 C99 中的 sizeof() 和 typeof() 编译器函数从未使用/未初始化的联合成员(因此类必须是具有子结构的联合)中自动索引类(一种“技巧”),在为了访问由宏创建的编译时常量查找表,它可以绑定数据和方法,并保证所有类型检查。等等等等等等。
例如:GCC 允许优化 const 结构和数组,当它们的成员仅作为常量表达式访问时,所以我想我可以使用它来构建一个基于宏的编译时绑定系统,其中 OOP 开销在 GCC 中处理,实际上优化了最终的二进制文件。
有了这个系统,我现在可以进行可变参数宏方法调用,例如: M( a , init, "with", "any", "parameters", 7 ) 查找变量 a 的类型,调用方法 init,使用可变数量的参数...
查看下面的代码示例,并尝试一下——它比解释更简单:使用 gcc -E 查看宏扩展,并注意对于仅限 ANSI 的编译器,typeof() 运算符必须替换为 ( void*) 类型转换;类型检查仅适用于 GCC。
代码被剪切和粘贴到文本编辑器中,文件名在第一行,它可以在普通的 PC 系统上编译和运行。
虽然我确实成功地摆脱了每个结构中的单个指针以“指向”类的方法列表,从而节省了有限内存微控制器中的内存,但我不太能弄清楚如何获得编译器来优化 unused 方法指针,因为我必须为类使用 (void*) 指针以将它们保存在数组中,而这些指针需要内存地址(结构的地址)和链接器实例;并且不要优化。
所以:我想知道是否有人知道通过制作某种 initialized 方法结构来改进我的解决方案,该结构会在编译后优化(没有链接器地址),例如:当它的成员只能作为代码中的常量表达式访问。本质上,我需要能够在数组中查找一个元素,其中每个数组元素的初始化部分是不同的 classXXX_mt,而不是所有类型转换为 (void*) 的 classXXX_mt 的地址列表。
如果有人能想到一个简单的解决方案,我还需要其他两项改进; cpp(c-预处理器)不允许通过令牌连接从前一个宏中定义新宏(据我所知),所以我必须制作固定长度的宏列表(在我的示例中最多为 10 ) 持有类别定义;这意味着我在一个程序中最多只能有 10 个课程;但理想情况下,我想要一种使我的代码更通用的方法,以便 cpp 可以动态创建可变长度列表。 eg: 问题与 c 预处理器无法自动“计数”有关。
其次,当我尝试对较新版本的 GCC 使用匿名结构时,我可能会通过删除'm' 来自类联合定义的名称,并使用 gcc -std=c11 编译,然后它只是给了我错误,声称该结构没有定义任何内容......因此,联合内部的匿名结构即使在 GCC 4.8 中也不起作用,尽管它应该到;如何让匿名结构工作?
以下是我如何测试和实现包含文件 voidbind.h 的示例,该文件构建类列表并将方法静态链接到该类类型的变量。
最终,系统允许我像这个例子一样编程;我用 gcc 4.0 编译到 4.9 没有问题:
//classtest.c
#ifndef MACROCHECK // Don't macro expand stdio.h, it's ugly...
#include <stdio.h> // to see macros, do gcc -D MACROCHECK -E classtest.c
#endif
#include "class1.h" // include example class, library.
#define _VOID_FINALIZE
#include "voidbind.h" // Make class list finalized, no more classes allowed
void main( void )
class1_ct a; // types ending in _ct are the macro created class types
class2_ct b;
M( a , init ); // Call method of variable, a, and the function init.
printf("a=%s %s\n",a.m.name, M( b, tryme, "echo is this" ) );
// I'd love to be rid of .m. in the previous line using anonymous struct
接下来是 class1 和 class2 的类定义/头文件,展示了如何使用宏预处理器创建绑定到方法和 _ct 类型的数据类;通常这可能会分成两个头文件和两个库;但为了简单起见,我只是通过将所有代码放在一起来滥用标题。
//class1.h
#ifndef _class1_h
#define _class1_h
// Define the data type structure for class1
typedef struct
char* name;
int one;
class1_t;
// Define the method type structure for class1
union class1_ctt ; // class type tag, incomplete tag type for class1_ct
typedef struct // method prototypes
void (*init)( union class1_ctt* ); // passed a pointer to class1_ct
class1_mt;
// bind class1_mt and class1_t together into class1_ct
#define _VOID_NEW_CLASS class1
#include "voidbind.h"
// Begin class2 definition
typedef struct // define data type for class2
int x;
class2_t;
union class2_ctt ; // class type tag, forward definition
typedef struct // method prototypes for class2
char* (*tryme)( union class2_ctt*, char* echo );
class2_mt;
// bind class2_t and class2_mt together into class2_ct
#define _VOID_NEW_CLASS class2
#include "voidbind.h"
// --------------------------------------------- Start library code
// This would normally be a separate file, and linked in
// but as were doing a test, this is in the header instead...
//#include <class1.h>
void class1_init( class1_ct* self )
self->m.name = "test";
self->m.one=5;
// Define class1's method type (_mt) instance of linker data (_ld):
// voidbind.h when it creates classes, expects an instance of the
// method type (_mt) named with _mt_ld appended to link the prototyped
// methods to C functions. This is the actual "binding" information
// and is the data that I can't get to "optimize out", eg: when there
// is more than one method, and some of them are not used by the program
class1_mt class1_mt_ld =
.init=class1_init
;
// ----------- CLASS2 libcode ----
char* class2_tryme( class2_ct* self, char* echo )
return echo;
// class2's method type (_mt) instance of linker data (_ld).
class2_mt class2_mt_ld = // linker information for method addresses
.tryme=class2_tryme
;
// --------------------------------------------- End of library code
#endif
最后,出现了 voidbind.h 这是系统的核心,让 CPP 生成指向方法结构的 void* 指针的编译时常量列表……只要传入的所有内容都是编译时常量,void* 列表将始终优化。 (但是列表中的结构不会完全优化出来。:(即使是常量。)
为了让这个想法起作用,我必须想办法让 cpp 计算 voidbind 头文件被#included 的次数,以便自动制作类指针列表,因为宏预处理器不能做添加,或定义基于相同宏名称的先前定义而改变的宏;我不得不使用内联函数将指向类方法结构 (_mt) 的指针从一次传递到下一次传递。这就是迫使我基本上使用 void* 指针的原因,尽管它可能可以通过其他方式解决。
// voidbind.h
// A way to build compile time void pointer arrays
// These arrays are lists of constants that are only important at compile
// time and which "go away" once the compilation is finished (eg:static bind).
// Example code written by: Andrew F. Robinson of Scappoose
#ifdef _VOID_WAS_FINALIZED //#
#error voidbind_h was included twice after a _VOID_FINALIZE was defined
#endif //#
// _VOID_FINALIZE, define only after all class headers have been included.
// It will simplify the macro expansion output, and minimize the memory impact
// of an optimization failure or disabling of the optimization in a bad compiler
// in hopes of making the program still work.
#ifdef _VOID_FINALIZE //#
#define _VOID_WAS_FINALIZED
#undef _VOID_BIND
static inline void* _VOID_BIND( int x )
return _VOID_BIND_OBJ[ x ];
#else
// Make sure this file has data predefined for binding before being
// included, or else error out so the user knows it's missing a define.
#if ! defined( _VOID_NEW_OBJ ) && ! defined( _VOID_NEW_CLASS ) //#
#error missing a define of _VOID_NEW_OBJ or _VOID_NEW_CLASS
#endif //#
// Initialize a macro (once) to count the number of times this file
// has been included; eg: since one object is to be added to the void
// list each time this file is #included. ( _VOID_OBJn )
#ifndef _VOID_OBJn //#
#define _VOID_OBJn _ERROR_VOID_OBJn_NOT_INITIALIZED_
// Initialize, once, macros to do name concatenations
#define __VOID_CAT( x, y ) x ## y
#define _VOID_CAT( x, y ) __VOID_CAT( x , y )
// Initialize, once, the empty void* list of pointers for classes, objs.
#define _VOID_BIND_OBJ (void* [])\
_VOID_OBJ0() , _VOID_OBJ1() , _VOID_OBJ2() , _VOID_OBJ3() , _VOID_OBJ4()\
, _VOID_OBJ5() , _VOID_OBJ6() , _VOID_OBJ7() , _VOID_OBJ8() , _VOID_OBJ9()\
// Define a function macro to return the list, so it can be easily
// replaced by a _FINALIZED inline() function, later
#define _VOID_BIND(x) _VOID_BIND_OBJ[ x ]
// All void pointers are initially null macros. So the void list is 0.
#define _VOID_OBJ0() 0
#define _VOID_OBJ1() 0
#define _VOID_OBJ2() 0
#define _VOID_OBJ3() 0
#define _VOID_OBJ4() 0
#define _VOID_OBJ5() 0
#define _VOID_OBJ6() 0
#define _VOID_OBJ7() 0
#define _VOID_OBJ8() 0
#define _VOID_OBJ9() 0
#endif //#
// Figure out how many times this macro has been called, by
// checking for how many _VOID_OBJn() function macros have been
// replaced by inline functions
#undef _VOID_OBJn
#if defined( _VOID_OBJ0 ) // #
#undef _VOID_OBJ0
#define _VOID_OBJn 0
#elif defined( _VOID_OBJ1 )
#undef _VOID_OBJ1
#define _VOID_OBJn 1
#elif defined( _VOID_OBJ2 )
#undef _VOID_OBJ2
#define _VOID_OBJn 2
#elif defined( _VOID_OBJ3 )
#undef _VOID_OBJ3
#define _VOID_OBJn 3
#elif defined( _VOID_OBJ4 )
#undef _VOID_OBJ4
#define _VOID_OBJn 4
#elif defined( _VOID_OBJ5 )
#undef _VOID_OBJ5
#define _VOID_OBJn 5
#elif defined( _VOID_OBJ6 )
#undef _VOID_OBJ6
#define _VOID_OBJn 6
#elif defined( _VOID_OBJ7 )
#undef _VOID_OBJ7
#define _VOID_OBJn 7
#elif defined( _VOID_OBJ8 )
#undef _VOID_OBJ8
#define _VOID_OBJn 8
#elif defined( _VOID_OBJ9 )
#undef _VOID_OBJ9
#define _VOID_OBJn 9
#else
#error Attempted to define more than ten objects
#endif //#
// -------------------------------------------------------
// If the user defines _VOID_NEW_CLASS
// Create a union of the two class structs, xxx_t and xxx_mt
// and call it xxx_ct. It must also be compatible with xxx_ctt, the tag
// which allows forward definitions in the class headers.
#ifdef _VOID_NEW_CLASS //#
#ifndef M //#
#define M( var , method , ... )\
(( (typeof(var._VOIDBIND_T))_VOID_BIND( sizeof(*(var._VOIDBIND)) ) )->\
method( & var , ## __VA_ARGS__ ))
#endif //#
extern _VOID_CAT( _VOID_NEW_CLASS , _mt ) _VOID_CAT( _VOID_NEW_CLASS , _mt_ld );
typedef union _VOID_CAT( _VOID_NEW_CLASS, _ctt )
char (*_VOIDBIND)[ _VOID_OBJn ];
_VOID_CAT( _VOID_NEW_CLASS , _mt ) *_VOIDBIND_T;
_VOID_CAT( _VOID_NEW_CLASS , _t ) m ;
_VOID_CAT( _VOID_NEW_CLASS , _ct );
static inline void* (_VOID_CAT( _VOID_OBJ , _VOID_OBJn )) ( void )
return & _VOID_CAT( _VOID_NEW_CLASS, _mt_ld );
#undef _VOID_NEW_CLASS
#else // ---------- Otherwise, just bind whatever object was passed in
static inline _VOID_CAT( _VOID_OBJ , _VOID_OBJn ) (void)
return (void*) & _VOID_NEW_OBJ ;
#undef _VOID_NEW_OBJ
#endif //#
// End of Macros to define a list of pointers to class method structures
// and to bind data types to method types.
#endif //#
【问题讨论】:
正如我所说,g++ 通常并非在所有微控制器平台上都可用。代码不仅可以编译,而且运行得很好。 class1_ct 实际上由 voidbind.h 中的绑定宏定义,并创建一个将 class1_t 和 class1_mt 绑定在一起的联合。 class1_ctt 是一个不完整的类型,与 class1_ct 的定义相同,因此可以在包含 voidbind.h 之前制作函数原型。 代码更容易阅读。通常,我不需要到处携带更长的名称来区分类,这正是您正在做的;因为当我编写代码时,我只是简单地说 M(a, init) 和 WHATEVER a 的类型——它会自动选择正确的类;因此,如果我更改“a”的类类型,则代码在我的其余程序中仍然是正确的。我将不得不使用您的技术手动重新编辑它注意:能够静态绑定,这是我在这里想要的,不会阻止以后的多态性的额外机制。 呃。难读如地狱。我什至不会试图理解它。正如@Cornstalks 所说,没有多态性意味着没有 OOP,所以仍然不清楚您要实现什么。 没有。你的 objects 没有方法。您的 静态类型 具有与之关联的方法。对于 OOP,您需要将方法与对象本身的(运行时值)相关联。 在运行时而不是在编译时应该选择正确的方法。这称为“后期绑定”,它唯一的独特功能是 OOP 的特征。 这根本不正确。历史上定义的 OOP 并不总是需要 vtable。您将 C++ 与 OOP 的一般概念混淆。她们不一样。 OOP 还有许多其他版本。 en.wikipedia.org/wiki/Object-oriented_programming 【参考方案1】:一般来说,您要求的是 C++。您发布的示例使用 C++ 编译器很可能会更高效或同样高效。
通常在嵌入式目标上,您的 gcc
版本已经过时,会为 c++ 生成错误代码,或者不支持所有血腥的 c++ 细节。
您可以尝试运行$your_arch_prefix-g++ --nostdlib --nostdinc
,这将在解析器中启用 c++ 语法,而不会浪费空间。如果你想禁用其他东西,你可以添加 -fno-rtti -fno-exceptions
并删除运行时类型检查和异常支持(参见 this question)。
由于 C++ 解析器是 C 前端的一部分,即使 C++ 不受您的微控制器供应商的正式支持,这可能仍然有效(有时您也可以尝试自己编译供应商特定版本并将 c++ 添加到配置脚本中的语言中)。
这通常被认为优于尝试发明自己的 OOP,如宏 DSL(领域特定语言)。
如果您不想走这条路并且不想使用手工制作的 vtables(如your link),则可以这么说。最简单的事情是有编码约定。如果您不想要多态性,下面的代码就足够了。您可以在.c
文件中定义您的结构和函数,并将声明放在标题中。下面的函数可以直接调用,所以它不在vtable中,第一个成员是c++中的this
指针。 struct impl
是对象保存的实际数据,不是 vtable 或类似的。
struct impl;
struct impl *make_impl();
// don't use this as it is a reserved keyword in c++
void do_bar(struct impl *myThis, int bar);
如果您想要多态性,请查看内核的功能。他们明确地将 vtable 嵌入到对象中,并使用宏来提取和初始化它们。
例如看char device的定义。
看看人们是如何在code 和headers 中实现这一点的。查看container_of
宏并了解media_entity_to_video_device 转换的工作原理。 (如果这对你来说太少了,请看这本书:Linux Device Drivers (LDD3))。
我知道您的代码有效,您应该为了解自己在做什么而感到自豪。但是,如果您向其他人展示您的代码,他们希望您编写 C 或 C++。如果您在 C 中并且缺少 OOP,我会尝试以一种其他人可以轻松掌握您在做什么的方式编写代码。使用宏来提取函数指针或获取多态成员通常很好,在宏中隐藏函数调用和生成结构通常是不可读的,人们必须在运行gcc -E
时调试您的代码,以查看您的创建从预处理器扩展以了解它们的内容真的在打电话。
编辑
我很快就从 clang++ 生成了 C 代码。 根据this so question 和this one 命令应该是:
$ clang++ -std=c++11 -S -emit-llvm -o out main.cc # Worked
$ llc -march=c out
llc: error: invalid target 'c'.
$ clang++ --version
clang version 3.7.0 (trunk 232670)
Target: x86_64-unknown-linux-gnu
Thread model: posix
clang C 后端似乎已被删除(另请参阅these sources 恢复 C 后端代码)。话虽如此,你也可以看看为你的目标平台生成一个后端,但我认为这肯定是过度设计的。
【讨论】:
感谢您对 clang 的评论,我会调查的;但我之前并不知道——我拥有的一些微控制器软件包中没有 g++ 二进制文件,尽管我很欣赏你这样概述标志。但是,我认为我不能通过传递给二进制 gcc 的标志来获得这个结果?我认为我在开篇文章中的错误主要在于没有评论和解释代码的约定。所以我试图在原始帖子中解决这个问题。但是是的,最好使用 gcc -E 来了解它是如何工作的。 如果您命名您使用的编译器,我可以快速浏览一下。我记得我在 AVR 上做过 c++。 您是否尝试过为编译器提供 .cc 或 .cpp 文件?这可能会触发解析器使用 c++(没有 c++ 运行时) "no g++ binary" 供应商必须提供他们用于构建二进制文件的源代码(GPL 事物)。您可以尝试从这些来源构建一个包含 g++ 的更完整的 GCC 安装。 @user2133679 关于在嵌入式上使用 C++ 没有意义。一些涉及模板的元编程技术可能很有趣,因为它们允许静态多态性,而不会浪费二进制文件中的任何空间。函数重载也是如此。同样有趣的是,如果类是模板类时不使用某些函数,编译器不会强制生成完整的类。【参考方案2】:对于附带问题,您可以使用-std=gnu99
来获取带有 gnu 扩展的 C99(例如结构和联合中的匿名结构和联合成员)。
【讨论】:
不好:( voidbind.h 的第 122 行和 classtest.c 的第 14 行和第 44 和 43 行的联合中删除 'm' 名称的结果class1.h,然后使用 gcc -std=gnu99 或 gnu11 编译会导致相同的错误:voidbind.h:122:39: 警告:声明没有声明任何内容 [默认启用] --- 联合本身,在 gcc 之后 - E,看起来像: typedef union class1_ctt char (*_VOIDBIND)[ 0 ]; class1_mt *_VOIDBIND_T; class1_t ; class1_ct; _VOID_CAT( _VOID_NEW_CLASS , _t ) ; 编辑出现故障;最后的 _VOID_CAT() 不应该在那里。对不起。 我知道 GCC,我的版本应该做匿名联合和结构,所以我不明白为什么编译器标志通过了,就像你展示的那样,我仍然得到“声明没有声明”警告,然后证明它已被删除,说“class1_ct”没有名为“name”的成员;联合中是否有一个简单的匿名结构示例,可能与我的解决方案无关,您知道它至少可以在 gcc 4.6.0 及更高版本上编译?例如:一个我可以看的例子?【参考方案3】:问题中提到了-std=c11
,所以我想在这种情况下使用_Generic
是可以的。
由于您似乎要求的是一种静态根据参数类型从共享名称解析方法的方法,因此查看重载( /static polymorphism/ad-hoc polymorphism/etc.) 作为系统操作的基础,而不是尝试优化通常用于运行时解析的模式。 _Generic
是一个静态类型->值选择运算符,专门用于帮助处理此类情况。它允许您将类型选择代码直接宏扩展为调用表达式,并保证在编译时将其删除,这正是您所需要的。
由于它是一个表达式运算符,_Generic
必须列出它将在表达式中操作的所有类型。这意味着必须对某些内容进行集群,这并不适合您的 OOP 策略。 Conventional overloading strategies 将函数定义聚集在一起,这会在尝试将方法组织到类中时搞砸;但是,如果您愿意明确列出程序中正在使用的所有类(即对类型进行聚类),则应该仍然可以以类似的方式实现静态解析。
例如(粗略的例子):
#include <stdio.h>
// shared method table structure for all classes
typedef struct
void (* init)( void* );
char* (* tryme)( void*, char* echo );
poly_method_table;
// define class1
typedef struct
char* name;
int one;
class1;
void class1_init( class1* self )
self->name = "test";
self->one=5;
const poly_method_table class1_mt =
.init = class1_init
;
// define class2
typedef struct
int x;
class2;
char* class2_tryme( class2* self, char* echo )
return echo;
const poly_method_table class2_mt =
.tryme = class2_tryme
;
// global lookup table
const poly_method_table * table_select[] =
&class1_mt,
&class2_mt,
;
#define M(MSG, THIS, ...) table_select[_Generic((THIS), \
class1 *: 0, \
class2 *: 1, \
default: "error")]->MSG((THIS), ## __VA_ARGS__)
int main( void )
class1 a;
class2 b;
M( init, &a );
printf("a=%s %s\n",a.name, M( tryme, &b, "echo is this" ) );
方法运算符M
生成一个常量查找值到全局vtables 表中(而不是尝试从对象本身检索vtable)。有了足够多的 const
声明,我希望一个体面的优化器能够删除它并直接转到所选函数,因为在选择 vtable 时没有运行时差异。
由于您已经在使用 GNU 扩展(即 ,##
用于方法调用),您可以通过使用 typeof
将 vtable 查找转换为每个类的专用类型(而不是使用单个 vtable 类)来改进这一点支持所有多态方法名称),可能会在一定程度上减小大小并为方法级别的进一步重载腾出空间。
您可以使用FOR_EACH
宏删除table_select
和M
定义中烦人的重复(它会自动填写表格、_Generic
块的中间以及用于构建索引的枚举),例如:
#define CLASSES class1, class2 //etc.
#define BUILD_ENUM(class) class ## _enum,
#define BUILD_SELECTOR(class) &class ## _mt,
#define SELECT_CLASS(class) class *: class ## _enum,
#define M(MSG, THIS, ...) table_select[_Generic((THIS), \
FOR_EACH(SELECT_CLASS, CLASSES) \
default: "error")]->MSG((THIS), ## __VA_ARGS__)
enum FOR_EACH(BUILD_ENUM, CLASSES) ;
const poly_method_table * table_select[] =
FOR_EACH(BUILD_SELECTOR, CLASSES)
;
(您可以在 SO 的其他地方找到 FOR_EACH
的合适定义)
【讨论】:
可以考虑 -std-C11 解决方案,尽管从 GCC 3.2 开始无法正常工作的任何 GCC 解决方案都将是基于我的 OP 的部分解决方案。我正在考虑根据编译中涉及的 GCC 版本来打开宏的附加功能,以便基本绑定适用于任何 GCC 3.2 向前版本,但如果有更强大的 GCC 版本可用,则可以启用附加功能.例如:匿名结构和命名结构的联合不会造成内存损失——因此兼容类型安全升级是可能的。 嗯... 什么版本的 GCC 开始允许 FOR_EACH ?因为我在 C11 标准注释中没有看到它;以及当我编译粗略的示例代码时, gcc -std=c11 rough.c ;它在 class1_t 之前被“crude.c:39:5 error expected expression”轰炸。这在 _Generic 宏调用中是正确的。在 class1_t 之前我没有发现任何问题……所以我不确定出了什么问题。 @user2133679FOR_EACH
未预定义,但需要在您的代码或包含的库 (example implementation) 中定义。有了这个,上面的代码应该在 GCC 4.9+ 中工作。 (第二个块的初始版本没有经过测试,只是输入了 SO - 我现在已经更正了)
如果我安装 gcc 4.9.2,它可以编译,但不能链接。所以第一个示例中的编译错误是编译器问题......不幸的是,在我的 32/64 位发行版上,我无法让你的程序与 4.9.2 链接——某种 mulilib 错误......不是你的过错。不过,奇怪的是 4.8.2 -- 无法编译它,因为它是在 2013 年 10 月发布的,即 >> std=c11 ;所以我认为 4.9.x 有某种错误修复?
原始OP,如果您查看代码,使用宏将您正在计算的常量自动嵌入_Generic中。因此,我没有看到您通过使用 _Generic 真正改进了系统,因为表选择仍然是类型转换指针的列表,这意味着 _mt 结构必须传递给链接器 - 并且无法优化。至少不是 gcc 3.2;为了避免链接器,我需要一个我们从不获取地址的实例化结构列表。 :) 你对共享方法表的想法给了我一个尝试的想法,因为 poly 表可以是类联合的联合......【参考方案4】:
如果您愿意放弃任何运行时多态性,您可以完全摆脱您的方法表对象,将它们替换为模拟 compile-time 调度表的_Generic
结构。您可以先调度对象的声明类型以选择其静态方法表,然后调度声明以匹配方法名称的虚拟类型,以解析要调用的实际方法。基本结构:
#define M(MSG, THIS, ...) _Generic((THIS), \
class1 *: class1_selector((struct class1_ ## MSG ## _dmy*)0), \
class2 *: class2_selector((struct class2_ ## MSG ## _dmy*)0), \
default: "error")(THIS, ## __VA_ARGS__)
(注意:我颠倒THIS
/MSG
操作数是有原因的,下面解释)
方法调用运算符M
是围绕程序中所有类的集中列表构建的。它在THIS
指针上分派以选择要调用的classX_selector
宏。它向选择器传递一个以方法命名的类型的虚拟指针(从零开始转换很好,我们无论如何都不会使用它)。
#define class1_selector(MSG) _Generic((MSG), \
struct class1_init_dmy *: class1_init, \
struct class1_show_dmy *: class1_show, \
struct class1_getOne_dmy *: class1_getOne, \
default: "error")
classX_selector
宏扩展为该类支持的所有方法的静态调度表。在这种情况下,class1
被定义为支持三种方法init
、show
和getOne
。虚拟指针的类型用于选择使用另一个类型调度表的方法。方法被返回,成为M
的_Generic
结构的返回值,被对象和参数调用。
_Generic
不是唯一的编译时运算符(例如,当给定常量时,三元运算符也应该是编译时的),但它具有三个优点:首先,它保证运行时不会发生操作;其次,它不会重复评估您的 THIS
指针(因为用于调度的表达式未编译);第三,由于虚拟类型表达式是基于name的,我们不需要浪费精力计算方法的enum
ID,确保它们在类定义中保持一致......只需粘贴名称和选择器有效。 (请注意,您甚至不必声明虚拟类型 - 它在使用中是隐含的,尽管这样做并没有什么坏处。)
从根本上说,这实际上只是overloading,但它倾向于按类而不是选择器名称对方法定义进行分组,因此它仍然包含一些面向对象的元素。
工作示例:
#include <stdio.h>
// centralized list of classes
#define CLASSES class1, class2
// static class dispatch
#define M(MSG, THIS, ...) _Generic((THIS), \
class1 *: class1_selector((struct class1_ ## MSG ## _dmy*)0), \
class2 *: class2_selector((struct class2_ ## MSG ## _dmy*)0), \
default: "error: unknown class")(THIS, ## __VA_ARGS__)
// define class1
typedef struct
char* name;
int one;
class1;
void class1_init( class1* self )
self->name = "test";
self->one=5;
void class1_show(class1 * self) printf("class1: (%s, %d)\n", self->name, self->one);
int class1_getOne(class1 * self) return self->one;
// class1 static method dispatch table
#define class1_selector(MSG) _Generic((MSG), \
struct class1_init_dmy *: class1_init, \
struct class1_show_dmy *: class1_show, \
struct class1_getOne_dmy *: class1_getOne, \
default: "error: unknown method")
// define class2
typedef struct
int x;
class2;
void class2_show(class2 * self) printf("class2: (%d)\n", self->x);
char* class2_tryme( class2* self, char* echo ) return echo;
// class2 static method dispatch table
#define class2_selector(MSG) _Generic((MSG), \
struct class2_tryme_dmy *: class2_tryme, \
struct class2_show_dmy *: class2_show, \
default: "error: unknown method")
int main(void)
class1 a;
class2 b;
M( init, &a );
b.x = 13;
M( show, &a );
M( show, &b );
因为我讨厌重复并且喜欢过度的元编程,所以这里有一个版本,它使用循环宏来消除定义类所涉及的大部分字符开销(顶部的块应该隐藏在不同的文件中;cmacros.h
is implemented here):
#include <stdio.h>
// !!METAPROGRAMMING BOILERPLATE
#include "cmacros.h"
// static class dispatch
#define M(MSG, ...) _Generic(M_FIRST(__VA_ARGS__), \
M_REST(M_REST(M_FOR_EACH(M_RE_EXP, \
(D1, D2, D3) \
M_ZIP_WITH(MSG_SEL, (CLASSES), M_ENLIST(MSG, M_NARGS(CLASSES))) ) )) \
,default: "error: unknown class") \
(__VA_ARGS__)
#define M_RE_EXP(E) ,M_FIRST E*: _Generic(DUMMY_SEL(M_FIRST E, M_FIRST(M_REST E)), \
M_CONC2(M, M_REST(M_REST E)) \
default: "error: unknown method")
#define M_CONC2(L, R) M_CONC2_(L, R)
#define M_CONC2_(L, R) L##R
#define MSG_SEL(CLASS, MSG) ,MSG_SEL_(CLASS, MSG)
#define MSG_SEL_(CLASS, MSG) (CLASS, MSG, LIST_METHODS(CLASS, CLASS ## _methods))
#define DUMMY_SEL(CLASS, MSG) DUMMY_SEL_(CLASS, MSG)
#define DUMMY_SEL_(CLASS, MSG) (struct CLASS##_##MSG##_dmy*)0
#define LIST_METHODS(CLASS, ...) \
_ZIP_WITH(METHOD_SEL, M_ENLIST(CLASS, M_NARGS(__VA_ARGS__)), (__VA_ARGS__))
#define METHOD_SEL(CLASS, METH) METHOD_SEL_(CLASS, METH)
#define METHOD_SEL_(CLASS, METH) struct CLASS##_##METH##_dmy*: CLASS##_##METH,
// !!END OF BOILERPLATE
// centralized list of classes
#define CLASSES class1, class2
// define class1
typedef struct
char* name;
int one;
class1;
void class1_init( class1* self )
self->name = "test";
self->one=5;
void class1_show(class1 * self) printf("class1: (%s, %d)\n", self->name, self->one);
int class1_getOne(class1 * self) return self->one;
#define class1_methods init, show, getOne
// define class2
typedef struct
int x;
class2;
void class2_show(class2 * self) printf("class2: (%d)\n", self->x);
char* class2_tryme( class2* self, char* echo ) return echo;
#define class2_methods show, tryme
int main(void)
class1 a;
class2 b;
M( init, &a );
b.x = 13;
M( show, &a );
M( show, &b );
最后,最后一个版本显示了在 M
的定义中交换 MSG
和 THIS
的原因 - 它可以在不依赖 GCC 扩展的情况下消除有关未使用的可变参数的警告。 (另外,谁说你需要被 C++ 的obj.method
约定控制?)
注意这个策略有一个可能的缺点(谁会想到) - 宏步骤将 每个类 的完整方法表选择粘贴到每个方法调用站点。没有运行时代码膨胀,因为_Generic
再次将其全部删除,但如果您有数百个类和方法,它可能会减慢编译速度,或者很可能会耗尽编译器内存!在这方面重载会更有效率。
【讨论】:
以上是关于使用 C、gcc、C99 和 Macros 优化微控制器的简约 OOP的主要内容,如果未能解决你的问题,请参考以下文章