|
|
|
Sponsored Link •
|
|
Advertisement
|
When inside a const member function in C++, calls to other
member functions on the same object may be made only if those functions are
also const. The sole exception to this is when a cast is employed
at the call site, i.e., when the constness of *this
is cast away. We can view the constness of const
member functions as a code feature, and we can view the rule that
prohibits const member functions from calling
non-const member functions as a constraint. Constraints
prevent code dependent on a feature from invoking code lacking that feature.
The constraint involving const is enforced by C++ compilers,
but it is easy to imagine useful code features that are not automatically
checked:
It would be convenient to be able to specify arbitrary code features and have the resulting constraints verified during compilation. This paper describes how that can be achieved in C++.
C++'s enforcement of the constraint on const member functions actually has
nothing to do with functions. const functions are simply member
functions where the implicit *this object is declared
const. What C++ compilers enforce is the rule prohibiting implicit
conversion from const T* (pointer to const object) to
T* (pointer to non-const object). const member
functions are based on the constness of objects, not
functions. Nevertheless, their behavior provides a motivation for the
development of a way to specify and enforce arbitrary user-defined code feature
constraints.
Code features can be created by defining empty “tag” structs, analogous to the structs used in the standard C++ library to represent STL iterator categories.17 Structs representing features are known as feature classes, analogous to the term traits classes for structs representing traits.9,17 Here are some example feature classes:
struct ThreadSafe {};
struct ExceptionSafe {};
struct Portable {};
Like those for STL iterator categories, these structs serve only as identifiers. They have no semantics. The meaning of “ThreadSafe” and “Portable” (as well as the enforcement of those meanings, i.e., ensuring that the behavior of a function's code is consistent with the features it claims to offer) is entirely up to programmers.
Combinations of features can be represented by compile-time collections of
feature classes, i.e., collections of types. Such collections are easy to
create using the MPL library1,10 for template metaprogramming available
at Boost.7 The MPL (“Metaprogramming
Library”) offers STL-like containers, iterators, and algorithms for working
with compile-time information, including types. Code to create a compile-time
vector-like container named TESafe that holds the
types ThreadSafe and ExceptionSafe, for example,
looks like this:
typedef boost::mpl::vector<ThreadSafe, ExceptionSafe> TESafe;
In principle, the proper container for code features is a set, because it
makes no sense for a function to offer a feature more than once. The MPL
includes a set container, but in Boost version 1.34 (the release
current at the time this research was performed), bugs in
mpl::set's implementation rendered it unusable for this project.
The implementation shown here relies on mpl::vectors instead.
C++ macros can be used to offer clients an easy way to create both feature classes and an MPL container holding all such classes; the “_n” suffix on each macro name indicates how many features are in the universal set. For example,
CREATE_CODE_FEATURES_4(ThreadSafe, ExceptionSafe, Portable, Reviewed);
defines the feature classes ThreadSafe,
ExceptionSafe, Portable, and Reviewed,
and it also defines an MPL container, AllCodeFeatures, containing
each of these types.
Nonvirtual functions (including non-member functions) document the features
they offer through a parameter of type
MakeFeatures<FeatureContainer>::type.
MakeFeatures is a struct template that acts as a
metafunction: a function that executes during compilation. Its
result—a type— is accessed via the nested
type typedef. MakeFeatures<FeatureContainer>::type
thus refers to the type computed by MakeFeatures given an MPL
container of types. This type, which we will examine in detail later,
corresponds to a set of code features, so we will refer to it as a feature
set type and to objects of such types as feature sets.
By convention, functions put their feature set parameter at the end of their
parameter list. A function f taking parameters of type
int and double and offering the
ThreadSafe and ExceptionSafe features (i.e., the
features in the container TESafe) would be defined this way:
void f(int x, double y, MakeFeatures<TESafe>::type features)
{
... // normal function body
}
The feature set parameter serves an unconventional role, because it's not used at runtime. During compilation, however, it specifies the features that f supports and it participates in ensuring that calls to f requiring unsupported features are rejected.
When invoking a function taking a feature set parameter, the calling function passes an object corresponding to the features it requires. Often, this is the same object it has in its parameter list. For example, consider the following function g, which offers a larger set of code features than f,
typedef boost::mpl::vector<ThreadSafe, ExceptionSafe, Portable> TEPSafe;
void g(MakeFeatures<TEPSafe>::type features); // g offers/requires thread-safe,
// exception-safe, and portable code
and a call from f to g:
void f(int x, double y, MakeFeatures<TESafe>::type features)
{
...
g(features); // fine, g offers the features f needs
...
}
The reverse call—from g to f—will not compile, because g requires the Portable code feature, but f does not offer it:
void g(MakeFeatures<TEPSafe>::type features)
{
int xVal, yVal;
...
f(xVal, yVal, features); // error! f doesn't offer the Portable feature
...
}
The compilation failure is due to the lack of a conversion from MakeFeatures<TEPSafe>::type to MakeFeatures<TESafe>::type, a problem different compilers report in different ways—some more comprehensible than others.
Figures 1 and 2 show the results of submitting the above code to g++ 4.1.1 and Visual C++ 9, respectively. Neither diagnostic is a paragon of clarity, but both identify type conversion as the fundamental problem.
articlecode.cpp: In function 'void g(
CodeFeatures::Features<
boost::mpl::v_item<
CodeFeatures::Portable
, boost::mpl::v_item<
CodeFeatures::ExceptionSafe
, boost::mpl::v_item<
CodeFeatures::ThreadSafe, boost::mpl::vector0<mpl_::na>
, 0
>, 0
>, 0
>
>
)':
articlecode.cpp:32: error: conversion from 'CodeFeatures::Features<
boost::mpl::v_item<
CodeFeatures::Portable
, boost::mpl::v_item<
CodeFeatures::ExceptionSafe
, boost::mpl::v_item<
CodeFeatures::ThreadSafe, boost::mpl::vector0<mpl_::na>, 0
>, 0
>, 0
>
>' to non-scalar type 'CodeFeatures::Features<
boost::mpl::v_item<
CodeFeatures::ExceptionSafe
, boost::mpl::v_item<
CodeFeatures::ThreadSafe, boost::mpl::vector0<mpl_::na>, 0
<, 0
>
>' requested
Figure 1: Diagnostic from g++ for a violated code feature constraint.
articlecode.cpp(32) : error C2664: 'f' : cannot convert parameter 3 from
'CodeFeatures::Features<S>' to 'CodeFeatures::Features<S>'
with
[
S=boost::mpl::vector3<CodeFeatures::ThreadSafe,CodeFeatures::ExceptionSafe,CodeFeatures::Portable>
]
and
[
S=boost::mpl::vector2<CodeFeatures::ThreadSafe,CodeFeatures::ExceptionSafe>
]
No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
Figure 2: Diagnostic from Visual C++ for a violated code feature constraint.
Functions lacking MakeFeatures parameters can call functions that have them by creating the appropriate object prior to or at the point of the call:
void h() // h has no feature set parameter
{
typedef mpl::container<...> NeededFeatures; // define features needed by h
int xVal, yVal;
...
f(xVal, yVal, MakeFeatures<NeededFeatures>::type()); // create anonymous feature set
... // object; call to f succeeds if all
// features in NeededFeatures
} // are in TESafe
|
Sponsored Links
|