使用C语言、gcc编译器、C99和宏优化微控制器的最小化面向对象编程

10
通常我需要使用C语言编程微控制器,因为C++编译器通常不可用,或者由于各种错误无法生成极小的代码。但是,当涉及到使程序更清晰地封装以便于维护时,面向对象(OOP)的“语法糖”非常方便;因此,我希望找到一种在C中实现OOP语法的方法,在尽可能多地优化OOP开销(当不需要时)的同时,可以以可移植的方式进行优化。例如:它将针对不同微控制器进行gcc优化,或者通过使用gcc的预处理器和通用ANSI-C编译器(如果该微控制器不支持gcc)进行优化。
我只发现了像这样的线程,Elegant way to emulate 'this' pointer when doing OOP in C? 它通常通过在结构体中嵌入指针来执行OOP,但这并不总是我想要的,因为当我不关心虚拟方法或其他类似功能时,它会浪费内存。我总是可以按照链接中需要这些功能的编码风格进行操作,但我想开发一些技术,以便在不需要这些功能时使用OOP范例进行编程,使用简单易懂的代码(不一定是C ++),并仍能实现最小的C程序内存使用情况。
因此,我开始尝试使用gcc和C99进行实验,因为通常大多数平台都可以使用gcc 3.2或更高版本。我意识到我可以使用C99的sizeof()和typeof()编译器函数自动索引类(一种“技巧”),从未使用/未初始化的联合成员中访问由宏创建的编译时常量查找表,该表可以绑定数据和方法,并保证所有类型检查等等。
例如:GCC允许在其成员仅被访问为常量表达式时优化其常量结构和数组,因此我认为我可能可以使用基于宏的编译时绑定系统来构建一个OOP开销在GCC中处理并实际优化出最终二进制文件的系统。
使用此系统,我现在可以执行可变参数宏方法调用,例如:M(a, init, "with", "any", "parameters", 7),它查找变量a的类型,调用init方法,并使用可变数量的参数……
请参见下面的代码示例,并尝试它们-这比解释更简单:使用gcc -E查看宏扩展,并注意对于仅限ANSI的编译器,typeof()运算符必须替换为(void*)类型转换;类型检查仅适用于GCC。
代码可剪切粘贴到文本编辑器中,第一行为文件名,并且在普通PC系统上可以编译和运行。

虽然我成功地消除了每个结构体中指向类方法列表的单个指针,这在有限内存微控制器中节省了内存,但我无法弄清如何让编译器优化掉未使用的方法指针,因为我必须使用(void*)指针将类保存在数组中,而这些需要一个内存地址(结构体的地址)和一个链接实例,不能优化掉。

所以:我想知道是否有人知道通过创建某种“初始化”的方法结构体来改进我的解决方案,该结构体在编译后进行优化(没有链接器地址),例如:当其成员仅作为代码中的常量表达式访问时。 实质上,我需要能够查找数组中的元素,其中每个数组元素的初始化部分都是不同的classXXX_mt,而不是所有类型转换为(void *)的classXXX_mt地址列表。

如果有人能想到简单的解决方案,我还希望获得另外两个改进的帮助; cpp(c预处理器)不允许通过标记连接从先前的宏中定义新宏(据我所知),因此我必须制作固定长度的宏列表(在我的示例中最多为10)来保存类定义; 这意味着我在程序中只能有最多10个类; 但是理想情况下,我希望能够使我的代码更加通用,以便cpp可以动态创建可变长度的列表。例如:问题与c预处理器无法自动“计数”有关。

其次,在尝试使用新版本GCC的匿名结构时(因此我可以通过从类联合定义中删除'm'名称并使用gcc -std=c11编译来摆脱访问ISO-C中成员数据所需的额外'm'),它只是给我错误,声称该结构未定义...因此,即使在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类型绑定的数据类;通常情况下,这可能会被分成两个头文件和两个库;但出于简单起见,我只是在头文件中滥用了所有代码。请注意保留HTML标签。
//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 头文件被 #include 的次数,以便自动创建一个类指针列表,而由于宏预处理器无法执行加法,或者定义基于同一宏名称的先前定义而更改的宏,所以我不得不使用内联函数将类方法结构体(_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 //#}

