Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C++/CLI Delegates and Events


July, 2005: C++/CLI: Delegates and Events

Rex Jaeschke is an independent consultant, author, and seminar leader. He serves as editor of the Standards for C++/CLI, CLI, and C#. Rex can be reached at [email protected].


We have seen numerous examples in which a handle can be made to refer to different objects at different times, allowing a more abstract approach to certain programming problems. You can achieve a similar effect with functions by using a delegate—an object that encapsulates some function, and for instance functions, it also associates that function with a particular instance. Once a delegate has been made to encapsulate some function, you can invoke that function via that delegate, without knowing which function has been encapsulated. (As you'll see, a delegate can actually encapsulate more than one function at a time.)

Consider the example in Listing 1. In case 1, you define a delegate type Del. This involves using the contextual keyword delegate with what looks like a function declaration; however, rather than declaring a function, you are declaring that an instance of the delegate type Del is capable of encapsulating any function taking one argument of type int and returning a value of type int. (Any valid argument list and return type combination is permitted.) Once a delegate type has been defined, it can only be used to encapsulate functions having that type. A delegate type can be defined at file or namespace scope, as well as inside a class. It can also have public or private visibility.

The static function A::Square and the instance function B::Cube both have the same argument list type set and return type as Del, so they can be encapsulated by a delegate of that type. Note that even though both functions are public, their accessibility is irrelevant when considering their compatibility with Del. Such functions can also be defined in the same or different classes, as the programmer sees fit.

Once a delegate type has been defined, you can create handles to instances of that type, as in case 2. Then you can initialize or assign to such an instance in case 2, where you make d encapsulate the static function A::Square, and in case 5, where you make it encapsulate the instance function B::Cube. (There is no good reason to make Cube an instance function in this example, other than for demonstration purposes.)

Creating a delegate instance involves calling a constructor. If you are encapsulating a static function, you pass in one argument, a pointer to that member function. For an instance function, you must pass in two arguments: a handle to that instance, as well a pointer to the instance member function.

Once a delegate instance has been created, it forever encapsulates the same given function. Of course, any handle that refers to that instance can be made to refer to some other instance.

Once a delegate instance has been initialized, you can indirectly call the function it encapsulates just as you would call it directly, except you use the delegate instance's name instead, as in cases 3 and 6. The value (if any) returned by the encapsulated function is obtained the same way as a direct function call. If a delegate instance has the value nullptr, and an attempt is made to call the "encapsulated" function, an exception of type System::NullReferenceException results.

The output produced is:

d(10) result = 100
d(10) result = 1000

Passing and Returning Delegates

Sometimes it is useful to be able to pass an encapsulated function to another function. The receiving function has no idea which function it is being passed, and it doesn't care; it simply calls that function indirectly through the encapsulating delegate.

For the most part, the only thing different about sorting a collection of elements in one order versus another is the way in which pairs of elements are compared. If you can provide the comparison function at runtime, a sort routine can sort in any order for which a corresponding comparison function has been defined. Listing 2 contains an example.

The delegate type Compare can encapsulate any function taking two String^ arguments and returning an int result. Two such functions are StrCompare::CompareExact and StrCompare::CompareIgnoreCase.

In case 6, I create an instance of delegate type Compare, and have it encapsulate StrCompare::CompareIgnoreCase. The handle to this delegate is passed to function Sort, which then proceeds to use whichever comparison function it was passed to sort some strings, as follows.

As you can see, Sort takes one argument whose type is a delegate handle (which, like any other argument, can be passed by value, address, or reference).

In case 7, I call function FindComparisonMethod, which returns a delegate of type Del. I then proceed to call the encapsulated function in cases 7 and 8. Case 8 is worth a further mention: First, FindComparisonMethod is called to get the delegate instance, which is then used to invoke the underlying function. The two function call operators have the same precedence, and they associate left-to-right.

FindComparisonMethod uses some (unspecified) logic to figure out just which function to encapsulate.

Delegate Type Compatibility

A delegate type is compatible only with itself; it is not compatible with any other delegate type, even if that other type encapsulates functions having the same type. Given the example in Listing 3, it is clear that delegate type D1 is compatible with functions A::M1 and A::M2. It is also clear that delegate type D2 is compatible with those functions. However, the two delegate types cannot be used interchangeably in cases 5, 6, 8, and 9.

Combining Delegates

A delegate instance can actually encapsulate more than one function. In such cases, the set of encapsulated functions is maintained in an invocation list. When two delegate instances are combined, their invocation lists are concatenated in the order specified, resulting in a new list; the two existing lists are not changed. One or more functions can be removed from an invocation list, resulting in a new list. Again, the original list is unchanged. Consider the example in Listing 4, in which the output produced by the indirect function calls is shown immediately following each call.

Delegates are combined via the binary + and += operators, as in cases 3 and 4. The two single-entry lists are concatenated into a new two-entry list, in the order left operand then right operand. The new list is referred to by cd3, and the two existing lists are unchanged. Delegates of different types cannot be combined with each other.

As you can see in case 4, the same function can be encapsulated multiple times in an invocation list. As shown in case 5, an invocation list can contain both class and instance functions. Delegates are removed via the binary - and -= operators, as is done in case 6.

When the same function is in an invocation list multiple times, a request to remove it results in the right-most entry for it's being removed. In case 6, this results in a new three-entry list, which is referred to by cd3, and the previous list is unchanged (although, because that list previously referred to by cd3 now has a reference count of zero, it can be garbage collected).

When the only entry is removed from an invocation list, a delegate removal operation results in the value nullptr; there is no such thing as an empty invocation list. Rather, there is no list at all.

Listing 5 contains another example of delegate combination and removal. As you can see in cases 3a and 3b, two multientry invocations are concatenated in the order left operand, then right operand.

Attempts to remove a multientry list succeed, but only when the list to be removed is an exact contiguous subset of the list from which it is being removed. For example, in case 4b, you can remove F1 and F2 because they are adjacent; likewise for the two F2s in case 5b and F2 and F1 in case 6b. However, in case 7b, there are not two consecutive F1s in the list, so the operation fails and the resulting invocation list is the one you started with, which contains all four entries.

System::Delegate

The definition of a delegate type causes the implicit creation of a corresponding class type, and all such delegate types are derived from the library class System::Delegate. The only way to define such a class is via the delegate contextual keyword. Delegate classes are implicitly sealed, so they cannot be used as base classes. In addition, a nondelegate type cannot be derived from System::Delegate.

The example in Listing 6 demonstrates the use of several Delegate functions:

The instance function Test::M is compatible with the delegate type D. When called, this function simply identifies the object for which it was called, along with some integer argument value.

In cases 1, 2, and 3, I define three objects of type Test, and encapsulate each of them and instance function Test::M in a separate delegate of type D. I then create an invocation list with four entries in case 4.

Provided the invocation list passed to it is not empty, function ProcessList calls all functions in that list, except for those encapsulating a given object. For example, in case 5a, no entry is excluded, so all functions are called. In case 5b, object t1 is excluded, while in case 5c, both entries pertaining to object t2 are excluded, resulting in this output:

Object t1: 100
Object t2: 100
Object t3: 100
Object t2: 100
Object t2: 200
Object t3: 200
Object t2: 200
Object t1: 300
Object t3: 300

In case 6b, I call Clone to make an exact copy of the delegate cd4. This function returns an Object^; hence, the cast to type D^. When the original and cloned delegates are invoked in cases 6c and 6d, the output produced is:

Object t1: 5
Object t2: 5
Object t1: 6
Object t2: 6

Regarding the function ProcessList, if the delegate instance is nullptr, there is no invocation list, so the function returns. If the object to be excluded is nullptr, then all functions in the invocation list are called. Otherwise, you extract the invocation list as an array of Delegate in case 8, then step through that array looking for entries that do not match the object to be excluded, as in case 9, and call such functions in case 10. Although each entry in the invocation list has type Del, GetInvocationList returns an array of the base type Delegate, so you must cast to type D in case 10 before invoking each delegate instance.

Events

In C++/CLI, an event is a mechanism for a class to provide notifications to its clients when some sort of significant thing happens. A typical example of an event is a mouse click. Prior to that event occurring, all interested parties will have registered themselves as being interested, so when the mouse click is detected, those parties can be notified.

The list of interested parties can grow and shrink at runtime by adding and removing one or more interested parties. Consider the definition of type Server in Listing 7. In case 1, class Server defines the delegate type NewMsgEventHandler. (The convention is to give delegate type names the suffix "EventHandler" when they are used in event handling.) Then in case 2, it defines a public event called ProcessNewMsg. (event is a contextual keyword.) An event must have a delegate type. In reality, an event such as this one is really a delegate instance, and since it is initialized to nullptr by default, it has no invocation list.

When called with a message, the public function Broadcast calls all the functions encapsulated in ProcessNewMsg's invocation list.

Class Client is defined in Listing 8. Whenever an instance of type Client is created, it registers its interest in being notified about new Server messages by adding an instance function (and its associated instance variable) to the delegate list being maintained for Server::ProcessNewMsg. This is done via the += operator, as in case 5. So long as this entry remains in the notification list, the function registered will be called whenever a new message is posted to Server.

To remove an entry from the notification list, you use the -= operator, as in the destructor defined in case 6.

Listing 9 is the main program. Initially, no functions are registered, so no one is notified when the first message is sent. However, once client c1 is constructed, the notification list contains one entry for that object. The subsequent construction of c2 and c3 cause the list to grow to three entries. Then once these objects are disposed of (via explicit calls to the destructor), the number of entries is reduced, until finally, there are none, so when the final message is broadcast, no one is listening. Here, then, is the output produced:

Client A received message Message 2
Client A received message Message 3
Client B received message Message 3
Client A received message Message 4
Client B received message Message 4
Client C received message Message 4
Client B received message Message 5
Client C received message Message 5
Client C received message Message 6

Although all three client objects have the same type, this is not required. So long as their types define a function that is compatible with NewMsgEventHandler, they can be any class type.

The event used in the aforementioned example is a trivial event. In addition, just like a trivial property, such an event is automatically backed up by private storage, and add and remove accessor functions are automatically generated. To customize what these add and/or remove accessor functions do, you must make the event nontrivial by providing definitions for these functions, as in Listing 10. The names add and remove are contextual keywords.

Reader Exercises

To reinforce the material we've covered, try this:

  1. Are two different delegate constructors really used when delegates for static and instance members are created? Use the assembly produced by the example in Listing 1 to find out.
  2. What are the advantages of delegates over pointers to functions?
  3. Find out the members available to you from type System::Delegate. In particular, read about Clone.
  4. Using one of the "combining delegates" assemblies, see how the compiler turns +, -, +=, and -= operations into calls to a member function of System::Delegate. You can call these functions directly yourself. Try it.
  5. Using the assembly produced by the event example, see the following: The delegate NewMsgEventHandler is a class nested inside class Server. The event ProcessNewMsg is automatically backed-up by a private instance data member. What is that member's name? Note the three member functions (add_ProcessNewMsg, raise_ProcessNewMsg, and remove_ProcessNewMsg) automatically created by the compiler. Confirm that the add and remove accessor functions are used to implement the += and -= operations on an event in Client.
  6. A delegate is just another handle type, so you can have arrays of delegates. Consider a system that processes five different types of transactions using five corresponding functions. Create an array of handles to delegates, each of which encapsulates one of these functions. Then loop 20 times getting a random number in the range 1-5, inclusive, and use that number as an index into the array, calling the function encapsulated by the corresponding delegate. (An array of delegates is sometimes referred to as a "jump table" because we can jump directly to a function based on the value of an index into that table.)


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.