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

ACF 简介(另一个 C++ 框架)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (33投票s)

2004 年 4 月 25 日

11分钟阅读

viewsIcon

140009

downloadIcon

753

本文介绍了ACF,一个将.NET框架引入标准C++的C++框架。

最新版本可以在这里找到(ACF主页)。

摘要

本文介绍了ACF,一个将.NET框架引入标准C++的C++框架。本文假定读者熟悉C++和.NET Framework。

目录

  • ACF是什么
  • 为什么选择ACF
  • 入门
  • 基本类型系统
  • 异常
  • 数组和集合
  • 委托和事件
  • 字符串和文本
  • I/O
  • 多线程
  • 历史
  • 联系方式

ACF是什么

ACF(Another C++ Framework),是一个C++框架,旨在将.NET框架的强大功能和RAD(快速应用开发)能力引入标准C++。ACF通过在标准C++中实现.NET框架的特性和API来实现这一点,同时还利用C++的独有特性,如模板。

为什么选择ACF

.NET框架无疑是微软未来的开发平台,而C#是该平台最佳的编程语言。然而,今天仍然有成千上万的现有C++项目和开发者。这些项目不太可能被完全重写为.NET,而开发者正面临着在C#和.NET框架的RAD和优势,以及C++的性能和控制之间艰难抉择的困境。显然,在未来迁移到.NET之前,C++社区现在需要一座桥梁。

微软的解决方案是Managed C++。MC++试图将C++引入.NET,结果是C++代码可以编译成IL,并且托管代码可以轻松地与原生代码互操作。然而,MC++可能并非适用于许多项目和开发者的好选择。首先,C++几乎是性能和控制的代名词,但JIT和GC非常慢,而且运行时对于许多应用程序来说也太庞大。其次,C++本身已经非常复杂且难以学习和使用,MC++更是增加了其复杂性。

ACF试图从另一个角度解决这个问题——将.NET引入C++。通过在标准C++中实现.NET框架,ACF可以帮助现有的C++项目和开发者

  1. ACF可以用于现有或新的C++项目(ACF可以与MFC/ATL/STL等无缝集成),并且所有代码都编译成机器码。
  2. 开发者可以利用现有的C++技能,同时使用与.NET框架非常接近的概念和API。他们的新技能可以在C#、C++和MC++之间复用(例如,他们总是使用String::Format来构建格式化字符串)。
  3. ACF还有助于在C++和C#之间移植代码。

入门

目前,ACF是在Visual C++ 2003下开发的,最好在继续之前获取它(如果您仍在使用Visual C++ 6.0,请将其丢弃,因为它不够符合标准,并且其模板支持非常有限)。第一步是构建ACF(如果之前没有完成)。例如,要构建Corlib,请在Visual Studio中打开Corlib.sln(位于“{...}\Acf\src\Corlib”下,其中“{...}”是包含ACF的目录)并进行构建。与其他C++库一样,在使用ACF构建应用程序之前,您需要在开发环境中设置包含目录和库目录。Corlib的包含目录是“{...}\Acf\src\Corlib”,库目录是“{...}\Acf\lib”。

"Hello World"程序可以这样写:

#include <AcfCorlib.h>
int main() {
    Acf::Console::WriteLine(L"hello, world");
}

要编译此程序,您需要启用“将wchar_t视为内置类型”和“启用运行时类型信息”编译器选项,并链接到多线程CRT。这是所有ACF项目必需的。

ACF遵循.NET框架的设计,被组织成多个命名空间。Acf命名空间对应于.NET框架中的System命名空间。AcfCorlib.h是Corlib的头文件。在我们编写更多有用的应用程序之前,让我们先了解一下ACF的类型系统。

基本类型系统

C++类型系统和C#/CLI类型系统之间存在一些显著差异,这影响了ACF类型系统的实现。

C++没有统一的类型系统。基本类型是“魔术”的;类型不需要继承自一个共同的根;并且允许多重继承。C++还有一个非常灵活的内存模型,开发者控制如何以及何时分配/释放内存。

C#/CLI拥有统一的类型系统。所有类型都直接或间接继承自System.Object;不允许多重继承,但一个类型可以实现多个接口。类型可以是值类型或引用类型。值类型的实例直接包含数据;引用类型的实例分配在GC堆上并自动回收。值类型和引用类型之间的转换称为装箱/拆箱,这由编译器和运行时处理。

ACF试图在C++上模拟C#/CLI类型系统,同时保留性能。结果是一个混合类型系统。在ACF中,类型包括引用类型和其他类型。引用类型分配在堆上,并通过引用计数进行管理(类似于COM)。引用类型必须继承自一个单一的根——Acf::Object。多重继承仅用于实现接口,并且所有引用接口都必须继承自Acf::InterfaceBase。其他类型包括基本类型(例如intfloat)、enum类型以及其他C++结构体和类。下图显示了基本对象模型(绿色类是值类型,蓝色类是引用类型)。

