Functions with virtual arguments and multi methods (functions / methods which are virtually dependent on arguments) are not directly supported by C++. That wouldn't be a problem if there were satisfying workarounds, but all of those workarounds tend to result in a bad (sometimes horrible) code and / or a weak performance. In addition, such code is usually difficult to maintain, since it is often necessary that classes "know" about other classes they are not related with. When you add another class to a hierarchy that is used as virtual argument, you have to change some "unrelated" code. (Well, when something doesn't work, you will see how unrelated that code was ;-). Even worse, such errors are often very difficult to track, and when you try to use a library with multi methods things become really serious.
Frost allows you to use FWVAs almost as if they were a native C++
feature - you don't have to care about changes in class
hierarchies. This is simply done by adding the virtual
keyword to function arguments and using Frost instead of your
compiler. (Frost will use it itself to do the compiling). Have a look
at the following piece of code (test/example1.cc):
class foo
{
public:
virtual
~foo() {}
};
class bar : public foo
{
public:
virtual
~bar() {}
};
void
func( virtual foo& )
{
cout<<"foo\n";
}
void
func( virtual bar& )
{
cout<<"bar\n";
}
int
main()
{
foo f1;
bar b;
foo& f2 = b;
func( f1 );
func( b );
func( f2 );
return 0;
}
This example shows the definition of a function with one virtual
argument. The virtual
keyword is simply put in front of
the argument type. Note that the order of the functions does not
matter. The declaration of f2 (type foo&) is just there to show that
the object is really dispatched according to the dynamic type. When
run the program prints the following output:
foo
bar
bar
Note that although most examples use regular functions with virtual arguments, the same rules apply for methods with virtual arguments.
Virtual arguments must be pointers or references to classes; it would not make much sense to allow anything else. Const pointers / references are also allowed. Have a look at the following change to the first example (test/error1.cc):
void
func( virtual foo ) // error: virtual argument is not a reference or a pointer
{
cout<<"foo\n";
}
void
func( virtual bar )
{
cout<<"bar\n";
}
The virtual arguments are not references anymore. If you try to compile the code an error message is printed by frost:
invalid virtual argument:
func(virtual bar)
arg 1: virtual argument must be a pointer or a reference to a class
While most of the limitations of earlier versions of Frost have been removed, there is still one thing you have to take care of. All classes which are used as virtual argument need a virtual table. That means that they must have at least one regular virtual function. Look at the following code (test/error2.cc):
class foo
{
public:
// virtual
// ~foo() {}
};
The virtual destructor has been commented out. If you try to compile this code the following error message is printed:
error:
class foo has no virtual table
Virtual arguments and multiple inheritance work together as you would expect (test/example2.cc):
class base1
{
public:
virtual
~base1() {}
};
class base2
{
public:
virtual
~base2() {}
};
class multi : public base1, public base2
{
public:
virtual
~multi() {}
};
void
func( virtual base1& )
{
cout<<"base1\n";
}
void
func( virtual base2& )
{
cout<<"base2\n";
}
void
func( virtual multi& )
{
cout<<"multi\n";
}
int
main()
{
base1 bas1;
base2 bas2;
multi mul;
base1& mul1 = mul;
base2& mul2 = mul;
func( bas1 );
func( bas2 );
func( mul );
func( mul1 );
func( mul2 );
return 0;
}
The output is:
base1
base2
multi
multi
multi
The number of virtual arguments is not limited. The following example (test/example3.cc) shows a FWVA with two virtual arguments. A simple hierarchy of three classes (foo, bar, and goo) is used, where bar is derived from foo and goo is derived from bar. In the main function all possible combinations of arguments are used. The calls are dispatched to the best matching function.
class foo
{
public:
virtual
~foo() {}
};
class bar : public foo
{
public:
virtual
~bar() {}
};
class goo : public bar
{
public:
virtual
~goo() {}
};
void
func( virtual foo&, virtual foo& )
{
cout<<"foo foo\n";
}
void
func( virtual foo&, virtual bar& )
{
cout<<"foo bar\n";
}
void
func( virtual bar&, virtual foo& )
{
cout<<"bar foo\n";
}
void
func( virtual bar&, virtual bar& )
{
cout<<"bar bar\n";
}
void
func( virtual goo&, virtual goo& )
{
cout<<"goo goo\n";
}
int
main()
{
foo f1;
bar b;
foo& f2 = b;
goo g;
foo& f3 = g;
func( f1, f1 );
func( f1, f2 );
func( f1, f3 );
func( f2, f1 );
func( f2, f2 );
func( f2, f3 );
func( f3, f1 );
func( f3, f2 );
func( f3, f3 );
return 0;
}
The output is:
foo foo
foo bar
foo bar
bar foo
bar bar
bar bar
bar foo
bar bar
goo goo
It is not always possible to find a single best matching function for a set of argument types. Have a look at the following example (test/error3.cc):
class foo
{
public:
virtual
~foo() {}
};
class bar : public foo
{
public:
virtual
~bar() {}
};
void
func( virtual foo&, virtual foo& )
{
cout<<"foo foo\n";
}
void
func( virtual foo&, virtual bar& )
{
cout<<"foo bar\n";
}
void
func( virtual bar&, virtual foo& )
{
cout<<"bar foo\n";
}
int
main()
{
foo f1;
bar b;
foo& f2 = b;
func( f1, f1 );
func( f1, f2 );
func( f2, f1 );
func( f2, f2 ); // this call would be ambigous
return 0;
}
The call func( f2, f2 )
is ambiguous since func(
foo&, bar& )
and func( bar&, foo& )
are equally good
matches. The compilation aborts with the following error:
error while building fwva group func(virtual foo&, virtual foo&)
call is ambigous for the following sets of classes:
bar, bar
Note that all possible ambiguities are detected at compile time. The compilation is aborted no matter whether an ambiguous call is done or not.
virtual
keyword
I wrote that Frost makes is possible to use FWVAs as if they were a
native C++ feature. Well, that is not entirely true. C++ allows you to
omit the virtual
keyword for a method if it redefines a
virtual method of a base class (whether this makes the code more
readable is a different question). In Frost this is not possible.
The following code (test/example4.cc) is a variation of the first
example:
class foo
{
public:
virtual
~foo() {}
};
class bar : public foo
{
public:
virtual
~bar() {}
};
void
func( virtual foo& )
{
cout<<"foo\n";
}
void
func( bar& ) // virtual keyword omitted
{
cout<<"bar\n";
}
int
main()
{
foo f1;
bar b;
foo& f2 = b;
func( f1 );
func( b );
func( f2 );
return 0;
}
The output is:
foo
bar
foo
The function func( bar& )
is not seen by Frost. It is a
regular function which does no virtual argument dispatching. Therefore
the call func( b )
prints bar
, and
the call func( f2 )
prints foo
.
Pure virtual FWVAs are supported. They declared in the same way as pure virtual methods (test/example5.cc):
class foo
{
public:
virtual
~foo() {}
};
class bar : public foo
{
public:
virtual
~bar() {}
};
void
func( virtual foo& ) = 0; // pure virtual FWVA
void
func( virtual bar& )
{
cout<<"bar\n";
}
int
main()
{
foo f1;
bar b;
foo& f2 = b;
func( b );
func( f2 );
func( f1 );
return 0;
}
The calls func( b )
and func( f2 )
print
bar
. The call func( f1 )
however invokes the
pure virtual function handler (which is provided by the compiler).
Here is the complete output:
bar
bar
pure virtual method called
Aborted (core dumped)
Only non-member FWVAs which are defined in the same namespace belong to a group. Have a look at the following variation of example1.cc (test/example6.cc):
class foo
{
public:
virtual
~foo() {}
};
class bar : public foo
{
public:
virtual
~bar() {}
};
void
func( virtual foo& )
{
cout<<"foo\n";
}
namespace ns
{
void
func( virtual bar& ) // this FWVA is independent from ::func( virtual foo& )
{
cout<<"bar\n";
}
}
int
main()
{
foo f1;
bar b;
foo& f2 = b;
func( f1 );
func( b );
func( f2 );
return 0;
}
The FWVAs ::func( virtual foo& )
and ns::func(
virtual bar& )
are independent from each other. Therefore the
output is:
foo
foo
foo
Like non-member FWVAs, class member functions which are not declared virtual but have virtual arguments belong only to the same group if they are defined in the same scope. Have a look at the following example (test/example7.cc):
class bar;
class foo
{
public:
virtual
~foo() {}
void
fwva1( virtual foo& ) { cout<<"foo foo\n"; }
void
fwva1( virtual bar& ) { cout<<"foo bar\n"; }
void
fwva2( virtual foo& ) { cout<<"foo foo\n"; }
};
class bar : public foo
{
public:
virtual
~bar() {}
void
fwva2( virtual bar& ) { cout<<"bar bar\n"; }
};
int
main()
{
foo f1;
bar b;
foo& f2 = b;
f1.fwva1( f1 );
f1.fwva1( f2 );
f1.fwva2( f1 );
f1.fwva2( f2 );
cout<<"---\n";
f2.fwva1( f1 );
f2.fwva1( f2 );
f2.fwva2( f1 );
f2.fwva2( f2 );
return 0;
}
The function bar::fwva2( virtual bar& )
will never be
called. The output is:
foo foo
foo bar
foo foo
foo foo
---
foo foo
foo bar
foo foo
foo foo
Multi Methods belong to a group if they are defined inside of a class hierarchy. Have a look at the following variation of the preceding example (test/example8.cc)
class bar;
class foo
{
public:
virtual
~foo() {}
virtual void
fwva1( virtual foo& ) { cout<<"foo foo\n"; }
virtual void
fwva1( virtual bar& ) { cout<<"foo bar\n"; }
virtual void
fwva2( virtual foo& ) { cout<<"foo foo\n"; }
};
class bar : public foo
{
public:
virtual
~bar() {}
virtual void
fwva2( virtual bar& ) { cout<<"bar bar\n"; }
};
int
main()
{
foo f1;
bar b;
foo& f2 = b;
f1.fwva1( f1 );
f1.fwva1( f2 );
f1.fwva2( f1 );
f1.fwva2( f2 );
cout<<"---\n";
f2.fwva1( f1 );
f2.fwva1( f2 );
f2.fwva2( f1 );
f2.fwva2( f2 );
return 0;
}
This time bar::fwva2( virtual bar& )
is called. The
output is:
foo foo
foo bar
foo foo
foo foo
---
foo foo
foo bar
foo foo
bar bar
Like multi methods, all static member FWVAs which are defined in a class hierarchy belong to the same group (test/example9.cc):
class bar;
class foo
{
public:
virtual
~foo() {}
static void
fwva( virtual foo& ) { cout<<"foo\n"; }
};
class bar : public foo
{
public:
virtual
~bar() {}
static void
fwva( virtual bar& ) { cout<<"bar\n"; }
};
int
main()
{
foo f1;
bar b;
foo& f2 = b;
foo::fwva( f1 );
foo::fwva( f2 );
return 0;
}
The output is:
foo
bar