More C++ Idioms/Non-Virtual Interface
Non-Virtual Interface
[edit | edit source]Intent
[edit | edit source]- To modularize/refactor common before and after code fragments (e.g., invariant checking, acquiring/releasing locks) for an entire class hierarchy at one location.
Also Known As
[edit | edit source]- Template Method - a more generic pattern, from the Gang of Four's Design Patterns book.
Motivation
[edit | edit source]Pre- and post-condition checking is known to be a useful object-oriented programming technique particularly at development time. Pre- and post-conditions ensure that invariants of a class hierarchy (and in general an abstraction) are not violated at designated points during execution of a program. Using them at development time or during debug builds helps in catching violations earlier. To maintain consistency and ease of maintenance of such pre- and post-conditions, they should be ideally modularized at one location. In a class hierarchy, where its invariants must be held before and after every method in the subclasses, modularization becomes important.
Similarly, acquiring and releasing locks on a data structure common to a class hierarchy can be considered as a pre- and post-condition, which must be ensured even at production time. It is useful to separate the responsibility of lock acquiring and releasing from subclasses and put it at one place - potentially in the base class.
Solution and Sample Code
[edit | edit source]Non-Virtual Interface (NVI) idiom allows us to refactor before and after code fragments at one convenient location - the base class. The NVI idiom is based on 4 guidelines outlined by Herb Sutter in his article named "Virtuality"[2]. Quoting Herb:
- Guideline #1: Prefer to make interfaces nonvirtual, using Template Method design pattern.
- Guideline #2: Prefer to make virtual functions private.
- Guideline #3: Only if derived classes need to invoke the base implementation of a virtual function, make the virtual function protected.
- Guideline #4: A base class destructor should be either public and virtual, or protected and nonvirtual. - Quote complete.
Here is some code that implements the NVI idiom following the above 4 guidelines.
class Base {
private:
ReaderWriterLock lock_;
SomeComplexDataType data_;
public:
void read_from( std::istream & i) { // Note non-virtual
lock_.acquire();
assert(data_.check_invariants()); // must be true
read_from_impl(i);
assert(data_.check_invariants()); // must be true
lock_.release();
}
void write_to( std::ostream & o) const { // Note non-virtual
lock_.acquire();
write_to_impl(o);
lock_.release();
}
virtual ~Base() {} // Virtual because Base is a polymorphic base class.
private:
virtual void read_from_impl( std::istream & ) = 0;
virtual void write_to_impl( std::ostream & ) const = 0;
};
class XMLReaderWriter : public Base {
private:
virtual void read_from_impl (std::istream &) {
// Read XML.
}
virtual void write_to_impl (std::ostream &) const {
// Write XML.
}
};
class TextReaderWriter : public Base {
private:
virtual void read_from_impl (std::istream &) {}
virtual void write_to_impl (std::ostream &) const {}
};
The above implementation of the base class captures several design intents that are central to achieving the benefits of the NVI idiom. This class intends to be used as a base class and therefore, it has a virtual destructor and some pure virtual functions (read_from_impl, write_to_impl), which must be implemented by all the concrete derived classes. The interface for clients (i.e., read_from and write_to) is separate from the interface for the subclasses (i.e. read_from_impl and write_to_impl). Although the read_from_impl and write_to_impl are two private functions, the base class can invoke the corresponding derived class functions using dynamic dispatch. These two functions give the necessary extension points to a family of derived classes. However, they are prevented from extending the client interface (read_from and write_to). Note that, it is possible to call interface for clients from the derived classes, however, it will lead to recursion. Finally, the NVI idiom suggests use of exactly one private virtual extension point per public non-virtual function.
Clients invoke only the public interface, which in turn invokes virtual *_impl functions as in the Template Method design pattern. Before and after invoking the *_impl functions, lock operations and invariant checking operations are performed by the base class. In this way, hierarchy wide before and after code fragments can be put together at one place, simplifying maintenance. Clients of the Base hierarchy still get polymorphic behavior even though they don't invoke virtual functions directly. Derived classes should ensure that direct access to the implementation functions (*_impl) is disallowed to the clients by making them private in the derived class as well.
Consequences
[edit | edit source]Using the NVI idiom may lead to fragile class hierarchies if proper care is not exercised. As described in [1], in Fragile Base Class (FBC) interface problem, subclass's virtual functions may get accidentally invoked when base class implementation changes without notice. For example, the following code snippet (inspired by [1]) uses the NVI idiom to implement CountingSet, which has Set as a base class.
class Set {
std::set<int> s_;
public:
void add (int i) {
s_.insert (i);
add_impl (i); // Note virtual call.
}
void addAll (int * begin, int * end) {
s_.insert (begin, end); // --------- (1)
addAll_impl (begin, end); // Note virtual call.
}
private:
virtual void add_impl (int i) = 0;
virtual void addAll_impl (int * begin, int * end) = 0;
};
class CountingSet : public Set {
private:
int count_;
virtual void add_impl (int i) {
count_++;
}
virtual void addAll_impl (int * begin, int * end) {
count_ += std::distance(begin,end);
}
};
The above class hierarchy is fragile in the sense that during maintenance, if the implementation of the addAll function (indicated by (1)) is changed to call public non-virtual add function for every integer from begin to end, then the derived class, CountingSet, breaks. As addAll calls add, the derived class's extension point add_impl is called for every integer and finally addAll_impl is also called counting the range of integers twice, which silently introduces a bug in the derived class! The solution is to observe a strict coding discipline of invoking exactly one private virtual extension point in any public non-virtual interface of the base class. However, the solution depends on programmer discipline and hence difficult to follow in practice.
Note how the NVI idiom treats each class hierarchy as a tiny (some may like to call trivial) object-oriented framework, where inversion of control (IoC) flow is commonly observed. Frameworks control the flow of the program as opposed to the functions and classes written by the client, which is why it is known as inversion of control. In NVI, the base class controls the program flow. In the example above, the Set class does the required common job of insertion before calling the *_impl virtual functions (the extension points). The Set class must not invoke any of its own public interface to prevent the FBC problem.
Finally, the NVI idiom leads to a moderate degree of code bloat in the class hierarchy as the number of functions double when the NVI is applied. Size of the refactored code in the base class should be substantial to justify the use of NVI.
Known Uses
[edit | edit source]Related Idioms
[edit | edit source]- Interface Class
- Thread-Safe Interface, from Pattern-Oriented Software Architecture (volume 2) by Douglas Schmidt et al.
- Public Overloaded Non-Virtuals Call Protected Non-Overloaded Virtuals [1]
References
[edit | edit source][1] Selective Open Recursion: Modular Reasoning about Components and Inheritance - Jonathan Aldrich, Kevin Donnelly.
[2] Virtuality! -- Herb Sutter
[3] Conversations: Virtually Yours -- Jim Hyslop and Herb Sutter
[4] Should I use protected virtuals instead of public virtuals? -- Marshall Cline