The Artima Developer Community
The C++ Source | C++ Community News | Discuss | Print | Email | First Page | Previous | Next
Sponsored Link

The C++ Source
Enforcing Code Feature Requirements in C++
by Scott Meyers
September 23, 2008

Page 1 of 4  >>

Advertisement

Summary
Functions often depend on particular behavioral characteristics (“features”) of code they invoke. For example, thread-safe code must invoke only thread-safe code if it is to remain thread-safe, and exception-safe code must invoke only exception-safe code. This paper describes a technique that enables the specification of arbitrary combinations of user-defined code features on a per-function basis and that detects violations of feature constraints during compilation. The technique applies to member functions (both nonvirtual and virtual), non-member functions, and function templates; operators are excluded.

Introduction

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.

Creating code features

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.

Feature constraints and nonvirtual functions

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

Page 1 of 4  >>

The C++ Source | C++ Community News | Discuss | Print | Email | First Page | Previous | Next

Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2014 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use - Advertise with Us