Coherence C++ is the latest addition to the Coherence product family. Shipped as a pure native library, Coherence C++ is made available for a select but growing number of platforms. As of Coherence 3.5, support is available for recent versions of Windows, Linux, Solaris, and Apple OS X—in both 32 and 64 bit variants.
Because Coherence C++ is a Coherence*Extend client, the available feature set is virtually identical to what is offered with the Coherence Java and .NET Extend clients. This includes the basic get/put cache access, as well as advanced features such as near cache, continuous query cache, events, entry processors, aggregators, and queries.
The C++ client used the Coherence Java client as a guide in terms of class and method names and signatures. The result is that it is quite easy to transfer your newly acquired Coherence Java knowledge to C++.
Coherence C++ supports the same configuration mechanisms, which you've seen being used in the Java and .NET editions. The C++ variants are modeled as a subset of the Java edition, and are DTD-based XML files. Because of this, it is unnecessary to reiterate most of the configuration options; you can reuse the cache and operational configuration files that had been built up for Java Extend clients.
There are exceptions worth noting. For instance, the system property configuration override feature of the Java client is available to C++ clients in a somewhat different form—in that environment variables are used. In some Unix shells, '.' delimited environment variables may not be supported. For this reason, you may also specify the property names in camel case, so tangosol.coherence.override
and TangosolCoherenceOverride
are considered to be equivalent. Another notable configuration difference in comparison with Java is that for advanced configurations where custom classes are supplied via configuration, you would make use of C++ naming conventions rather the Java ones, that is, '::' rather than '.' class name delimiters.
POF configuration is where C++ differs from Java and .NET. Currently, C++ POF configuration is programmatic rather then declarative. This means that you will need to include your POF type ID to class registration within your code. This is accomplished via a simple one-line addition to your data object's .cpp
file. POF configuration will be covered in detail later when we demonstrate how to serialize your data objects.
If included in the application's startup directory, the cache and optional operational configuration files will be automatically loaded without any additional effort. The default filenames are inherited from the Java client, namely coherence-cache-config.xml
and tangosol-coherence-override.xml
. If an alternative location or filename is used, you will need to tell Coherence C++ where to look for these files. This can be done programmatically by calling the CacheFactory::configure
method, which takes up to two XmlElement
configuration descriptors as input. The first argument is the cache configuration and the optional second argument is the operational configuration. By taking in XmlElements
rather than file descriptors or names, it allows the application to determine how the configuration content is obtained. As the most likely means of doing so is from a file, convenience helpers are also included so that you can easily load the XML configuration from a file as follows:
CacheFactory::configure( CacheFactory::loadXmlFile("c:/config/MyCacheConfig.xml"));
Alternatively, you can use environment variables to override the default name and location if you wish to avoid specifying them programmatically. The names are again inherited from Java. The tangosol.coherence.cacheconfig
property specifies the path to the cache configuration, and tangosol.coherence.override
specifies the path for the operational configuration.
The Coherence C++ API is built on top of a managed object model, which takes on the responsibility for object lifecycle management. This means you will not make explicit calls to new
or delete
when dealing with managed objects. A managed object is an instance of any class that derives from coherence::lang::Object
. This base class is responsible for maintaining the object's reference count, allowing for automatic destruction once the object is no longer being referenced.
This model is useful for Coherence because caches both return and retain references to their cached objects. A cache may be a subset of a larger data set and may evict an item while the application could still be using a previously obtained reference to the same item. The result is that there is no clear owner for cached items, and thus no answer to the question of who should take responsibility for deleting these objects. The managed object model removes this issue by taking over the ownership, ensuring that objects are automatically deleted once they are no longer being referenced.
Application code interacts with these managed objects not via traditional C++ raw pointers, or references, but via smart pointers that transparently update the managed object's reference count, but are otherwise used much the same way as raw pointers. While many C++ smart pointer implementations will declare the smart pointer for a given class Foo
via templates (that is smart_ptr<Foo>
and smart_ptr<const Foo>)
, the Coherence managed object model uses nested typedefs to declare the smart pointers for a class (that is Foo::Handle
and Foo::View)
.
For each managed class, there are three smart pointer typedefs: Handle, View
, and Holder
.
For a given managed class Foo:
Foo::Handle
is the functional equivalent of Foo*
Foo::View
is the functional equivalent of const Foo*
Foo::Holder
is the functional equivalent of a union of a Foo*
and const Foo*
A Holder
acts just like a View
, but allows for a safe cast attempt to a Handle
. Unlike the standard C++ const_cast<>
the Coherence cast<>
is safe and only succeeds if the Holder
had been assigned from Handle
. A Holder
is typically used with containers such as coherence::util::Collection
, which allows the caller to choose to store either a Handle
or View
within the collection.
The assignment rules for the smart pointers follow the same rules as regular pointers:
A Holder
may be assigned from a Handle, View
, or Holder
, and will remember the smart pointer type it has been assigned from.
A Handle
may be assigned from a Handle
, or from a Holder
via cast<>.
A smart pointer for a parent class may be assigned from the smart pointer of a derived class.
A smart pointer may only be assigned from a non-derived class via cast<>
.
To properly maintain the reference count, it is important that the objects are only referenced via smart pointers, and never via raw pointers. As such, creation is not performed via the traditional new
operator, but instead by a static create
factory method (for example, Foo::create())
. This factory method returns the initial Handle
, ensuring that raw pointers are avoided from the point of object creation.
The factory methods are auto-generated via inherited template definitions, and take parameter sets matching the class's constructors. The constructors are declared as protected, to ensure that these objects are not allocated by other means.
As previously mentioned, the object model also includes a helper function for performing dynamic casts. Its functionality is roughly equivalent to the standard C++ dynamic_cast
, although it is specialized to work with managed classes and their smart pointers. For example, given an Object::View
, we can attempt to cast it to a String::View
as follows:
Object::View v = ... String::View vs = cast<String::View>(v);
If the View
referenced something other than a String
, then a ClassCastException
would be thrown from the cast<>
function call. There is also a related instanceof<>
helper function, which will identify if a cast will succeed.
Object::View v = ... if (instanceof<String::View>(v)) { // v references a String }
These functions work similarly for a Handle, View
, or a Holder
. In the case of a Holder
, they are also the mechanism for extracting a stored Handle
. The following cast will succeed if the Holder
was assigned from String::Handle:
Object::Holder oh = ... String::Handle hs = cast<String::Handle>(oh);
Error conditions in Coherence C++ are communicated by throwing exceptions. The managed object model defines a hierarchy of managed exceptions, rooted at coherence::lang::Exception
. A special COH_THROW
macro is used to throw and re-throw managed exceptions. This macro will, if possible, record a full stack trace into the exception to aid error diagnosis.
The exceptions are caught via their nested (or inherited) View
type:
try { ... COH_THROW (IOException::create("test")); ... } catch (IOException::View vexIO) { // handle IOException } catch (Exception::View vex) { // print exceptions description and stack trace std::cerr << vex << std::endl; COH_THROW (vex); // re-throw }
Managed exceptions may also be caught as std::exceptions
, allowing pre-existing error handling logic to handle them as well. For instance, a thrown IOException
could also be caught as follows:
try { ... COH_THROW (IOException::create("test")); ... } catch (const std::ios_base::failure& exIO) { // handle IOException or other } catch (const std::exception& ex) { // handle any Exception or std::exception std::cerr << ex.what() << std::endl; throw; }
The Coherence classes are organized into a set of namespaces based on their functionality. Their header files are organized similarly. For example, coherence::lang::Object
is defined in coherence/lang/Object.hpp
.
For convenience, there is a special header file for each namespace, which includes all headers for that namespace. This mechanism along with a using namespace
statement is often used to bring the entire coherence::lang
namespace into your application code, while explicit class headers and using
statements are preferred for classes outside of the coherence::lang
namespace:
#include "coherence/lang.ns" #include "coherence/net/CacheFactory.hpp" #include "coherence/net/NamedCache.hpp" using namespace coherence::lang; using coherence::net::CacheFactory; using coherence::net::NamedCache;
For the sake of simplicity, the examples in the remainder of the chapter do not show the include
or using
statements.
This basic introduction of the managed object model is enough to get us started with Coherence C++ coding. The object model, internally known as Sanka, also contains a large number of generic utility classes, which reside mostly in the coherence::lang
and coherence::util
namespaces. You'll find that these classes and the model itself have a noticeable Java inspiration to them. The Coherence C++ cache classes are also inspired by their Java counterparts. The result is that those familiar with Coherence Java should find Coherence C++ quite easy to learn.
There are many more details to the managed object model, but those details are beyond scope of this book. The Coherence C++ product documentation contains an in-depth guide to the object model, and it is recommended that you familiarize yourself with the object model in greater depth as you begin the work of building your Coherence C++ based solutions.
In this section, we will finally write some C++ cache-related code. Let's start by obtaining a NamedCache
from the CacheFactory:
NamedCache::Handle hCache = CacheFactory::getCache("accounts");
We can then proceed to operate on the cache in much the same way as we did in Java:
String::View vsKeySRB = "SRB"; String::View vsKeyUSA = "USA"; hCache->put(vsKeySRB, String::create("Serbia")); hCache->put(vsKeyUSA, String::create("United States")); hCache->put(vsKeyUSA, String::create("United States of America")); hCache->remove(vsKeySRB); std::cout << vsKeyUSA << " = " << hCache->get(vsKeyUSA) << std::endl; std::cout << "Cache size = " << hCache->size() << std::endl; hCache->clear();
The C++ NamedCache
interface contains the full method set from the Java NamedCache
, allowing access to other standard operations such as putAll
and getAll
.
You'll notice that, unlike Java, there is no automatic conversion support from quoted string literals to Coherence managed objects, except for methods whose signature states that they take a String
, such as CacheFactory::getCache(String::View)
. Thus to create and pass a String
to a method such as NamedCache::put(Object::View, Object:Holder)
, we must explicitly call String::create()
to produce our managed String. String
objects can also be assigned from and to std::string
and std::wstring
.
The previous example demonstrated how to use the basic cache APIs to store strings using C++. Of course, we want to work with more interesting types than just strings. Let's define a simple C++ class that will represent a pre-existing custom data object used in your application, which you'd like to cache.
class Account { // ----- data members ------------------------------------------- private: const long long m_lId; std::string m_sDescription; Money m_balance; long long m_lLastTransactionId; const long long m_lCustomerId; // ----- constructors -------------------------------------------- public: Account(long long lId, const std::string& sDesc, const Money& balance, long long lLastTransId, long long lCustomerId) : m_lId(lId), m_sDescription(sDesc), m_balance(balance), m_lLastTransactionId(lLastTransId), m_lCustomerId(lCustomerId) { } Account() : m_lId(0), m_lLastTransactionId(0), m_lCustomerId(0) { } // ----- accessors ----------------------------------------------- public: long long getId() const { return m_lId; } std::string getDescription() const { return m_sDescription; } void setDescription(const std::string& sDesc) { m_sDescription = sDesc; } Money getBalance() const { return m_balance; } long long getLastTransactionId() const { return m_lLastTransactionId; } long long getCustomerId() const { return m_lCustomerId; } };
Can we just pass an instance of this into the NamedCache::put()
method? Unfortunately, it is not quite that simple.
Remember that the caching API deals with managed objects as there is no clear owner for a piece of cached data. Our plain old Account
class is definitely not managed. Aside from it being a managed class, there are a few other basic requirements for cached data as well:
It should implement Object::hashCode/equals
(for keys)
It should implement Object::clone
(for values)
It should be POF serializable
In many cases, it may not be desirable to retrofit your data objects to be managed, as this could impose some far-reaching application-level changes. For this reason, the Coherence API includes a Managed<>
template adapter, which will adapt pre-existing classes so they may be stored in Coherence.
In order to be compatible with the Managed
template, the data object class must have:
A public or protected zero parameter constructor
A copy constructor
An equality comparison operator
A std::ostream
output function
A hash function
The Managed
adapter will implement the initial set of requirements, delegating where applicable to the previously described functions. Our Account
class already meets the first two requirements. So all that is needed to make it Managed-compatible
is to define three functions as follows:
bool operator==(const Account& accountA, const Account& accountB) { return accountA.getId() == accountB.getId() && accountA.getDescription() == accountB.getDescription() && accountA.getBalance() == accountB.getBalance() && accountA.getLastTransactionId() == accountB.getLastTransactionId() && accountA.getCustomerId() == accountB.getCustomerId(); } std::ostream& operator<<(std::ostream& out, const Account& account) { out << "Account(" << "Id=" << account.getId() << ", Description=" << account.getDescription() << ", Balance=" << account.getBalance() << ", LastTransactionId=" << account.getLastTransactionId() << ", CustomerId=" << account.getCustomerId() << ')'; return out; } size_t hash_value(const Account& account) { return (size_t) account.getId(); }
As you can see, adding these functions is quite simple and does not require that the data object takes on any awareness of Coherence. Now it becomes possible to use Managed<Account>
in our code:
// construct plain old Account object Account account(32105, "checking", Money(7374, 10, "USD"), 55, 62409); // construct managed key and value Integer64::Handle hlKey = Integer64::create(32105); Managed<Account>::Handle hAccount = Managed<Account>::create(account); // cache hAccount hCache->put(hlKey, hAccount); // retrieve the cached value Managed<Account>::View vResult = cast<Managed<Account>::View>( hCache->get(hlKey)); std::cout << "retrieved " << vResult << " from cache for key " << vResult->getId() << std::endl; // convert the cached value back to a non-managed type Account accountResult(*vResult);
This code demonstrates how a Managed<Account>
instance is created from a non-managed Account
data object.
The create method delegates to the copy constructor on the Account
class—thus, the Managed<Account>
instance is a copy of and retains no references to the Account
instance it was constructed from. Managed<Account>
is then inserted into the cache using its identifier as a key. Next, it is extracted from the cache. The NamedCache::get()
operation returns an Object::Holder
, which then must be dynamically cast back to the expected data object class. Finally, the Managed<Account>
instance is used to construct a non-managed Account
, which can be used by pre-existing application logic.
Note that it is certainly allowed for the application to use the Managed<Account>
object directly, because all of Account's
public methods are still accessible as is demonstrated when we call the getId
method.
The requirement to address is serialization, which when using the Managed adapter is accomplished by implementing two additional free functions:
template<> void serialize<Account>(PofWriter::Handle hOut, const Account& account) { hOut->writeInt64(0, account.getId()); hOut->writeString(1, account.getDescription()); hOut->writeObject(2, Managed<Money>::create( account.getBalance())); hOut->writeInt64(3, account.getLastTransactionId()); hOut->writeInt64(4, account.getCustomerId()); } template<> Account deserialize<Account>(PofReader::Handle hIn) { long long lId = hIn->readInt64(0); std::string sDesc = hIn->readString(1); Managed<Money>::View vBalance = cast<Managed<Money>::View>( hIn->readObject(2)); long long lTransId = hIn->readInt64(3); long long lCustomerId = hIn->readInt64(4); return Account(lId, sDesc, *vBalance, lTransId, lCustomerId); }
Notice that Account
includes a data member Money
, which is an instance of another plain old C++ class. To serialize this nested object, we simply write it out as a Managed<Money>
object, applying the same patterns as were used for Managed<Account>
. The serialization functions obviously have Coherence awareness, and thus it may not be desirable to declare them inside the same source file as that of the Account
class. The sole requirement is that they are defined within some .cpp
file and ultimately linked into your application. Interestingly, they do not need to appear in any header file, which means that they could be put into something like a standalone AccountSerializer.cpp
file, without the need to modify the Account.hpp/cpp
that is used in application code.
We must also register our serializable Managed<Account>
class with the Coherence C++ library. This is accomplished via a simple call to a macro.
COH_REGISTER_MANAGED_CLASS(POF_TYPE_ACCOUNT, Account);
The registration statement specifies the POF type ID to class mapping. Unlike the Java and .NET versions, in C++, this mapping is performed at compilation time. The registration statement relies on the declaration of the serialization functions, and is therefore typically part of the same source file. Note the registration macro does not need to be called as part of the application logic—it is triggered automatically as part of static initialization.
The only thing missing now is the definition of POF_TYPE_ACCOUNT
, which could just be a #define
statement to the numeric POF type ID for the Account
class. While you may just choose to embed the ID number directly in the registration line, it is recommended that #define
, or some other external constant be used instead. Defining an external constant allows for the creation of a PofConfig.hpp
file for the application that includes all the POF type IDs. This results in a single place to perform the ID assignment, so you do not have to search through the various data-object serialization files to adjust any of the IDs. Here is an example of this PofConfig.hpp
file:
#ifndef POF_CONFIG_HPP #define POF_CONFIG_HPP #define POF_TYPE_ACCOUNT 1000 #define POF_TYPE_MONEY 1003 #define POF_TYPE_TRANSACTION 1004 #define POF_TYPE_TRANSACTION_ID 1005 #define POF_TYPE_TRANSACTION_TYPE 1006 #define POF_TYPE_DEPOSIT_PROCESSOR 1051 #define POF_TYPE_WITHDRAW_PROCESSOR 1050 #define POF_TYPE_CURRENCY 2000 #endif // POF_CONFIG_HPP
This header file is analogous to the pof-config.xml
file we would have used in Java or .NET. With these last pieces in place, our example will now work with both local and remote caches, automatically being serialized as needed.
It is, of course, possible to directly implement a managed class as well, in which case we can also implement the PortableObject
interface or, make use of PofSerializer
. While it may be unlikely that you would choose to implement your cached data types directly as managed classes, it is the normal pattern for custom implementations of entry processors, map listeners, and other Coherence-related classes.
To demonstrate the process, let's rewrite our Account
sample class as a managed class. In doing so, we will also choose to make use of the Coherence included types for its data members.
class Account : public cloneable_spec<Account> { friend class factory<Account>; // ----- data members -------------------------------------------- private: const int64_t m_lId; MemberView<String> m_sDescription; MemberHandle<Money> m_balance; int64_t m_lLastTransactionId; const int64_t m_lCustomerId; // ----- constructors -------------------------------------------- protected: Account(int64_t lId, String::View sDesc, Money::View balance, int64_t lLastTransId, int64_t lCustomerId) : m_lId(lId), m_sDescription(self(), sDesc), m_balance(self(), balance), m_lLastTransactionId(lLastTransId), m_lCustomerId(lCustomerId) { } Account() : m_lId(0), m_sDescription(self(), sDesc), m_balance(self(), balance), m_lLastTransactionId(0), m_lCustomerId(0) { } Account(const Account& that) : m_lId(lId), m_sDescription(self(), sDesc), m_balance(self(), cast<Money::View>(balance->clone()), m_lLastTransactionId(lLastTransId), m_lCustomerId(lCustomerId) { } // ----- accessors ----------------------------------------------- public: virtual int64_t getId() const { return m_lId; } virtual String::View getDescription() const { return m_sDescription; } virtual void setDescription(String::View sDesc) { m_sDescription = sDesc; } virtual Money::View getBalance() const { return m_balance; } virtual int64_t getLastTransactionId() const { return m_lLastTransactionId; } virtual int64_t getCustomerId() const { return m_lCustomerId; } // ----- Object methods ------------------------------------------ virtual bool equals(Object::View vThat) const { if (!instanceof<Account::View>(vThat)) { return false; } Account::View that = cast<Account::View>(vThat); return this == that || ( getId() == that->getId() && getLastTransactionId() == that->getLastTransactionId() && getCustomerId() == that->getCustomerId() && getDescription()->equals(that->getDescription()) && getBalance()->equals(that->getBalance)); } //optional ostream output function virtual void toStream(std::ostream& out) const { out << "Account(" << "Id=" << getId() << ", Description=" << getDescription() << ", Balance=" << getBalance() << ", LastTransactionId=" << getLastTransactionId() << ", CustomerId=" << getCustomerId() << ')'; } virtual size32_t hashCode() const { return (size32_t) getId(); } };
Overall, the code isn't much different from the original. Let's go through the differences one by one.
Perhaps, the strangest looking bit is the inheritance statement:
public cloneable_spec<Account>
This is part of the object model, and is called a "spec"-based class definition, where spec is short for specification. Specs do a fair amount of boiler-plate code injection to make the authoring of new managed classes easier than it would otherwise be. For instance, the following items (and more) are injected:
Implied virtual inheritance from Object
, making it managed
Defined Account::Handle/View/Holder
nested smart pointer typedefs
Defined Account::super
typedef to parent class
Added static create
methods to match Account's
constructors, returning Account::Handle
Added clone()
method implementation that delegates to Account's
copy constructor
There is an entire family of specs. In the previous example, we used cloneable_spec
because the item is going to be stored within a cache and needs to be cloneable. The other types of specs are:
class_spec—the
most basic of specs, which just defines a managed class
cloneable_spec—a class_spec
that supports cloning
abstract_spec—
defines a non-instantiable class with a partial implementation
interface_spec—
defines a non-instantiable class with all pure virtual methods
throwable_spec—
defines a spec-based exception class
All specs will automatically add inheritance from Object. Each will also add in some specific features of its own. Specs take the following template parameters:
spec<class, extends<parent>, implements<interface, ...> >
The arguments for the spec are:
class: Required, specifies the name of the class being defined
extends<parent>:
Optional, specifies a class to derive from, defaults to extends <Object>
, not included for interface_spec
implements<interface1
, interface2, ...>: Optional, the list of interfaces that this class implements, defaults to implements<>
friend class factory<Account>;
This friend declaration allows the auto-generated create
methods to access the protected constructors. It is the only bit of boiler-plate that specs cannot inject themselves.
Next, we can look at the data member declarations:
private: const int64_t m_lId; MemberView<String> m_sDescription; MemberHandle<Money> m_balance; int64_t m_lLastTransactionId; const int64_t m_lCustomerId;
First we switch our long long
data members to use int64_t
. On most modern C++ compilers, long long
is a 64-bit integer type, but it is not required to be of a specific size. The int64_t
is a fixed-sized type, which is guaranteed to be 64 bits wide. It is part of a family of fixed-size types that exist on many systems—for those on which it does not, Coherence adds the definitions. By convention, managed types use fixed-size primitives, though this is not a requirement.
Next, we switch from std::string
to a Coherence managed String
, referenced by a View
. The Money
class is similarly overhauled, allowing us to reference it via Handle
. You are not required to change these types—it is done here to improve serialization efficiency by avoiding the need to perform type conversion during serialization later on.
You'll also notice that, for String
and Money
, we used MemberView<String>
and MemberHandle<Money>
rather than nested String::View
, and Money::Handle
. This is done because the nested Handle/View/Holder
smart pointer types are not thread-safe. As objects stored in a cache could be accessed by multiple threads, it is important that they internally be thread-safe. MemberHandle/View/Holder
are thread-safe variants and should be used as data member references.
This doesn't mean you should avoid using the nested smart pointer types. In fact, they are what you will use most often. They are used as local variables and function/method parameters, basically anything that is stack allocated. Note that using the nested types for data members would have compiled and appeared to function just fine, but there would have been a memory leak/corruption looming. It is, therefore, highly recommended that you use the thread-safe variants for data members of all managed classes.
There are two additional thread-safe smart pointer variants included with Coherence C++:
FinalHandle/View/Holder—an
immutable smart-pointer data member
WeakHandle/View/Holder—a
weak reference-style smart pointer3
The Final
variants are similar in functionality to const MemberHandle/View/Holder
, but include object model performance benefits based on the awareness that it is immutable. The Weak
variants are used in conditions where your object graph includes cycles. As the object model makes use of reference counting, a cyclical graph would result in a memory leak. These "weak" smart pointers avoid the leak by automatically being NULL'ed
out once they are the sole reference to an object, thus allowing the object to be collected. The use of these variants is otherwise identical to the Member
variants.
Moving right along, we get to the constructors:
protected: Account(int64_t lId, String::View sDesc, Money::View balance, int64_t lLastTransId, int64_t lCustomerId) : m_lId(lId), m_sDescription(self(), sDesc), m_balance(self(), balance), m_lLastTransactionId(lLastTransId), m_lCustomerId(lCustomerId) { } Account() : m_lId(0), m_sDescription(self(), sDesc), m_balance(self(), balance), m_lLastTransactionId(0), m_lCustomerId(0) { } Account(const Account& that) : m_lId(lId), m_sDescription(self(), sDesc), m_balance(self(), cast<Money::View>(balance->clone()), m_lLastTransactionId(lLastTransId), m_lCustomerId(lCustomerId) { }
First we notice that the constructors are now declared as protected, which blocks both stack-based as well as operator new-based
allocations, leaving the static create
method as the only allocation mechanism.
Next we see that MemberView
and MemberHandle
are initialized with self()
. The thread-safe smart pointers take an optional second parameter, which is the object they are to reference, and if left out, defaults to NULL
. The self()
used in initialization is an easy pattern to follow, though perhaps a bit difficult to understand.
Each managed object contains an embedded micro read/write lock, which is used to provide the thread-safety to its smart pointer data members. The smart pointers thus require a reference to their enclosing object, and this is exactly what self()
returns. Specifically, the self()
method returns a reference to the base class of the managed object being created. It is conceptually similar to the this
pointer, except that it references the fully initialized base class, while this
refers to the partially initialized derived type.
Note that the thread-safe smart pointers can be used outside of managed classes as well. In this case, you will need to provide them a surrogate object that will protect them, as there is no self
. This surrogate can either be an object you allocate yourself, or you can use one from a pool obtained by calling System::common()
.
The next change is that the methods are now all declared as virtual. This is certainly not required, but in general, managed classes are designed to operate like Java where all methods are virtual.
Finally, we override some standard methods declared by Object
. These allow for hashing, equality testing, and printing of the object.
This was a bit of a detour, but finally, we can get back to making our class implement PortableObject
. To do this, we'll simply modify the inheritance statement to indicate that this class implements PortableObject
, and then implement its methods and register the type:
class Account : public cloneable_spec<Account, extends<Object>, implements<PortableObject> > { ... // ----- PortableObject methods -------------------------------- public: virtual void writeExternal(PofWriter::Handle hWriter) const { hWriter->writeInt64(0, getId()); hWriter->writeString(1, getDescription()); hWriter->writeObject(2, getBalance()); hWriter->writeInt64(3, getLastTransactionId()); hWriter->writeInt64(4, getCustomerId()); } virtual void readExternal(PofReader::Handle hReader) { m_lId = hReader->readInt64(0, getId()); setDescription(hReader->readString(1)); setBalance(cast<Money::Handle>(hReader->readObject(2))); setLastTransactionId(hReader->readInt64(3)); m_lCustomerId = hReader->readInt64(4); } }; COH_REGISTER_PORTABLE_CLASS( POF_TYPE_ACCOUNT, Account); // must be in .cpp
So after a lot of explanation, the act of making the class POF serializable is quite trivial. You will notice that in readExternal
, we need to set two const
data members, which is not allowable. This issue exists because PortableObject
deserialization occurs after the object has already been instantiated. To achieve const-correctness
, we would unfortunately need to either remove the const
modifier from the declaration of these data members, or cast it away within readExternal
.
The final serialization option available to us is to write an external serializer for our managed data object. Here we'll create one for the non-PortableObject version of the managed Account
class. Note that the serializer-based solution does not exhibit the const
data member issues we encountered with PortableObject
.
class AccountSerializer : public class_spec<AccountSerializer, extends<Object>, implements<PofSerializer> > { friend class factory<AccountSerializer>; public: virtual void serialize(PofWriter::Handle hWriter, Object::View v) const { Account::View vAccount = cast<Account::View>(v); hWriter->writeInt64(0, vAccount->getId()); hWriter->writeString(1, vAccount->getDescription()); hWriter->writeObject(2, vAccount->getBalance()); hWriter->writeInt64(3, vAccount->getLastTransactionId()); hWriter->writeInt64(4, vAccount->getCustomerId()); hWriter->writeRemainder(NULL); // mark end of object } virtual Object::Holder deserialize(PofReader::Handle hReader) const { int64_t lId = hReader->readInt64(0); String::View sDesc = hReader->readString(1); Money::Handle hBalance = cast<Money::Handle>( hReader->readObject(2)); int64_t lTransId = hReader->readInt64(3); int64_t lCustomerId = hReader->readInt64(4); hReader->readRemainder(); // read to end of object return Account::create(lId, sDesc, hBalance, lTransId, lCustomerId); } }; COH_REGISTER_POF_SERIALIZER(POF_TYPE_ACCOUNT, TypedClass<Account>::create(), AccountSerializer::create()); // must be in .cpp
All in all, this is pretty much the same as we'd done in Java, the prime difference being the COH_REGISTER
statement that registers AccountSerializer
and Account
class with the Coherence library. The usage of the new managed Account
class is somewhat more direct than with Managed<Account>:
// construct managed key and value Account::Handle hAccount = Account::create(32105, "checking", Money::create(7374, 10, "USD"), 55, 62409); Integer64::Handle hlKey = Integer64::create(32105); // cache hAccount hCache->put(hlKey, hAccount); // retrieve the cached value Account::View vResult = cast<Account::View>(hCache->get(hlKey)); std::cout << "retrieved " << vResult << " from cache for key " << vResult->getId() << std::endl;
In later sections, we'll make use of specs to write other types of custom classes such as filters and aggregators.
Coherence C++ offers a QueryMap
interface that closely resembles its Java counterpart:
class QueryMap : public interface_spec<QueryMap, implements<Map> > { public: virtual Set::View keySet(Filter::View vFilter) const; virtual Set::View entrySet(Filter::View vFilter) const; virtual Set::View entrySet(Filter::View vFilter, Comparator::View vComparator) const; virtual void addIndex(ValueExtractor::View vExtractor, bool fOrdered, Comparator::View vComparator); virtual void removeIndex(ValueExtractor::View vExtractor); };
Executing a query is a simple matter of constructing the filter to identify the record matching criteria, and then supplying it to either the keySet
or entrySet
methods. The Comparator
variants can be used to order the result set if necessary.
Unless operating on a local cache, the supplied filters, extractors, and comparators are only used as serializable stubs, as all processing will be done remotely in Java on the cache servers.
Central to QueryMap
is the concept of the ValueExtractor
, which is used to extract an embedded value from a cached entry. For instance, you can use a value extractor to extract the balance from our Account
data object.
These extractors are used both in expressing the filter criteria and in applying indexes to the cache in order to optimize query performance. Included with Coherence C++ is a ReflectionExtractor
implementation, which is suitable for queries against remote caches, so long as the cache server contains a Java version of the data object:
hCache->addIndex(ReflectionExtractor::create("getBalance"), false, NULL);
As the C++ language does not have reflection support, it should come as no surprise that the ReflectionExtractor
throws an UnsupportedOperationExpection
if it is used against a C++ local cache. If you intended to perform queries against local caches, it would appear that you would be left with writing your own custom C++ extractors for each of your data objects to obtain embedded values. While this is not terribly difficult, it is also thankfully unnecessary. Coherence C++ includes a TypedExtractor
that utilizes a combination of macros, templates, and function pointers to do a fairly decent job of emulating the Java ReflectionExtractor
.
ValueExtractor::View vExtractor = COH_TYPED_EXTRACTOR(Money::View, Account, getBalance);
The macro parameters are the type of the extracted value, the class type to perform the extraction on, and finally, the const
accessor method used to obtain the value. In the case of accessor methods that return non-managed types, there is a BoxExtractor
variant, which will wrap the primitive type back into its corresponding managed type, which is required to implement the ValueExtractor
interface.
ValueExtractor::View vExtractor = COH_BOX_EXTRACTOR(Integer64::View, Account, getId);
Note that if you are working with the Managed<>
template helper, you need to use a special COH_BOX_MANAGED_EXTRACTOR
version instead.
To make things even nicer, these extractors actually extend ReflectionExtractor
, and thus can be used against both local and remote caches, so long as the cache server has the corresponding Java version of the class and uses the same method names.
The final type of built-in extractor included with Coherence C++ is the PofExtractor
. As described earlier, PofExtractor
allows for efficient extraction on the server side without the need for full de-serialization, or the corresponding Java classes.
ValueExtractor::View vExtractor = PofExtractor::create(typeid(Money), Account::BALANCE);
When using PofExtractor
, it is best practice to create static identifiers for the property indexes. The previous example assumes that we've defined one for the BALANCE
field.
In Coherence 3.5, C++ PofExtractor
can only be applied to remote caches. This is for two reasons—for one, local caches hold onto data in the object rather the serialized form, and secondly (largely because of the first reason), the C++ PofExtractor
is currently implemented as a stub, because its primary use is to be evaluated on cache servers within the cluster.
In Java and .NET, we've introduced PropertyExtractor
, and you might find it useful to have the same in C++. This can be accomplished easily enough with a new macro helper around TypedExtractor.
#define COH_PROPERTY_EXTRACTOR(TYPE, CLASS, PROPERTY) coherence::util::extractor::TypedExtractor< TYPE, CLASS, &CLASS::get##PROPERTY> ::create(COH_TO_STRING("get" << #PROPERTY));
This allows usage like the following:
ValueExtractor::View vExtractor = COH_PROPERTY_EXTRACTOR(Money::View, Account, Balance);
We will need an additional version if we wish to handle Managed<>
data objects, such as our original Managed<Account>:
#define COH_MANAGED_PROPERTY_EXTRACTOR(TYPE, CLASS, PROPERTY) coherence::util::extractor::BoxExtractor< TYPE, CLASS, &CLASS::get##PROPERTY, coherence::lang::Managed<CLASS>::Holder> ::create(COH_TO_STRING("get" << #PROPERTY));
The usage remains quite similar:
ValueExtractor::View vExtractor = COH_MANAGED_PROPERTY_EXTRACTOR(Money::View, Account, Balance);
Ok, these aren't quite the same as our other PropertyExtractor
implementations, but they are close enough to be just as useful. The key differences are that property names are capitalized, it assumes a get
accessor, and that on the Java side it will deserialize and execute as plain old ReflectionExtractor
rather than a PropertyExtractor
.
It is certainly possible to create a full fledged C++ PropertyExtractor
implementation; it would closely follow the pattern laid out in TypedExtractor
whose source is entirely within the TypedExtractor.hpp
header.
Coherence C++ ships with the same built-in filter set as that provided for Java. These filters are capable of executing against both local and remote caches. When chaining together the built-in filters is not sufficient to express your query, you are free to also write custom filters. Unless the filters will only be targeted at a local cache, you will also need to produce a Java version, as that is what will actually do the filtering when running against a remote cache.
While the logic within Java and C++ implementations doesn't have to be identical, it is important that the implementations are compatible, which implies:
For a given set of inputs, both implementations should produce the same result
The serialized form of both implementations should be equivalent
Now we can put all these things together and finally perform a query from within C++:
// add an index for the description property on the Account class ValueExtractor::View vExtractor = COH_PROPERTY_EXTRACTOR(String::View, Account, Description); hCache->addIndex(vExtractor, false, NULL); // query for all "checking" Accounts Set::View vSetResult = hCache->entrySet(LikeFilter::create(vExtractor, "checking%")); // iterate the result set printing each matching entry for (Iterator::Handle hIter = vSetResult->iterator(); hIter->hasNext(); ) { Map::Entry::View vEntry = cast<Map::Entry::View>(hIter->next()); Account::View vAccount = cast<Account::View>(vEntry->getValue()); std::cout << vAccount << std::endl; }
As you can see, performing the query and iterating the results is quite trivial. All the real work is in defining extractors and filters. Knowing how to implement custom extractors and filters is important, but keep in mind that you can get quite far with the built-in ones.
Coherence C++ includes full support for aggregators and entry processors via a native InvocableMap
interface, which closely resembles the Java version described in Chapters 5 and 6.
class InvocableMap : public interface_spec<InvocableMap, implements<Map> > { public: virtual Object::Holder invoke(Object::View vKey, EntryProcessor::Handle hAgent); virtual Map::View invokeAll(Collection::View vCollKeys, EntryProcessor::Handle hAgent); virtual Map::View invokeAll(Filter::View vFilter, EntryProcessor::Handle hAgent); virtual Object::Holder aggregate(Collection::View vCollKeys, EntryAggregator::Handle hAgent) const; virtual Object::Holder aggregate(Filter::View vFilter, EntryAggregator::Handle hAgent) const; };
As you can see, the interface allows for explicit key-and-filter based selection of the entries to be processed or aggregated. The operation to perform is expressed as either an EntryProcessor
or Aggregator
, for which there are a number of built-in implementations. You may also supply your own custom implementations.
The InvocableMap
interface is supported by both local and remote caches. By far, the more common case is to use them on remote (clustered) caches. If your custom EntryProcessors
and Aggregators
will be used against remote caches, you will need to have the corresponding Java version in the classpath of your cache servers. Just as with .NET, if you only intend to use them remotely, your C++ implementations need to only contain state and serialization logic, and can skip the actual processing logic.
The following is a entry processor which can be used to deposit funds into our remote Account
cache:
class DepositProcessor : public class_spec<DepositProcessor, extends<AbstractProcessor>, implements<PortableObject> > { friend class factory<DepositProcessor>; // ----- constructors -------------------------------------------- protected: DepositProcessor() : m_vMoney(self()), m_vsDescription(self()) {} DepositProcessor(Managed<Money>::View vMoney, String::View vsDescription) : m_vMoney(self(), vMoney), m_vsDescription(self(), vsDescription) {} // ----- InvocableMap::EntryProcessor interface ------------------ public: virtual Object::Holder process( InvocableMap::Entry::Handle hEntry) const { COH_THROW (UnsupportedOperationException::create()); } // ----- PortableObject interface -------------------------------- public: virtual void readExternal(PofReader::Handle hIn) { initialize(m_vMoney, cast<Managed<Money>::View>( hIn->readObject(0))); initialize(m_vsDescription, hIn->readString(1)); } virtual void writeExternal(PofWriter::Handle hOut) const { hOut->writeObject(0, m_vMoney); hOut->writeString(1, m_vsDescription); } // ----- data members -------------------------------------------- protected: FinalView<Managed<Money> > m_vMoney; FinalView<String> m_vsDescription; }; COH_REGISTER_PORTABLE_CLASS(POF_TYPE_DEPOSIT_PROCESSOR, DepositProcessor);
As you can see, this stub implementation contains no processing logic. Its only purpose is to convey the operation type and state when serialized and transmitted to the Java cache servers, where the deserialized Java version will handle the processing work.
Aggregators will follow the same pattern of just being client-side state conveying stubs, and an example is omitted here due to their close similarity with the stub entry processor we've just presented.
Just as with Java, C++ clients may register event listeners to be notified when cache entries are inserted, updated, or deleted. These event feeds are extremely useful in building real-time non-polling applications based on cached state.
Coherence C++ follows the Java-style observer pattern, and event registration is performed against methods declared as part of the ObservableMap
interface.
class ObservableMap : public interface_spec<ObservableMap, implements<Map> > { public: virtual void addKeyListener(MapListener::Handle hListener, Object::View vKey, bool fLite); virtual void removeKeyListener(MapListener::Handle hListener, Object::View vKey); virtual void addFilterListener(MapListener::Handle hListener, Filter::View vFilter = NULL, bool fLite = false); virtual void removeFilterListener( MapListener::Handle hListener, Filter::View vFilter = NULL); };
As you can see, the feature set is the same as that available in the Java version of ObservableMap
, including key-and-filter based registrations, and the ability to request lite events. Lite events are free to omit the old and new values in order to avoid the additional resources required to carry them over the network.
When matching events occur, you are notified by a callback on the supplied custom MapListener
implementation. The MapListener
interface is again similar to the Java version, and the usage is the same as described in Chapter 7,
class MapListener : public interface_spec<MapListener, implements<EventListener> > { public: virtual void entryInserted(MapEvent::View vEvent); virtual void entryUpdated(MapEvent::View vEvent); virtual void entryDeleted(MapEvent::View vEvent); };
Let's put together a simple custom listener that prints cache changes to standard output:
class VerboseMapListener : public class_spec<VerboseMapListener, extends<Object>, implements<MapListener> > { friend class factory<VerboseMapListener>; public: virtual void entryInserted(MapEvent::View vEvent) { std::cout << "inserted " << vEvent->getKey() << ", " << vEvent->getNewValue() << std::endl; } virtual void entryUpdated(MapEvent::View vEvent) { std::cout << "updated " << vEvent->getKey() << " from " << vEvent->getOldValue() << " to " << vEvent->getNewValue() << std::endl; } virtual void entryDeleted(MapEvent::View vEvent) { std::cout << "deleted " << vEvent->getKey() << std::endl; } };
Event listener registration is performed just as in Java:
hCache->addFilterListener(VerboseMapListener::create());
Event notifications occur locally on a dedicated event-dispatching thread associated with the cache. It is thus important to consider pushing any long running listener logic onto application threads so that subsequent events are not blocked or delayed.
The final Coherence C++ feature we will look at is integration with standard C++ data types. We've already seen that the Coherence-managed String
can interoperate with char*
and std::string
, but there are a number of other type integrations worth considering.
Managed type |
Non-managed type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For each of these type integrations, the managed type will support some form of assignment from and to the standard type. In most cases, the managed type will be constructable from the standard type, and can be de-referenced and assigned to the standard type:
Integer32::View vInt = Integer32::create(5); int32_t nInt = *vInt; // assigns 5 to nInt
Another integration point is with std::map
, or more specifically, the STL pair associative container concept. It is probably quite clear by this point that the Coherence caches do not operate based on std::map
, but rather based on coherence::util::Map
interface, which mimics java.util.Map
. For those who prefer the feel of std::map
, or those replacing an existing std::map-based
local cache, Coherence includes an adapter to make any Coherence map or cache implementation usable through a std::map-style
API.
The adapter class boxing_map
is an implementation of the std::map
(pair associative container) concept, which delegates to any coherence::util::Map
implementation. This includes doing the work of converting the keys and values back to their non-managed C++ types, making for an even stronger traditional C++ feel.
As an example, let's create a boxing_map
around our Account
cache, and re-write our original cache access code:
boxing_map<Integer64, Managed<Account> > cache(CacheFactory::getCache("accounts")); // construct plain old Account object Account account(32105, "checking", Money(7374, 10, "USD"), 55, 62409); // cache account cache[32105] = account; // retrieve the cached value Account accountResult = cache[32105];
As you can see, other than the declaration, all the other statements are just as they would be for std::map
. The boxing_map
includes all the standard operators and methods you'd expect from std::map
. This allows for some interesting combinations. For instance, you can make use of STL algorithms to operate on the cache, for example, copying std::map
into a cache:
std::map<int64_t, Account> mapBatch; // fill up mapBatch with records // ... // use std::copy to transfer them to the cache std::copy(mapBatch.begin(), mapBatch.end(), std::inserter(cache, cache.begin()));
While the ability to access caches as std::maps
is useful, it is also limited. Most of the advanced features of Coherence caches are unavailable because the std::map
API does not have corresponding concepts. Ultimately, the boxing_map
is really intended for applications that would primarily access the cache in a get/put style, leaving more advanced cache usage to be accessible only through the Coherence cache interfaces.