65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 类代码生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (9投票s)

2013年10月20日

MIT

9分钟阅读

viewsIcon

64243

downloadIcon

1170

自动生成 C++ 类头文件和实现文件

引言

程序 make_cpp_class.py 的目的是为了更轻松地生成 C++ 类头文件和实现文件。  这个程序通常可以生成正确的代码,但它无法始终识别需要包含的正确头文件,因此生成的代码在成功编译之前可能需要进行编辑。  这个程序可以节省大量输入工作。

该程序适用于经验丰富的 C++ 程序员,因为理解对生成代码必须进行的更改需要理解 C++ 语言。

该程序以一个文件、一个类名和一些可选的开关参数作为输入。  输入文件包含一种非常简单的语言的代码。  一旦使用此程序生成代码,就可以丢弃输入文件。

输入文件中任何方法的体都会原样复制到生成的代码中,不做任何更改。  换句话说,您仍然必须编写执行任何操作的代码,该程序仅消除了编写 C++ 类样板代码的需要。

会创建一个与命令行传入的类名同名的文件夹,生成的代码将在此文件夹中创建。

程序文件 

  • make_cpp_class.py         - 主程序文件,用于解析参数并调用代码生成器。
  • bclass.py                       - 主要代码生成模块,用于解析输入文件并创建生成的。文件。
  • bparse.py                     - 解析输入文件。
  • file_buffer.py                - 缓冲输入文件数据;由 bparse.py 使用
  • function_info.py          - 存储方法信息,包括方法名、返回类型和参数类型。
  • include_file_manager.py   - 存储某些标准类型的包含文件名。
  • type_and_name_info.py     - 存储一个名称,可以是变量或方法的名称,以及一个 type_info 实例。
  • type_info.py              - 存储类型的属性。
  • test_input.txt            - 可用作程序输入的演示某些功能的。文件。此文件不是程序的一部分。

使用代码

程序以输入文件名、类名和一些可选的开关参数作为输入。  输入文件包含一种非常简单的语言的代码。  一旦使用此程序生成代码,就可以丢弃输入文件。

输入文件中任何方法的体都会原样复制到生成的代码中,不做任何更改。  换句话说,您仍然必须编写执行任何操作的代码,该程序仅消除了在生成 C++ 类时通常需要编写的样板代码。

虽然输入语言如下所述,但查看文件 'test_input.txt' 会使下面的描述更加清晰。

该程序对输入文件中的数据进行的词法分析相对较少。  虽然一些序列会被识别为错误,但如果输入文件不正确,程序可能会生成垃圾。  这也是如果您使用此程序,了解 C++ 很重要的另一个原因。

用法

    python make_cpp_class.py <class_name> <input_file_name> [-b base_class_name] [-a author_name] [-f] [-t]

    程序接受以下开关

        -b base_class_name, --base_class base_class_name  - 继承自指定的基类。
        -a author_name, --author author_name                  - 作者姓名。
        -f, --full                                                                   - 编写完整详细的头信息。
        -t, --abstract                                                           - 将所有虚方法设为纯虚方法。
        -h, --help                                                                - 显示帮助并退出

 输入文件格式  

 输入文件中的数据成员格式 

数据成员在文件的开头,使用以下格式声明

<类型> <名称> [= 初始值]<;>

类型可以是指针参数、引用参数,并可以加上 const、volatile 或 static。  'Const volatile' 虽然有效,但不能同时使用。  我没有实现这一点只是为了简化解析器;如果需要,使用一个关键字,然后在生成的代码中添加另一个。

一组示例数据成员可能是

short m_age;
int m_count = 7;
Foo_t * m_foo;
static const float m_height = 1.0;
 
如果为数据成员提供了初始值,则等号和终止分号之间的所有内容都将用作生成代码中的初始值。

静态数据成员会在头文件和实现文件中生成相应的代码。

 构造函数和析构函数 

数据成员声明之后,在输入文件中列出了构造函数、析构函数和方法。  对于所有方法都会自动编写头文件。

方法的返回值可以与数据成员类型相同,并且还允许使用其他关键字 'inline' 和 'virtual'。  这些不能一起使用,并且都应放在函数声明行的开头。

由于类名是在命令行上指定的,因此在构造函数或析构函数中使用时,类名在输入文件中使用字符 '@' 指定。

以下是一些示例,显示了两个构造函数和一个虚析构函数。

@()
{
}

@(int x)
{
    // Some code here.
}

virtual ~@()
{
} 

为每个构造函数创建成员初始化列表。  成员初始化列表可能需要进行编辑,但大多数时候所写的内容可以保留原样。