在ACF中,没有自动装箱/拆箱,因为它需要编译器和运行时支持。开发者必须单独定义值类和引用类,并处理装箱/拆箱请求(在C#/CLI中,只有一个类型,并且装箱/拆箱是自动处理的)。例如,Int32是一个值类型,没有基类。它有一个实例int字段,并提供了用于解析和格式化方法。Int32Object引用类型有一个实例Int32字段,并继承自Acf::Object并实现了IFormattable等接口。(ACF使用XXObject的命名约定来区分值类型及其对应的引用类型,另一个例子是Acf::GuidAcf::GuidObject。)

示例

int main() {
    int i = 123;
    ObjectPtr o = box(i); // boxing
    int j = unbox<int>(o); // unboxing
}

展示了如何使用装箱和拆箱。boxunbox<T>函数是Acf命名空间下的模板函数。box的默认实现返回一个Acf::Boxed<T>的实例,它继承自Acf::Object并充当对象框。boxunbox<T>函数应该为具有单独值类和引用类定义的自定义类型(例如Int32Int32Object)重载。

在ACF中,所有引用类型都通过运算符new在堆上分配。new运算符被重载,以便在内存分配失败时抛出Acf::OutOfMemoryException。成功分配后,返回的内存会被清零,因此没有随机位。如前所述,ACF使用引用计数来管理对象生命周期(所有接口都与它们的拥有对象共享相同的引用计数)。持有对象或接口时,应调用AddRef;完成后,应调用Release。对象的初始引用计数为0,当其引用计数返回到0时会被释放。智能指针类RefPtr<T>被设计用于自动执行AddRef/Release作业。作为一种常见的设计理念,RefPtr<T>被视为强引用,而原始C++指针被视为弱引用。我们需要区分强引用和弱引用,因为引用计数方法在处理循环引用时存在问题。例如,如果对象A对对象B有强引用,而对象B对对象A有强引用,那么这两个对象就形成了循环引用,两者都永远不会被释放。这个问题可以通过让B持有对A的弱引用(或反之,取决于场景)来解决。

以下示例展示了如何定义和使用引用类型。

#include <AcfCorlib.h>
using namespace Acf;

class Person : public Object {
public:
    StringPtr Name;
    int Age;
    
    Person() {
    }
    
protected:
    virtual ~Person() {
    }
};

int main() {
    StringPtr name = new String(L"Foo");

    RefPtr<Person> person = new Person();
    person->Name = name;
    person->Age = 25;

    Console::WriteLine(L"Name: {0}, Age: {1}", 
       person->Name, str(person->Age));
}

Person是一个引用类型,继承自Acf::Object。它有两个实例字段,Namestring)和Ageint)。StringPtrRefPtr<String>typedef。析构函数是保护的,因此此类型不能在栈上分配(引用类型应始终在堆上分配)。

在main函数中,第一条语句定义并分配了一个新的字符串实例。接下来,分配了一个新的Person实例并修改了其字段。然后,使用Console::WriteLine来格式化并打印该人的姓名和年龄(格式字符串由.NET框架定义)。

异常

ACF使用异常处理作为其基本的错误引发和处理机制。在.NET框架中,异常也是引用类型并由GC收集。在ACF中,异常是值类型。这是因为在C++中,抛出和捕获异常的首选方式是throw MyException()catch (const MyException& e)Acf::Exception是所有异常的基类。常见的异常类包括ArgumentNullExceptionIOExceptionFormatException等。

数组和集合

ACF中的数组和集合与.NET框架中的不同,因为C++提供了良好的模板和模板特化支持(尤其是偏特化)。

Array<T>是ACF中的数组类(目前不支持多维数组)。实际的类声明是Array<T, Tr>,其中Tr是集合特征类(大多数类都有默认实现)。集合特征控制如何传递参数、如何清除元素、如何比较两个元素以及如何生成哈希码。此技术被所有ACF集合类和接口使用。

这里有两个数组示例:

RefPtr<Array<int> > array1 = Array<int>::Build(3, 1, 2, 5, 4);
Array<int>::Sort(array1);

RefPtr<Array<StringPtr> > array2 = new Array<StringPtr>(2);
array2->Item[0] = str(L"hello");
array2->Item[1] = str(L"world");

第一个示例构建了一个包含5个元素的int数组并对其进行排序。第二个示例创建了一个包含两个元素的字符串数组并设置了元素。Item[0]语法是Visual C++语言扩展,表示一个索引属性。在编译时,它会被翻译成array1->set_Item(0, str(L"hello"))

ACF中的其他集合类和接口(在Acf::Collections命名空间下)与.NET框架中的非常相似,只是它们是基于模板的。这里有一些例子:

RefPtr<List<int> > list = new List<int>();
list->Add(10);
list->Add(20);

RefPtr<Dictionary<StringPtr, int> > map = new Dictionary<StringPtr, int>();
map->Item[str(L"s1")] = 1;
map->Item[str(L"s2")] = 10;

C#有一个foreach语句来遍历数组和集合,许多开发者非常喜欢它。ACF定义了一个FOREACH宏来模拟这个特性。例如,给定一个存储int的数组,我们可以像这样遍历数组:

FOREACH (int, n, array) {
    Console::WriteLine(n);
}

委托和事件

