Doc. No:

SC22/WG21/N1363

   

J16/02/0021

 

Author:

Lois Goldthwaite

   

Lois@LoisGoldthwaite.com

 

Date:

8 May 2002

 

C++ Support for Delegation

 

Summary:

Certain object-oriented design constructs are better represented by delegation to a contained object than by inheritance. But while C++ has language mechanisms to support code reuse through inheritance, utilizing delegation requires extra work by the programmer. A modest extension of the syntax of using-declarations would provide support for a better design construct and thus encourage novice programmers to follow good practice.

 

Motivation and general background:

Books which teach the principles of object-oriented analysis, design, and programming emphasize that inheritance is a useful concept, but one which is too often used inappropriately.

Public inheritance expresses the design concept that a derived class is a specialized type of a more general base class, also called the IS-A relationship. But when one object is a part of another one rather than a subtype of it, this is called the HAS-A relationship. Such a relationship is usually better represented through composition (also called containment, aggregation, or layering). There are other kinds of relationship (HOLDS-A, USES-A, KNOWS-ABOUT-A, and so forth), but they do not figure in this discussion. Private inheritance is a special C++ language construct which does not directly express either relationship; more discussion about that later.

Authors take pains to explain when to use inheritance and when not to. Taking a quick glance at some of the books in my library, I see that Scott Meyers's 1992 Effective C++ has articles titled:

From Arthur Riel's Object-Oriented Design Heuristics:

From Herb Sutter’s Exceptional C++:

And from Alan Holub's exquisitely titled Enough Rope to Shoot Yourself in the Foot: Rules for C and C++ Programming, a succinct:

These books also make the point that many (not all, but many) designs using multiple inheritance in particular would be better expressed through composition:

// bad example
class Airplane : public Engine, public Wings, public Fuselage, public Cockpit
{
// ...
};

And yet ... inheritance is so closely identified with the object-oriented paradigm that it is almost considered to be the definition of it. And the mechanisms of C++ make it easy to use inheritance. A derived class automatically acquires all the interfaces and behaviors of its base class, simply by declaring the inheritance:

class Impl
{
private:
void * buckets[100];
public:
bool empty();
size_t size();
void set( size_t index, void * value );
// newbie error -- no virtual destructor
};

template <class T>
class Hashtable : public Impl
{
public:
void insert( T* value );
};

Hashtable h;
size_t s;
if ( !h.empty() ) s = h.size();

There is a danger here not always appreciated by novices. Inheritance exposes all of the public behaviors and data of the base class, which may allow client programs access not intended by the programmer of Hashtable -- such as directly changing values in Impl's array of buckets. Encapsulation and type safety can be enforced by making the Impl object a data member of Hashtable, but that involves extra work in writing forwarding functions to delegate to Impl's methods:

class Hashtable
{
private:
Impl my_impl;
public:
bool empty() { return my_impl.empty(); }
size_t size() { return my_impl.size(); }
};

Educators and theorists may say that composition and delegation are superior design constructs, but if that is so, thinks the novice, why does the syntax of C++ so strongly favor inheritance-based design?