1
正如我所说,g++ 在许多微控制器平台上通常不可用。而且这段代码不仅可以编译,而且可以正常运行。class1_ct 实际上是由 voidbind.h 中的绑定宏定义的,它创建了一个将 class1_t 和 class1_mt 绑定在一起的联合体。class1_ctt 是一个不完整的类型,与将定义为 class1_ct 相同,因此可以在包含 voidbind.h 之前制作函数原型。 - Andrew of Scappoose
1
代码更易读。通常情况下,我不需要在所有地方都带着更长的名称来区分类别,这恰恰是您正在做的事情。当我编写代码时,我只需说 M(a,init),而无论 a 的类型是什么,它都会自动选择正确的类;因此,如果我更改 'a' 的类类型,整个程序仍然会正确运行。如果使用您的技术,我将不得不手动重新编辑它。注意:在这里绑定静态,这正是我想要的,但这并不妨碍以后使用其他机制进行多态性。 - Andrew of Scappoose
2
呃。看起来真难懂。我甚至不会试图去理解它。正如@Cornstalks所说,没有多态性就没有面向对象编程,因此仍然不清楚你试图实现什么。 - n. m.
1
不,你的“对象”没有方法。你的“静态类型”有与之相关联的方法。对于面向对象编程,你需要将方法与对象本身(运行时值)关联起来。正确的方法应该在运行时可选择,而不是在编译时。这被称为“后期绑定”,它是表征面向对象编程的单一独特特性。 - n. m.
1
那并不正确。历史上定义的面向对象编程并不总是需要虚函数表。你把C++和面向对象编程的一般概念混淆了。它们并不相同。还有许多其他版本的面向对象编程。http://en.wikipedia.org/wiki/Object-oriented_programming - Andrew of Scappoose
显示剩余19条评论
4个回答

6
一般来说,您需要的是C ++。您发布的示例很可能使用C ++编译器会更有效率或同样有效率。
通常在嵌入式目标上,您拥有过时版本的gcc,它们会为c ++生成糟糕的代码或不支持所有令人头疼的c ++细节。
您可以尝试运行${your_arch_prefix}-g ++ --nostdlib --nostdinc以在解析器中启用c ++语法,而不会浪费空间。如果要禁用其他功能,可以添加-fno-rtti -fno-exceptions以删除运行时类型检查和异常支持(请参见this question)。
由于C ++解析器是C前端的一部分,即使C ++不受您的微控制器供应商的官方支持,这可能仍然有效(有时还可以尝试自己编译供应商特定的版本,并将c ++添加到configure脚本中的语言中)。
这通常被认为比尝试发明自己的类似宏DSL(领域特定语言)优越。

话虽如此,如果您不想走这条路并且不想使用手工制作的虚函数表(如 your link)。最简单的方法是遵循编码约定。如果您不想使用多态性,则下面的代码就足够了。您可以在.c文件中定义结构体和函数,并将声明放在头文件中。下面的函数可以直接调用,因此它不在虚函数表中,而第一个成员是 c ++ 中的this指针。struct impl是对象持有的实际数据,而不是虚函数表或类似的东西。

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的定义。
并且看看人们如何在codeheaders中实例化它。查看container_of宏,并理解media_entity_to_video_device转换的工作原理。(如果这对你来说上下文太少,请查看这本书:Linux Device Drivers (LDD3))。
我知道你的代码可以运行,你应该为自己所做的事情感到骄傲。但是如果你向其他人展示你的代码,他们会期望你使用C或C++编写。如果你在C语言中缺少面向对象编程,我建议你以一种易于理解的方式编写代码。使用宏来提取函数指针或获取多态成员通常是可以接受的,但在宏中隐藏函数调用并生成结构体通常是难以阅读的,人们必须在运行gcc -E以查看预处理器将你的创建扩展后实际调用的内容来调试你的代码。

编辑

我尝试从clang++生成C代码。 根据this so questionthis 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后端已被删除(请参见这些资源,它们复活了C后端代码)。话虽如此,您也可以查看为目标平台生成后端,但我认为这肯定是过度工程化的。


