If a C++ method foo of a class B is virtual and it is known at
compile-time that no method of any subclass of B overrides foo
or that B has no subclasses, then assuming an intelligent
compiler, calls to foo should result in either:
A non-virtual method call, or given optimisation
An inlined method call.
This article presents a design pattern that guarantees this result,
even with a dumb compiler.
1. The Design Pattern
Consider the following C++ code:
#include <iostream>
class B
{
public:
virtualvoid foo()
{
std::cout << "I am B's foo\n";
}
};
void bar(B* b)
{
// *** Checkpoint 1
b->foo(); // PRINTS OUT: "I am B's foo"
}
int main()
{
// ALLOCATION:
B* b = new B;
bar(b);
// DEALLOCATION:
delete b;
}
If no subclasses of B (direct or indirect) overrides foo then
the call to b->foo() at checkpoint 1 should result in either
a non-virtual method call or with optimisation, an inlined method
call. However if no subclasses of B override foo then there
is no point in having foo as a virtual method. Therefore the
design pattern is not applicable in this case.
However if the class B is a subclass of another class called (say)
A and no subclass of B overrides foo then the design
pattern can be used to achieve the above result. Consider the
following code:
#include <iostream>
class A
{
public:
virtualvoid foo()
{
std::cout << "I am A's foo\n";
}
};
class B : public A
{
public:
virtualvoid foo()
{
std::cout << "I am B's foo\n";
}
};
void bar(B* b)
{
// *** Checkpoint 1
b->foo(); // PRINTS OUT: "I am B's foo"
}
int main()
{
// ALLOCATION:
B* b = new B;
bar(b);
// DEALLOCATION:
delete b;
}
Examining the assembler output of GNU C++ shows that even under
-O3 strength optimisation, a virtual method call is generated at
checkpoint 1. Removing the virtual keyword from B's foo
method makes no difference in the code that is generated. To improve
this result we need to rearrange the code to what follows:
#include <iostream>
class A
{
public:
virtualvoid virtual_foo()
{
std::cout << "I am A's foo\n";
}
};
class B : public A
{
public:
void foo()
{
std::cout << "I am B's foo\n";
}
virtualvoid virtual_foo()
{
foo();
}
};
void bar(B* b)
{
// *** Checkpoint 1
b->foo(); // PRINTS OUT: "I am B's foo"
}
int main()
{
// ALLOCATION:
B* b = new B;
bar(b);
// DEALLOCATION:
delete b;
}
Examining the assembler output of GNU C++ on the above, calling
b->foo() at checkpoint 1 results in a non-virtual method call
under no optimisation and an inlined method call under optimisation.
Because B has has no subclasses, this is the same result as
calling b->virtual_foo(), only the latter generates a virtual
method call and is therefore less efficient.
Unfortunately in C++ there is no way to guarantee that a method is
not overridden in any subclass (direct or indirect) of a given
class, so this design pattern must be used with care. In Java,
classes and methods can be marked as final and either of these would
ensure that this is the case.
2. Applying the Design Pattern to an I/O System
Consider the following simplified code snippet of the I/O system
described in an earlier article.
Actually the string, string_buffer and string_buffer2
classes were one and the same thing in the earlier article but this
difference is of no significance.
// Class for readonly stringsclass string {
// Method for compatibility with C stringsconstchar* const_char_star() const;
// Rest of class definition omitted...
};
// Abstract class for stream output
class Writer
{
public:
virtual ~Writer() {}
virtual Writer& operator << (char ch) = null;
virtual Writer& operator << (int i) = null;
virtual Writer& operator << (double d) = null;
virtual Writer& operator << (constchar* s) = null;
Writer& operator << (const string& s) { *this << s.const_char_star(); return *this; }
};
// Class for readable writable strings
class string_buffer : public Writer , public string
{
public:
virtual Writer& operator << (char ch);
virtual Writer& operator << (int i);
virtual Writer& operator << (double d);
virtual Writer& operator << (constchar* s);
};
// Class for efficient anonymous strings
class string_buffer2 : public string_buffer
{
// Code omitted
};
// Class for writing to files
class File_Writer : public Writer
{
private:
FILE* f;
bool ours_to_close;
public:
File_Writer(string filename);
File_Writer(FILE* f);
~File_Writer();
virtual Writer& operator << (char ch);
virtual Writer& operator << (int i);
virtual Writer& operator << (double d);
virtual Writer& operator << (constchar* s);
};
int main()
{
{
string_buffer sb;
sb << "apple "; // *** Appends "apple " to sb
sb << "banana "; // *** Appends "banana " to sb
sb << "carrot "; // *** Appends "carrot " to sb
cout << sb; // Outputs "apple banana carrot" to the standard output stream
}
{
File_Writer fw("output-1.el");
fw << "apple "; // *** Appends "apple " to file "output-1.el"
fw << "banana "; // *** Appends "banana " to file "output-1.el"
fw << "carrot "; // *** Appends "carrot " to file "output-1.el"
}
{
string_buffer sb;
Writer& w = sb;
w << "apple "; // ??? Appends "apple " to w
w << "banana "; // ??? Appends "banana " to w
w << "carrot "; // ??? Appends "carrot " to w
cout << sb; // Outputs "apple banana carrot" to the standard output stream
}
{
File_Writer fw("output-2.el");
Writer& w = fw;
w << "apple "; // ??? Appends "apple " to file "output-2.el"
w << "banana "; // ??? Appends "banana " to file "output-2.el"
w << "carrot "; // ??? Appends "carrot " to file "output-2.el"
}
}
The code labelled as ??? above would most probably result in
virtual method calls. Because the string_buffer2 subclass of
string_buffer doesn't override any of the methods of
string_buffer and class File_Writer has no subclasses, one
would hope that the code labelled as *** above would result in
non-virtual method calls. To guarantee this result, we need to
rearrange the code in the above classes to what follows:
// Class for readonly stringsclass string {
// Method for compatibility with C stringsconstchar* const_char_star() const;
// Rest of class definition omitted...
};
int main()
{
{
string_buffer sb;
sb << "apple "; // *** Appends "apple " to sb
sb << "banana "; // *** Appends "banana " to sb
sb << "carrot "; // *** Appends "carrot " to sb
cout << sb; // Outputs "apple banana carrot" to the standard output stream
}
{
File_Writer fw("output-1.el");
fw << "apple "; // *** Appends "apple " to file "output-1.el"
fw << "banana "; // *** Appends "banana " to file "output-1.el"
fw << "carrot "; // *** Appends "carrot " to file "output-1.el"
}
{
string_buffer sb;
Writer& w = sb;
w << "apple "; // ??? Appends "apple " to w
w << "banana "; // ??? Appends "banana " to w
w << "carrot "; // ??? Appends "carrot " to w
cout << sb; // Outputs "apple banana carrot" to the standard output stream
}
{
File_Writer fw("output-2.el");
Writer& w = fw;
w << "apple "; // ??? Appends "apple " to file "output-2.el"
w << "banana "; // ??? Appends "banana " to file "output-2.el"
w << "carrot "; // ??? Appends "carrot " to file "output-2.el"
}
}
With the above definitions in force, the function calls labelled as
*** above can be guaranteed to generate non-virtual method
calls. I use this design pattern in my non-standard I/O library
presented on two earlier articles (1
and 2).