#include <iostream> class context {}; class A { public: A(context* ctx) {} void a1() { std::cout << "a1" << std::endl; } void a2() { std::cout << "a2" << std::endl; } }; class B { public: B(context* ctx) {} void b1() { std::cout << "b2" << std::endl; } void b2() { std::cout << "b2" << std::endl; } }; class route { public: route(const char* path) : _path(path) {} virtual void invoke(context* ctx) = 0; private: const char* _path; }; template<class T> class troute : public route { public: typedef void (T::*F)(); troute(const char* path, F f) : route(path), _f(f) {} void invoke(context* ctx) { (T(ctx).*_f)(); } private: F _f; }; int main() { troute<A> r1("A/a1", &A::a1); troute<A> r2("A/a2", &A::a1); troute<B> r3("B/b1", &B::b1); troute<B> r4("B/b2", &B::b2); route* routes[] = { &r1, &r2, &r3, &r4 }; context ctx; for (int i = 0; i < 4; i++) routes[i]->invoke(&ctx); return 0; }C# has delegates and generics in place of pointers to member functions and templates. My first attempt to port this to C# went as follows:
class context { }; class A { public A(context ctx) { } public void a1() { Console.WriteLine("a1"); } public void a2() { Console.WriteLine("a2"); } } class B { public B(context ctx) { } public void b1() { Console.WriteLine("b1"); } public void b2() { Console.WriteLine("b2"); } } class route { public delegate void F(context ctx); public route(string path, F f) { _path = path; _f = f; } public void invoke(context ctx) { _f(ctx); } private string _path; private F _f; } static void Main(string[] args) { route[] routes = { new route("A/a1", (c)=>new A(c).a1()), new route("A/a2", (c)=>new A(c).a2()), new route("B/b1", (c)=>new B(c).b1()), new route("B/b2", (c)=>new B(c).b2()), }; context ctx = new context(); for (int i = 0; i < 4; i++) routes[i].invoke(ctx); }A couple of notable differences. The first is that the C++ pointer to member function syntax is much cleaner than the corresponding delegate syntax, to my mind at least. The C# syntax is noisy. The second is that the A/B objects are instantiated by the delegate rather than within the route class. A nice side-effect of that is the route class itself has no need for polymorphism. In some cases this may be OK, but in others possibly not. This can be remedied as follows:
class context { }; class initable { public void Init(context ctx) { } } class A : initable { public void a1() { Console.WriteLine("a1"); } public void a2() { Console.WriteLine("a2"); } } class B : initable { public void b1() { Console.WriteLine("b1"); } public void b2() { Console.WriteLine("b2"); } } abstract class route { public route(string path) { _path = path; } public abstract void invoke(context ctx); private string _path; } class troute<T> : route where T : initable, new() { public delegate void F(T t); public troute(string path, F f) : base(path) { _f = f; } public void invoke(context ctx) { T t = new T(); t.Init(ctx); _f(t); } private F _f; } static void Main(string[] args) { route[] routes = { new troute<A>("A/a1", (t)=>t.a1()), new troute<A>("A/a2", (t)=>t.a2()), new troute<B>("B/b1", (t)=>t.b1()), new troute<B>("B/b2", (t)=>t.b2()), }; context ctx = new context(); for (int i = 0; i < 4; i++) routes[i].invoke(ctx); }
This approach reproduces the C++ pointer to member function technique more faithfully in the sense of preserving the structure of the code and maintaining the responsibilities in corresponding places. In this case the delegate provides precisely the same functionality as a C++ pointer to member function, although the C# syntax is still more ugly. The trade-off here is that C# generics do not allow for a constructor with parameters, so it's necessary to construct using a parameterless constructor and then initialise after the fact. This in turn necessitated adding the initable base clase to achieve polymorphism between classes A and B. Because of this the C# above is more convoluted than my original attempt, but at least it's a more faithful port. Furthermore, although not inherently required by the syntax, I suspect this polymorphism is resolved dynamically at run-time which highlights the run-time dynamic nature of C# generics compared to the completely compile-time static nature of the C++ template system.
Stroustrup's own thoughts on C# generics reflect precisely what is going on here:
generics are primarily syntactic sugar for abstract classes; that is, with generics (whether Java or C# generics), you program against precisely defined interfaces and typically pay the cost of virtual function calls and/or dynamic casts to use arguments.
[C++] Templates supports generic programming, template metaprogramming, etc. through a combination of features such as integer template arguments, specialization, and uniform treatment of built-in and user-defined types. The result is flexibility, generality, and performance unmatched by "generics".I can't help but wonder why Microsoft stripped out such useful parts of C++ when producing C#. Are they trying to protect us lowly developers from complexity? If so I'm not sure it has worked because as this discussion shows you have to do convoluted things to recover routine C++ functionality. A programming language is all about providing the tools to create a solution to a problem. Dumbing down tools on the grounds of simplification is not helpful. Possibly the real motivation for removing some of the C++ features from C# was more to simplify the implementation of the language rather than the language itself.
great
ReplyDelete