谢谢您提到clang的评论,我会查阅一下;但是在此之前我并不知道它-- 在我的一些微控制器软件包中没有g++二进制文件,尽管我很欣赏您概述了这些标记。但是,我认为我不能通过传递给二进制文件gcc一个标志来获得这个结果?我想,在开头的帖子中,我的错误大多数在于没有注释和解释代码的约定;因此,我已经试图在原始帖子中修复它。但是,最好使用gcc -E来理解它的工作原理。 - Andrew of Scappoose
1
你尝试过将 .cc 或 .cpp 文件输入编译器吗?这可能会触发解析器使用 C++(而不是 C++ 运行时)。 - Alexander Oh
1
没有g++二进制文件。供应商必须提供他们用来构建二进制文件的源代码(GPL条款)。您可以尝试从这些源代码构建一个更完整的GCC安装,其中包括g++。 - n. m.
1
@user2133679 关于在嵌入式系统中使用C++并不合理的问题。一些涉及模板的元编程技术可能会很有趣,因为它们允许静态多态性而不浪费二进制空间。函数重载也是同样有趣的。此外,有趣的是,如果类是一个模板类,当某些函数未被使用时,编译器不强制生成完整的类。 - Alexander Oh
是的Alex,你和我思考的方式一样...我正在想我的宏系统也允许人们通过#define在C中进行模板,然后#include模板文件,原因完全相同。注意,我尝试在几个旧版本的gcc包上使用.cc扩展名,但没有成功。因此,显然这取决于二进制工具最初是如何编译的。但很酷的是,有时它确实起作用。 - Andrew of Scappoose
显示剩余3条评论

1
问题提到了-std=c11,所以我猜测在这种情况下使用_Generic是可以的。
由于您似乎正在寻找一种基于参数类型的共享名称静态解析方法的方法,因此将重载(/静态多态性/自适应多态性等)作为系统操作的基础,而不是尝试优化通常用于运行时分辨率的模式是有一定道理的。 _Generic是一个静态类型->值选择运算符,专门用于帮助处理这类情况。它允许您直接将类型选择代码宏展开到调用表达式中,并保证它将在编译时被删除,这正是您所需要的。
由于它是一个表达式操作符,_Generic 必须列出它将在表达式中操作的所有类型。这意味着需要进行聚类,这不完全符合面向对象编程(OOP)的策略。传统的重载策略 聚集函数定义,这会破坏试图将方法组织到类中的尝试;但是,如果愿意明确列出程序中使用的所有类别(即聚集类型),仍然可以以类似的方式实现静态解析。
例如(粗略示例):
#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 方法会产生一个常数查找值,用于全局虚函数表(而不是尝试从对象本身检索虚函数表)。如果有足够的 const 声明,我期望一个好的优化器能够移除这个查找,直接进入选定的函数,因为选择虚函数表没有运行时变化。

既然你已经在使用GNU扩展(例如方法调用中的,##),你可以通过使用typeof将虚函数表查找强制转换为每个类的专门类型(而不是具支持所有多态方法名称的单一虚函数表类)来改进它,可能会稍微减小大小,并为方法级别的进一步重载腾出空间。

您可以使用FOR_EACH宏消除table_selectM定义中的重复部分(它会自动填充表格、_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)
};

(您可以在 Stack Overflow 上找到有关 FOR_EACH 的合适定义)


