This document describes design rules to follow while developing and using object-oriented frameworks for communication systems.
void *thread_entrypoint (void *arg)
{
// ...
}
// ...
thr_create (0, 0, thread_entrypoint, param, 0, &tid);
However, defining functions like my_callback at global
scope can pollute the namespace. Namespace pollution makes it hard to
integrate the various frameworks and class libraries. Therefore, all
global functions in a framework should be preceded with a unique
prefix. One way to do this is as follows:
void *ace_thread_entrypoint (void *arg)
{
// ...
}
// ...
thr_create (0, 0, ace_thread_entrypoint, param, 0, &tid);
Often, a better way to structure this code is to scope it as a static
method within a class, as follows:
class ACE
{
static void *thread_entrypoint (void *arg)
{
// ...
}
};
// ...
thr_create (0, 0, ACE::thread_entrypoint, param, 0, &tid);
This solution is more abstract, but may cause problems with certain
C++ compilers (such as the IBM MVS C++ compiler) that don't allow
static member functions to be used as parameters for functions that
expect C functions (such as thr_create or
signal). If a compiler supports namespaces, an even better way to structure this code is to scope it within a namespace, as follows:
namespace ACE
{
void *thread_entrypoint (void *arg)
{
// ...
}
}
// ...
thr_create (0, 0, ACE::thread_entrypoint, param, 0, &tid);
However, many older C++ compilers don't support namespaces yet, so
this approach is less portable.
ace_ prefix. Moreover, it places most stand-alone
functions as static methods within the ACE class.
class A *arr = Allocator::malloc (MAX * sizeof (class A)); for (int i = 0; i < MAX; i++) new (arr + i) A (i); // ... Allocator::free (arr);
for (int i = 0; i < MAX; i++) (arr + i)->A::~A (); // arr[i].A::~A (); is equivalent,Note that it is incorrect to attempt to use delete on any part of arr, since the the memory was not allocated with new.
In addition, if the line of code is executed in a performance-critical section of the program many C++ compilers will not optimize this.
Adherence to this rule can lead to framework designs that are more cohesive, and easier to understand.
May result in programmers manually creating temporaries before derefencing the next method call. However, these can often be optimized away by the compiler.
namespace ACE_OS {
char *
mktemp (char *s) {
#if defined (ACE_LACKS_MKTEMP)
// ... framework provided emulation
#else
return ::mktemp (s);
#endif /* ACE_LACKS_MKTEMP */
}
}
Thus, a temporary file can now be created with a call to
ACE_OS::mktemp().
If a framework design lacks consistent error-handling, then semantically similar objects (such as containers) may present the framework user with different mechanisms for detecting error conditions. Which may be a potential source of confusion.
#if defined (ACE_WIN32) typedef HANDLE ACE_HANDLE; #else typedef int ACE_HANDLE; #endif /* ACE_WIN32 */Here, HANDLE is the Win32 type name given for file descriptors, whose underlying representation is a void *.
The ACE programming framework provides such tracing capabilities through its ACE_DEBUG macro. This flexible facility has the ability to send the trace messages to the screen, a file, or to a log server.
Moreover, as indicated in the Initialize on first use rule, it is often useful to create static objects but to postpone their initialization (until the existence and initialization of their dependents can be verified). The existence of an open() method provides a mechanism for allowing this to take place.
For destructors, consider the Use unguarded destructors rule. However, if shared data has been dynamically allocated, then it becomes necessary to serialize the contexts that attempt to release the allocated data. The presence of a close() method provides a callable method for which this can be done and allows the destructor to remain guard free.
class ACE_SOCK_IO
{
public:
ssize_t send (const void *buf, int n) const
{
// Call underlying OS function to send <buf>.
return ::write (this->get_handle (), buf, buf_size);
}
// ...
};
Although this approach works, it is inflexible since it hard-codes
the use of inlining into the framework and makes it difficult to
separate the interface of a class from the implementations of its
methods. It is often useful, however, to disable inlining when debugging a framework or debugging applications build using a framework for the following reasons:
// System-wide include file OS.h
#if defined (__ACE_INLINE__)
#define ACE_INLINE inline
#else
#define ACE_INLINE
#endif /* __ACE_INLINE__ */
// Header file SOCK_IO.h
#include "ace/OS.h"
class ACE_SOCK_IO
{
public:
ssize_t send (const void *buf, int n) const;
// ...
};
#if defined (__ACE_INLINE__)
#include "ace/SOCK_IO.i"
#endif /* __ACE_INLINE__ */
// Include file SOCK_IO.i
ACE_INLINE
ssize_t
ACE_SOCK_IO::send (const void *buf, int n) const
{
// Call underlying OS function to send <buf>.
return ::write (this->get_handle (), buf, buf_size);
}
// Implementation file SOCK_IO.cpp
#if !defined (__ACE_INLINE__)
#include "ace/SOCK_IO.i"
#endif /* __ACE_INLINE__ */
If the inlined code has not been separated, then the change causes all dependent objects to be recompiled. In addition, by completely separating method implementation from method interfaces, header files can more clearly document the semantics of each method.
inline keyword directly in the *.i file,
as follows:
// Include file SOCK_IO.i
inline
ssize_t
send (const void *buf, int n) const
{
return ::write (this->get_handle (), buf, buf_size);
};
Note that it is still a good idea to have a separate include file,
rather than including the method implementation in the class
definition to enhance clarity and ensure flexibility for future
changes.
#ifndef TASK_H
#define TASK_H
class ACE_Task_Base
{ // ...
};
template
class ACE_Task : public ACE_Task_Base
{ // ...
};
#endif
However, upon compiling applications based on the framework, the
developer finds the linker to complain about multiple definitions
for ACE_Task_Base.
| class.h |
class.i |
#if !defined (CLASS_H)
#define CLASS_H
#include "ace/OS.h"
class A
{
public:
ACE_INLINE A (void);
~A (void);
};
#if defined (__ACE_INLINE__)
#include "class.i"
#endif /* __ACE_INLINE__ */
#endif /* CLASS_H */
|
#if !defined (CLASS_I)
#define CLASS_I
A::A (void)
{
// ...
}
#endif /* CLASS_I */
|
| class.cpp |
|
#include "class.h"
#if !defined (__ACE_INLINE__)
#include "class.i"
#endif /* ! __ACE_INLINE__ */
A::~A (void)
{
// ...
}
|
The alternative would be identify every conditional pre-processing line in the source and determine whether or not the platform under consideration requires to be added to the conditional or not.
#ifdefs UNIX
// ...
#else
// ...
#endif
Everywhere the code needs to change.
One way to control of memory allocation/deallocation in C++ is to
overload operators new and delete either
globally or in a class-specific manner. However, this design is too
general and can cause many unrelated parts of the system to behave
incorrectly.
A more flexible way of controlling memory allocation/deallocation in
C++ is to define an Allocator component that can be
parameterized in various ways, such as on a per-object or per-thread
basis. For example, rather than saying:
Image *create_image (bool use_shared_memory)
{
Image *image;
if (use_shared_memory)
{
// ... mmap() file, obtain a pointer to shared memory
// region, etc.
image = new (shared_memory_pointer) Image (use_shared_memory);
}
else // use local memory.
image = new Image (use_shared_memory);
return image;
}
Developers can write
Image *create_image (ACE_Allocator *allocator)
{
char *buf = allocator.malloc (sizeof Image);
return new (buf) Image (allocator);
}
This design makes it possible to replace memory management strategies
wholesale without affecting the application interfaces. In addition,
it greatly reduces the effort required to keep track of which memory
management strategies are associated with each object or thread.
bind/unbind, which insert and remove items in
a container and find, which searches for an item in
a container. Common implementations of these containers use
linked data structures to implement hash tables, lists, and
trees. In non-threaded applications, it is often possible to speed up container operations by using a sentinel. For instance, an implementation of a map that uses a hash table with ``bucket chaining'' to resolve collisions might be implemented as follows:
templateIn a sentinel-based hash-table implementation, the constructor typically allocates a sentinel and initializes all the pointers in hash table to reference the sentinel, as follows:class ACE_Hash_Map_Manager { public: ACE_Hash_Map_Manager (size_t table_size); // Constructor. int find (EXT_ID ext_id, INT_ID &int_id); // Returns the if is in the hash table. // ... private: struct ACE_Hash_Map_Manager_Entry { EXT_ID ext_id_; // External identify used as a key to find // the internal identifer. INT_ID ext_id_; // Internal identifier holds the value. ACE_Hash_Map_Manager_Entry *next_; // Points to the next entry in the overflow bucket. }; ACE_Hash_Map_Manager_Entry **hash_table_; // Array of pointers to the linked list of entries. ACE_Hash_Map_Manager_Entry *sentinel_; // Sentinel node. LOCK lock_; // Synchronization strategy. };
ACE_Hash_Map_ManagerOnce the sentinel has been initialized, the::ACE_Hash_Map_Manager (size_t table_size) { hash_table_ = new ACE_Hash_Map_Manager_Entry *[table_size]; sentinel_ = new ACE_Hash_Map_Manager_Entry; // All pointers in the table initially point to the sentinel. for (size_t i = 0; i < table_size; i++) hash_table[i] = sentinel_; }
find method
can be optimized as follows:
int ACE_Hash_Map_ManagerAlthough this code will work correctly if given a ``NULL'' lock (such as::find (EXT_ID ext_id, INT_ID &int_id) { ACE_Read_Guard mon (lock_); // This guard calls the acquire_read() method of class LOCK on // object lock_. // Initialize the sentinel with the value of // the entry we're trying to find. sentinel_->ext_id_ = ext_id; // Compute the hash value. size_t index = ext_id.hash (); // Find the beginning of the bucket chain. ACE_Hash_Map_Manager_Entry *entry = hash_table_[index]; // Because we put the value we're looking for in the sentinel, // we don't need to check for a NULL pointer the end of the // chain since we'll always find the entry. while (entry->ext_id_ != ext_id) entry = entry->next_; // Determine if we really found the entry or just the sentinel. if (entry == sentinel_) return 0; else { int_id = entry->int_id_; return 1; } }
ACE_Null_Mutex) it will fail if given a readers/writer
lock (such as ACE_RW_Mutex). The problem is that a
readers/writer lock only works correctly if the region of code it
protects does not modify the state of the object. In this case, the
sentinel_ maintains state that is shared by all threads
that access the hash table. Therefore, the find can fail
since there are race conditions if multiple threads concurrently
update the sentinel_.
find operations
reduces contention by removing the critical section around
the storage of search value in the sentinel. In addition, it
also avoids subtle race conditions that arise if the
synchronization strategies are configured into parameterized
types.
find operations and
can be configured with readers/writer locks do not use sentinels.
This problem is resolved by providing private internal methods that perform the actual actions of finding, inserting and removing. These internal methods assume that locks are already held when they are called, and so do not require locking themselves. If these methods are called find_i, insert_i, and remove_i, then update can easily be implemented by applying remove_i followed by insert_i.
One possible design is to have both sensor inputs be modeled as prioritized events that are fed into a single event queue, and each thread queries the the event queue for their respective sensor input. The problem with this approach is that it may happen that the higher priority thread may be blocked out by the lower priority thread if the lower priority holds a lock on the queue while a new sensor input has come in for the higher priority thread. This results in priority inversion.
The better design is to dedicate a separte event queue for each thread, which removes contention for accessing the events.
In ACE, this is accomplished by creating overloaded wrapper functions for these utility functions. For example, ACE provides the following typedef and wrappers for these two static functions.
#if defined (UNICODE)
typedef char TCHAR
#else
typedef wchar_t TCHAR
#endif
namespace ACE
{
static int strcmp (const char *s, const char *t);
static int strcmp (const wchar_t *s, const wchar_t *t);
}
Users of the framework can use TCHAR, and their code can then be
easily ported to a UNICODE environment.