Perhaps the solution is to use private inheritance? This language feature, not found in Java or Smalltalk, uses inheritance for implementation only and not to express generalization–specialization. A derived object cannot be converted to the type of its private base (except by holding one's nose and invoking an explicit old-style cast), and therefore its members are not accessible from outside the derived class.

// some sample code modeled after something I found in a book --
// (C++: Effective Object-Oriented Software Construction)
// not intended for design review!

// a class representing a node on a TList
class TListElement
{
public:
TListElement( void * data );
void * GetData() const { return _element; }
// and many other methods
private:
void * _element;
// and many more
};

// a class to hold a list of any kind of object
class TList
{
public:
TList();
// returns true if successful
virtual bool Prepend( TListElement * item );
virtual TListElement * RemoveFirst();
virtual unsigned HowMany() const;
bool Empty() const;
virtual ~TList();
// and many more methods.
// also a public data member, for use in an illustration below.
// sp shows how much memory space is consumed by the data structure
size_t sp;
private:
// call Empty( true ) to clear elements from the list
// This is not a particularly good example of class design,
// but it is not unusual to have private or protected
// helper functions which are overloads of public functions
void Empty( bool do_it );
};

//a heterogeneous stack inheriting solely for the purpose of reusing code from TList
class TStack : private TList
{
public:
bool Push( void * theElement )
{
TList::Prepend( new TListElement( theElement ) );
}


void * Pop()
{
TListElement * pElem = TList::RemoveFirst();
void * elem = pElem->GetData();
delete pElem; // remember that we created it in Push
return elem;
}


using TList::HowMany;
};

Note the class-scope using declaration in the last line of code. This has the effect of bringing an accessible member of the private base class into the derived class's interface at the access level specified. Public and protected members of the private TList base class are accessible from the derived TStack class, but not from outside the derived class. This using declaration makes them accessible to clients of the TStack object.

In fact, the name introduced by a using declaration becomes not just an accessible inherited name but a member of the derived class, at least for purposes of overload resolution. In particular, the implicit this parameter shall be treated as if it were a pointer to the derived class rather than to the base class. 7.3.3p13 goes on to say, "this has no effect on the type of the function, and in all other respects the function remains a member of the base class."

However, there are some caveats in 7.3.3p14 that render the using declaration not always usable:

-- It brings a name, not a single function, from a base class into scope. If the name is a set of overloaded functions, all of them are brought into scope at the current access level. This may or may not be what the programmer intended.

-- The name(s) must be accessible. If one of the overloads is private in the base class, then it is not accessible and the using declaration is an error. Thus it would be an error to add

using TList::Empty;

-- A using declaration cannot reduce the access level from that specified in the base class, but can increase it if the name is accessible . A base class may declare certain protected members with the intention that derived classes are able to make them public.

-- A using declaration cannot mention a template specialization (7.3.3p5)

In these cases, forwarding functions are the only way to expose functionality from the private base.

Most authors emphasize that delegation nearly always produces a better design than private inheritance. Only when virtual or protected functions from the base class are used should private inheritance be preferred.

This is the new stuff

The argument of this paper is that if C++ provided direct language support for delegation, less effort would be needed to educate programmers to use it.

I am proposing a modest extension to the syntax of using declarations to provide support for delegating functionality to a member sub-object. The basic effect is to have the compiler generate forwarding functions which delegate to a specific member sub-object instead of the base sub-object. To revise the example above:

//a heterogeneous stack using a member TList for implementation
class TStack
{
private:
TList my_list;
public:
using my_list { unsigned HowMany() const; } // new syntax

The line above is equivalent to an inline forwarding function:

unsigned HowMany() const { return this->my_list.HowMany(); }

With a little more syntactic creativity, it becomes possible to rename the function as it becomes part of the containing class's interface:

using my_list { bool Empty() const = Vacant; } // new syntax

This is a natural extension to the syntax for designating a pure virtual function. The line above is equivalent to an inline forwarding function:

 bool Vacant() const { return this->my_list.Empty(); }

The ability to rename functions may be useful if more than one member sub-object are of the same class, or if name clashes arise between functions of two members not of the same class.

Data members may also be exposed and renamed by a similar extension to the syntax:

using my_list { size_t sp = space_occupied; } // new syntax

The effect is to create an alias in the containing class (TStack::space_occupied) bound to a reference to a member of the contained class. As always, the contained class member must be accessible, either because it is public or because the container class has been granted friendship.

size_t & space_occupied = my_list.sp;

I recognize that this last worm is slimier than the others in the can, as it affects object data rather than class functionality, and trespasses into an area which properly belongs to constructors. If too controversial, it can be dropped from this proposal without affecting other paragraphs.

For convenience, all the using declarations referring to a single sub-object can be combined:

using my_list {
unsigned HowMany() const;
bool Empty() const = Vacant;
size_t sp = space_occupied;
}

This should render class declarations more compact and readable than a series of explicit forwarding functions.

A common use of delegation is forwarding through a member pointer to an object, rather than to a member sub-object. This is known as the Pimpl (pointer to implementation) or Cheshire Cat idiom. If the delegatee member object is a pointer, the forwarding function is generated as if invoking operator-> on the pointer:

//another stack class using the pimpl idiom for implementation
class TStack2
{
private:
TList * pmy_list; // initialization in unshown constructor
public:
using pmy_list {
unsigned HowMany() const;
bool Empty() const = Vacant;
size_t sp = space_occupied;
}
};

The effect of the using declaration is to compiler-generate forwarding functions with these semantics:

unsigned HowMany() const { return this->pmy_list->HowMany(); }
bool Vacant() const { return this->pmy_list->Empty(); }
size_t & space_occupied = this->my_list->sp;

 

Specific Points of Syntax To Note

The using-delegation-declaration (to coin a phrase – or how about using-alias-definition by analogy to namespace-alias-definition?) refers to a specific member sub-object, not a base sub-object.

If it refers to a function, the full function parameter list must be specified. Thus only a specific function signature is exposed, not the full overload set. An erroneously-specified function in a using-delegation-declaration will be diagnosed by the compiler.

The return value of the function remains the same. In particular, if the return value of the delegatee function is a reference to the member sub-object, it remains a reference to the member sub-object. Such a reference may not be usable to the caller if the member sub-object is not accessible from outside the class. Thus it may not be possible to chain a series of forwarded invocations, but I anticipate this will be only a minor inconvenience.

If the delegatee function or data member is cv-qualified, the same qualification applies to the delegating function or member.

The implicit this parameter of the exposed function is the type of the delegating class (just as happens with a normal using-declaration referring to a member of the base class, or with a manually-written forwarding function).

Other parameters to the function in the delegating class are the same types and in the same order as the parameters to the delegatee function. All arguments are simply passed through to the invocation of the contained object’s delegatee function.

If the delegatee name refers to an accessible data member of a member sub-object, the using-delegation-declaration creates an alias member in the delegating class. (The slimy worm again rears its head here.)

If the delegatee is a member-function-template, the using-delegation-declaration may expose the template or only a certain specialization of it. (At present 7.3.3p5 disallows using-declarations from referring to template specializations.)

// member template function in the Impl class
template <class T>
T munge_this_number( T input );

// delegated template function in the Visible class
Impl my_impl;
using my_impl { template <class T> munge_this_number( T input ); }

// renamed and specialized delegated template function in the Visible class
using my_impl { template <> munge_this_number<double>( double input ) =
transform_a_double; }

A motivation for exposing only certain specializations might be to hide the template-based implementation from clients of the class, for simplicity. Another motivation might be to limit the interfaces available to the client, because some instantiations might be undesirable (if the implementation class uses a vector<T> internally, the delegating class could restrict the client from using a bool as T).

Another logical extension is to expose partial specializations of member templates, but let’s leave complicated syntax issues to be thrashed out later.

The specialization or partial specialization of the member template must be defined in the scope of the delegatee class where the member template itself is defined. The usual rules for visibility in respect to name lookup apply. (Therefore two different delegating classes cannot define conflicting specializations for a function in a delegatee class.)

The compiler generation of forwarding functions through a member sub-object which is a pointer applies only to real pointers. If the delegatee object is not a real pointer, but some kind of smart pointer class which defines operator->(), the generated functions will nevertheless call operator.() rather than operator->(). It can be argued (by others, if they want to) that any object which defines operator->() is the moral equivalent of a pointer and should be substitutable for one. However, I am leaving that out of this proposal because adding this new function to some class during maintenance could unknowingly break other code which delegates to an object of the revised class. Programmers who want to delegate to a smart pointer can always write their own forwarding functions.

A delegatee pointer might itself be a base class pointer to some derived object. If so, the forwarding function invokes the behavior found by normal vtbl lookup – the derived class’s overriding function.

A using-delegation-declaration may not refer to a nested class of a member sub-object (if only because I can't see any use for the construct).

 

Prior Art

[D&E] (12.7) discusses an earlier proposal to add delegation to C++. The delegation feature involved specifying a pointer to some class among the base classes in a class declaration:

class B { int b; void f(); };
class C : *p { B* p; int c; };

A C object would "inherit" the public functions of B and delegate them to the object referenced by p, which could be changed at runtime by assigning to the delegation pointer.

void func( C * q )
{
q->f(); // meaning q->p->f();
}

Unfortunately every user of this delegation mechanism suffered serious bugs and confusion, attributed to two problems which led to this idea being excluded from Cfront 2.0:

[1] Functions in the delegating class do not override functions of the class delegated to.

[2] The function delegated to cannot use functions from the delegating class or in other ways "get back" to the delegating object. This meant that it was not possible to delegate through a B* pointer to an object of derived class C. And changing the implementation mechanism to address these two issues would make it impossible for two derived objects to delegate to the same "shared" base object.

Because the using-delegation-declarations discussed in this paper do not involve inheritance in any way, I hope they can avoid the conceptual baggage that apparently caused some of the confusion. A containing class can "override" some functionality by defining its own function instead of delegating through a declaration. Two objects can share a delegatee by delegating through a pointer to the same object.

[D&E] (12.8) also mentions a proposal to add renaming to C++ to resolve name clashes arising in multiple inheritance hierarchies. However, it was discovered that the problem could be solved with simple forwarding functions: "Consequently, a language extension to express renaming is not necessary and is only worthwhile if the need to resolve such name clashes proves common." Stroustrup also observes, "A further -- and more general -- objection to renaming is that I dislike chasing chains of aliases while maintaining code. If the name I see spelled f is really the g defined in the header that actually is described as h in the documentation and what is called k in your code, then we have a problem... Every renaming requires understanding a mapping by both users and tools. "

Both of these comments are equally valid in reference to the renaming proposal in this paper. However, while a class cannot appear more than once in the list of direct base classes, it is conceivable that more than one member sub-object might commonly be of the same type:

class Battleship
{
Motor port_engine;
Motor starboard_engine;
Cannon battery1;
Cannon battery2;
public:
using battery1 { void fire() = fire1; }
using battery2 { void fire() = fire2; }
// ...
};

It is also conceivable that a class might commonly contain a more varied group of sub-objects than it is likely to inherit from. Either of the circumstances would increase the likelihood of name clashes which would have to be resolved through manual forwarding functions if the renaming facility is not present.

In response to the comment that too much renaming obfuscates code, it can be argued that there is less conceptual correspondence between a delegating class and its private members used for implementation than there is between a base class and its substitutable derivatives. The new names become part of the interface of the delegating class and should be relevant to its context. If the delegating class later substitutes a different implementation, its public interface need not change. In any case, simple forwarding functions can always accomplish the renaming, without any need for special syntax to support it.

Possible Disadvantages

This is a pure extension to the syntax, so it would not break any existing code. However, it might create a user expectation that certain syntax should be valid in other contexts. For example, if 7.3.3p5 is revised to allow a using declaration at class scope to refer to a single template specialization, then people might logically expect that permission should extend to using-declarations at namespace scope as well.

Currently using-declarations refer to names rather than specific functions, so that all overloaded versions of a name are covered by the using-declaration. Again, a non-expert user might expect the rules for class-scope and namespace-scope declarations to be similar. On the other hand, it can be argued that greater consistency in the rules for scopes (whether class- or namespace-delimited) would be no bad thing.

If writing them manually, most people would choose to designate simple forwarding functions inline, for better performance. Compiler-generated forwarding functions could logically be expected to be inlined as well. However, inline functions create a hazard that binary compatibility may be broken if programmers later write manual forwarding functions to replace them. There is a similar problem with compiler-generated default constructors. Compiler vendors should have the freedom to generate forwarding functions in ways that reduce this hazard, if it is possible. In environments where binary compatibility is important, programmers may need special education to avoid problematic constructs.

Additional Extensions Which Are Not Part of This Proposal

Some comments I have received suggest even more extensions to C++ syntax. I am not arguing for these proposals, but I include them here for completeness:

Class-scope using-directives:

class Derived : protected Base1, protected Base2
{
private:
Contained c;
public:
using Base1;
using c;

This proposal would enable the complete public interface of a base class or member object to be brought into scope in the delegating class. It would require the compiler to identify only the public members of the delegatee class and generate forwarding functions only for them.

It can be argued that the extension would provide better support for meta-programming constructs such as generic proxies.

However, if the desire is to bring in the whole interface, one might as well use public inheritance. Of course this would this then permit automatic conversions to the base class, but there is a separate unrelated proposal to allow the explicit qualifier to be applied to base classes, to prevent automatic conversions. I think that is a superior mechanism to address this problem.

Semantic change to using-declaration:

using Base2::Some_overloaded_function;
};

This suggestion was proposed by an expert C++ user who was surprised when the using-declaration encompassed not only the public functions but also the private (and therefore inaccessible) overloads in the base class. It would revise 7.3.3p14 to apply only to public and protected members of the base class.

Alternatively, an extension would allow some functions to be removed from the interface:

not using Base2::Some_overloaded_function( bool dont_want_this_one );

Inheritance is an expression of generalization—specialization. Narrowing the inherited interface in a derived class is generally considered shaky on theoretical grounds.

Strengthened using-declarations at namespace scope

Currently a using-declaration at namespace scope creates an alias for a class or function name from another scope, but does not make it a member of the namespace for purposes of Koenig lookup. Strengthening using-declarations to enable namespaces to be assembled by incorporating names from other namespaces is a much larger change to the core language than is suggested in this paper. Nevertheless, I believe some changes in this direction will be necessary to provide proper support for versioning libraries (including the next version of the Standard Library).

Conclusion

Considering that C++ currently has a perfectly adequate mechanism – forwarding functions – to express delegation to a member object, it is hard to argue that there is a burning need for this extension to the syntax. However, adding direct language support for delegation would provide an example of good practice, ideally helping novices better to understand OO design principles and to produce more robust code. There may also be some benefit to template metaprogramming, such as writing generic proxy classes.

When the Evolution Group examines issues of versioning libraries and the specific issue of the next version of the Standard Library, I expect that some modifications to using-declarations will need to be considered. In the context of those discussions, it would be appropriate to consider additional extensions to provide direct support for good design constructs.

 

Reference:

[D&E] Stroustrup, Bjarne. The Design and Evolution of C++. Addison-Wesley, 1994.