考虑使用-std-C11解决方案是可以的,尽管任何GCC解决方案如果从GCC 3.2开始就无法正常工作,则将是基于我的OP的部分解决方案。我正在考虑使我的宏根据编译所涉及的GCC版本切换附加功能,以便基本绑定在任何GCC 3.2及更高版本上都能正常工作,但是如果有更强大的GCC版本可用,则可以启用附加功能。例如:具有匿名结构体和命名结构体的联合体没有内存惩罚,因此可以与类型安全升级兼容。 - Andrew of Scappoose
1
@user2133679 FOR_EACH没有预定义,但需要在您的代码或包含的库中进行定义(示例实现)。有了这个,在GCC 4.9+中上述代码应该可以工作。(第二个块的初始版本未经过测试,只是在SO中输入 - 我现在已经更正了它) - Alex Celeste
如果我安装gcc 4.9.2,它可以编译,但无法链接。因此,第一个示例中的编译错误是编译器问题...不幸的是,在我的32/64位发行版上,我无法使用4.9.2链接您的程序——某种mulilib错误...不是您的错。不过奇怪的是,4.8.2不能编译,因为它发布于2013年10月,这是>>std=c11的版本;所以我认为4.9.x有某种bug修复? - Andrew of Scappoose
如果您查看代码,原始的OP使用宏自动嵌入您正在计算的常量。因此,我认为使用_Generic并没有真正改进系统,因为表选择仍然是类型转换指针列表,这意味着_mt结构必须传递给链接器 - 无法进行优化。至少gcc 3.2不能。为了避免链接器,我需要一个实例化结构的列表,我们从未取地址。:) 您提出的共享方法表的想法给了我一个尝试的想法,因为多态表可以是类联合的联合体... - Andrew of Scappoose
@user2133679 噢噢噢,我想我有点误解了原始问题。也许 _Generic 确实有一种方法可以帮助你 - 一个两部分的选择器,首先根据类别进行调度,然后根据每个方法名称定义的占位符类型进行调度?也许稍后可以尝试写一下。 - Alex Celeste
显示剩余2条评论

1
如果您愿意放弃任何运行时多态性,您可以通过使用模拟编译时分派表的_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__)

(Note: 我反转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被定义为支持三种方法initshowgetOne。虚指针的类型用于使用另一个类型分派表选择方法。方法被返回,成为M_Generic结构的返回值,并与对象和参数一起调用。 _Generic不是唯一的编译时运算符(例如,当给定常量时,三元运算符也应该是编译时的),但它有三个优点:首先,它保证操作不会在运行时发生;其次,它不会使您的THIS指针重复评估(因为用于分派的表达式未编译);第三,由于虚类型表达式是基于名称的,我们不需要浪费精力计算方法的enum ID,确保它们在类定义中保持一致......只需粘贴名称即可使用选择器。(请注意,您甚至不必声明虚类型 - 它在使用中是隐含的,尽管这样做没有坏处。)
基本上,这只是 重载,但它是针对按类别而不是按选择器名称分组方法定义的,因此仍然具有面向对象编程的某些元素。
工作示例:
#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 );
}

因为我厌恶重复,喜欢过度的元编程,所以这里有一个版本使用循环宏来消除定义类时的大部分字符开销(顶部的块应该被隐藏在不同的文件中;{{link1:cmacros.h 在这里实现}})。
#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时交换MSGTHIS的原因——这使得可以消除有关未使用可变参数的警告,而不依赖于GCC扩展。(此外,谁说您需要受C++的obj.method约定控制呢?)

N.B. 这种策略可能存在一个缺点(谁会想到呢)——宏步骤会在每个方法调用站点插入完整的方法表选择。由于_Generic会将其全部删除,因此没有运行时代码膨胀,但如果您有数百个类和方法,则编译速度可能会变慢,或者很可能耗尽编译器内存!在这方面,重载会更加高效。


1

对于这个附属问题,你可以使用-std=gnu99来获取带有gnu扩展的C99(例如匿名结构体和联合体成员在结构体和联合体中)。


不好 :( 在voidbind.h的第122行删除联合中的“m”名称,以及classtest.c的第14行和class1.h的第44和43行中的“.m”,然后使用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 ) ; - Andrew of Scappoose
编辑出现了故障;最终的_VOID_CAT()不应该在那里。抱歉。 - Andrew of Scappoose
我知道GCC可以使用匿名联合和结构体,而且我的版本也应该支持,所以我不明白为什么像你展示的那样传递编译器标志后,我仍然会收到“声明未声明”的警告,然后证明它已被删除,说“class1_ct”没有名为'name'的成员; 也许有一个与我的解决方案无关的匿名结构体内部的简单示例,您至少知道它可以在gcc 4.6.0及以上版本上编译吗?例如:我可以查看的示例? - Andrew of Scappoose

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接