委托和事件是C#和.NET框架中如此吸引人的特性,ACF当然不能错过它们。

ACF中的委托定义与C#/CLI有很大不同。例如,C#中的EventHandler委托定义如下:

delegate void EventHandler(object sender, EventArgs e);

在ACF中,它看起来像这样:

typedef Delegate2<void, Object*, EventArgs*>   EventHandler;

这里DelegateN<R, P1, P2, ..., Pn>是ACF中的委托类,其中N是函数参数的数量,R是函数的返回类型。要调用一个委托,请调用实例方法Invoke并传入函数参数。

在C#中,定义和使用事件非常容易,但在ACF中,由于没有编译器支持,它有点复杂。要定义一个事件,请使用ACF_DECLARE_EVENT宏。

以下是一个关于如何在ACF中使用委托和事件的示例:

typedef Delegate2<void, Object*, EventArgs*>   EventHandler;
typedef RefPtr<EventHandler>   EventHandlerPtr;

class Button : public Object {
public:
    ACF_DECLARE_EVENT(EventHandler, Click)

protected:
    void OnClick(EventArgs* e) {
        if (Click != null)
            Click->Invoke(this, e);
    }
};

class Form1 : public Object {
private:
    RefPtr<Button> _button;

public:
    Form1() {
        this->_button = new Button();

        EventHandlerPtr h1 = new EventHandler(this, Form1::ButtonClickHandler);
        this->_button->add_Click(h1);

        EventHandlerPtr h2 = new EventHandler(Form1::StaticButtonClickHandler);
        this->_button->add_Click(h2);
    }

private:
    void ButtonClickHandler(Object* sender, EventArgs* e) {
        std::cout << "Button clicked!" << std::endl;
    }

    static void StaticButtonClickHandler(Object* sender, EventArgs* e) {
        std::cout << "Static: Button clicked!" << std::endl;
    }
};

ACF_DECLARE_EVENT宏实际上生成了以下代码:

class Button : public Object {
private:    
    RefPtr<EventHandler> Click;
    
public:
    void add_Click(EventHandler* h) {
        LOCK (this)
            this->Click = EventHandler::Combine(this->Click, h);
    }
    void remove_Click(EventHandler* h) {
        LOCK (this)
            this->Click = EventHandler::Remove(this->Click, h);
    }
};

Form1的构造函数中,创建了两个委托,一个绑定到一个静态方法,另一个绑定到一个实例方法。对于实例方法,委托还需要一个指向对象的引用(这里是Form1)。由于Form1Button有强引用,而ButtonClick有强引用,所以如果ClickForm1有强引用,就会形成循环引用。然而,在其他情况下,委托可能确实需要对对象的强引用。因此,委托类的构造函数被重载为支持T*RefPtr<T>。如果您在构造委托时传递一个T*(通常使用this指针),委托将持有对对象的弱引用。否则,它将持有强引用。

字符串和文本

StringStringBuilder类提供了基本的字符串操作。Acf::Text命名空间也提供了用于编码和解码文本的类,就像.NET框架一样。这是一个简单的例子:

StringPtr s = String::Format(L"Text: {0} {1}", s1, obj);

EncodingPtr enc = Encoding::get_Default();
RefPtr<Array<byte> > bytes = enc->GetBytes(s);

str函数将数值和C风格字符串转换为String

int a = 10;
float b = 15.2;
const char* c = "hello";

StringPtr s = String::Format(L"{0}{1}{2}", str(a), str(b), str(c));

I/O

Acf::IO命名空间包含用于读写流和文件的类(目前ACF不支持异步文件I/O)。

例如,要从文件中读取文本,您可以编写如下代码:

StringPtr path = new String(L"C:\in.txt");
StreamReaderPtr reader = new StreamReader(path);
StringPtr text = reader->ReadToEnd();

ACF支持基本流,包括FileStreamMemoryStream,以及读写器,包括BinaryReader/BinaryWriterTextReader/TextWriterStreamReader/StreamWriterStringReader/StringWriter

ACF还通过PathFileDirectory类支持简单的文件系统管理。

多线程

ACF为编写多线程应用程序提供了基本支持。以下代码展示了如何创建和启动工作线程:

static void WorkerThreadProc() {
    ...
}

class MyObject : public Object {
public:
    void WorkerThreadProc() {
        ...
    }
};

int main()
{
    ThreadPtr thread1 = new Thread(WorkerThreadProc);
    thread1->Start();

    RefPtr<MyObject> obj = new MyObject();

    ThreadStartPtr start = new ThreadStart(obj, MyObject::WorkerThreadProc);
    ThreadPtr thread2 = new Thread(start);
    thread2->Start();

    ...
}

为了同步数据访问,请使用LOCK宏或InterlockedMonitorAutoResetEventManualResetEventMutex类。

历史

  • 2004年5月25日,版本0.2。
  • 2004年4月25日,版本0.1。

联系方式

您的反馈和建议是使该框架变得更好的关键因素,请随时发送给Yingle Jia(yljia@msn.com)。您也可以访问他的博客:http://blogs.wwwcoder.com/yljia/

© . All rights reserved.