如果命令行上通过的类名是 'Foobar',并且数据成员列表如上所示,那么上面显示的第一个构造函数定义,它以 '@' 字符开头,将生成以下代码

Foobar::Foobar()
  : m_age(0),
  , m_count(7)
  , m_foo(NULL)
{
}

复制构造函数和赋值运算符

在输入行上放置关键字 "copy:" 将自动为复制构造函数和赋值运算符生成代码。  这两个方法都将调用一个生成的 'Copy' 方法,该方法对类数据成员进行浅拷贝。

copy:

放置关键字 "nocopy:" 在输入行上将生成一个 'private' 复制构造函数和赋值运算符,两者都没有实现,因此类的实例不能被复制。

nocopy:

同时使用 "copy:" 和 "nocopy:" 会生成一个错误消息。

方法 

方法的声明类似于 C 函数,但可以通过在末尾放置 'const' 关键字来使方法成为 const。  此外,可以在末尾添加 "= 0" 使方法成为纯虚方法。  如果使用 "= 0",则不会在类实现文件中生成方法体。  这里有三个可以在输入文件中指定的方法示例

double accumulate(double addend)
{
    m_sum += addend;
    return m_sum;
}

const & Foobar GetFoobar() const
{
    return m_foobar;
}

virtual int doSomething(int anIntegerToUseForSomething) const
{
    // Need to return something here, but make_cpp_class.py won't detect
    // that no value is returned as the function body is merely copied
    // into the method in the generated code.
} 
也可以使用 'static' 关键字声明一个方法。

属性方法

还有一个特殊的关键字用于定义属性。  属性的数据成员不应在上面提到的数据成员部分声明。
生成属性的语法,使用 ":property" 关键字,是

:property <类型> <数据成员名> <属性名>

此定义将创建一个名为 set<属性名> 的方法和一个名为 get<属性名> 的方法。

这是一个属性定义的示例。

property: int m_age Age 


上面的属性定义将导致代码生成器编写以下数据成员和方法。  为简洁起见,此处省略了代码头。  另外,数据成员 m_age 将在类头文件的成员数据部分声明。

int m_age;

int Age() const
{
    return m_age;
}

void setAge(int the_value)
{
     m_age = the_value;
}

如果属性声明中使用的数据类型不是内置类型,则代码生成器将生成与上述略有不同的代码。  这是一个例子

property: Person_t * m_person Person

Person_t * m_person;

Person_t Person() const
{
    return m_person;
}

void setPerson(const Person_t * the_value)
{
     m_person = the_value;
} 

消息体 

解析器通过解析方法签名到第一个开大括号字符 '{' 来检测方法体的开始。

此时,忽略字符串声明中包含的大括号,开大括号会使最初为零的计数器递增,而闭大括号 '}' 会使计数器递减。  当计数返回到零时,方法体结束。

如果输入文件中的大括号不正确,程序将生成不正确的代码。

该程序犯的错误

如前所述,对输入文件的词法分析很少,所以输入垃圾将导致输出垃圾。

此外,程序假定所有表示非内置(非内建)类型或不是在 include-file-manager 代码中处理的特殊类型名称的类型名称,都将生成一个 'include' 语句来包含一个头文件,其名称与类型名称相同,后跟扩展名 ".h"。  当然,这通常不是一个有效的头文件名,因此通常需要删除 include 语句或在 include 语句中更改头文件名。

此外,不会检测代码体中的任何数据类型,并且可能需要为这些类型包含头文件到生成的代码中。

提示与技巧

如果我有一些我知道程序不会生成的数据,例如注释块或 'include 语句',并且我希望它们出现在类头文件中,我会声明一个内联函数,例如

inline void dummy()
{
#include "foobar.h"
#include "barfoo.h"
}
  

之后,我编辑文件,将文本移动到它所属的位置,并删除不需要的方法。

此外,使用 '@' 作为类名允许通过多次运行程序,在输入行上指定不同的类名和相同的基类,来生成具有相同方法集且都派生自同一基类的多个类。 

 有趣的点

该程序是出于消除编写 C++ 头文件和 C++ 实现文件中的所有冗余信息以及编写每个类所需的所有样板代码的必要性的愿望而编写的。

 这个程序本可以做得更好,但可能需要数月甚至数年才能编写出一个能够处理所有情况且没有错误的程序。  对我来说,这比编写这个工具然后修复头文件中的小错误要慢。 

不过,如果成千上万的人要生成代码,那么创建一个输入语言语法,并使用 yacc 或 ANTLR 来创建解析器,然后添加一个代码生成器,可能会是值得的。  这个假想的程序可以处理消息体的解析,并且会更健壮。

历史

首次发布

© . All rights reserved.