Extending ArcObjects  

Implementing Cloning

This topic is relevant for the following:
Product(s): ArcGIS Desktop: All
Version(s): 9.0, 9.1, 9.2
Language(s): VB6, VC++
Experience level(s): Intermediate to advanced



In this section:

  1. Implementing Cloning
  2. Copying members: Values and object References
  3. Implementing IClone

       - Coding IClone in VB

       - Coding IClone in VC++


Implementing Cloning

Cloning is the process of copying an object—the creation of a new instance of a class, representing information equivalent to the original instance.

Creating a copy of a particular object is more complex than simply assigning a new variable. For example, the code below simply creates two variables that point to the same object in memory.

[Visual Basic 6]
Dim pPointOne As esriGeometry.IPoint
Dim pPointTwo as esriGeometry.IPoint
Set pPointOne = New esriGeometry.Point
Set pPointTwo = pPointOne

Copying an object is more complex than simply assigning a new variable.

To actually copy the Point object, creating a new instance of a Point with comparable data to the first Point, use the IClone interface.

[Visual Basic 6]
Dim pClone As esriSystem.IClone
Set pClone = pPointOne
Set pPointTwo = pClone.Clone

Cloning creates a new instance in memory.

The concepts reviewed in this section should be familiar to any VC++ developer; however, some VB developers may find they do not normally consider these kinds of issues.

Cloning in ArcGIS

This technique is used extensively throughout ArcGIS by ArcObjects classes that implement the IClone interface.

Cloning is a technique used extensively throughout ArcGIS.

For example, before the Application passes an object to a property page, it clones the object. Only if the OK or Apply button is pressed are the properties of the cloned object set into the original object. Another use of cloning in ArcObjects is by methods or properties, which specifically return a copy of an object, for example, the IFeature::ShapeCopy method.

You can also find other examples of how cloning is used by searching the samples included in the ArcGIS Developer Help.

Terminology

Throughout this section, the original object will be referred to as the Cloner—this object performs the cloning operation. The object resulting from the cloning process will be called the Clonee.


Copying members: Values and object References

The exact details of the clone operation are encapsulated in the class implementation—the class regulates which of its members should be copied and also how they should be copied.

Each class that implements cloning decides how to clone itself.

Shallow and deep cloning

For a simple object whose members contain only value type information, the cloning process is relatively simple. A new instance of the class is created, and the values of all the members of the clonee are set to equal the values of the cloner. The clonee object is independent of the cloner.

For an object whose members contain object references, the cloning process becomes more complex. Should the cloner copy only the object references to the clonee? This is sometimes known as a shallow clone. Or should new instances of each of the referenced objects be created also and the clonee's members be set to reference these new objects? This is referred to as a deep clone.

There are different levels of cloning, shallow and deep.

Both shallow and deep cloning are used by ArcObjects classes. An example of a deep clone, where referenced objects are also cloned, is when a graphic element is cloned. Both the Geometry and Symbol of the graphic element are also cloned—the Geometry and Symbol properties of the clonee are entirely separate from the original objects' Geometry and Symbol.

In other cases it is logical to simply copy an object reference to the new object. Shallow cloning is used, for example, on the geometries in a FeatureClass. Every geometry has an object reference indicating its coordinate system (IGeometry::SpatialReference). Cloning the Geometry produces an object with a reference to the same underlying SpatialReference object. In this case, only the object reference is copied, as it is logical for all geometries in a FeatureClass to hold a reference to the same spatial reference object, as does the layer itself. The SpatialReference can then be changed by a single method call.

There is no simple rule for deciding whether an object reference should be copied or the referenced object itself should be cloned. This is decided on a case-by-case basis, and both techniques may be included in a single class.

For each of its private members, a class needs to make the appropriate choice between shallow or deep cloning.

You must be particularly careful when cloning objects that hold object references. In many cases, a referenced object may hold references to yet more objects, which in turn hold references to other objects, and so on.

Transient members

When coding a clone method, bear in mind that some members should not be directly copied at all—window handles (hWnd), device contexts (hDC), file handles, and Graphical Device Interface (GDI) resources, for example, contain instance-specific information, which should not be duplicated directly to another object.

In most cases, it is inappropriate to clone an object that contains this type of instance-specific information. For example, a Workspace and FeatureClass both have connection-specific information and are not clonable; an OverviewWindow and a ToolControl both have a window handle and are not clonable. If a new instance is required, the object is created from scratch.

Sometimes it is more appropriate for a class not to replicate a member in its clone at all.

If you need to implement IClone on such an object, ensure that any instance-specific information is populated from scratch, instead of simply copying the instance-specific values.


Implementing IClone

If you implement cloning in your custom components, you will need to make some decisions about how you copy the information contained in your class—whether shallow or deep cloning is most appropriate for each member and how to implement this.

The sections below show you how to implement each of the IClone members in your custom class.

Two different approaches are discussed. The first approach is straightforward and can be implemented using similar logic in either VB or VC++. The second approach can only be used in VC++, as it uses a class's own persistence implementation to perform the clone.

Coding IClone in VB

In the Clone method, begin by creating a new instance of the class, which is the clonee. You can then call the IClone::Assign, to copy the properties of the cloner to the clonee. Lastly, return a reference to the clonee from Clone.

[Visual Basic 6]
Private Function IClone_Clone() As esriSystem.IClone
  Dim pNewObject As MyLibrary.MyClass, pClone As esriSystem.IClone
  Set pNewObject = New MyLibrary.MyClass
  Set pClone = pNewObject   ' pNewObject represents the Clonee.
  pClone.Assign Me           ' Me represents the Cloner.
  Set IClone_Clone = pClone
End Function

Clone should create a new instance of the class.

The Assign method receives a reference to a second instance of the class, src—this is the clonee. First, check src to see if it is pointing to a valid object—if not, raise the appropriate standard COM error.

[Visual Basic 6]
Const E_POINTER = &H80004003

Assign should receive a valid instance of the class.

Then copy the values of the members from src (the clonee) to the current instance of the class, Me (the cloner).

[Visual Basic 6]
Private Sub IClone_Assign(ByVal src As esriSystem.IClone)
  If (src Is Nothing) Then
    Err.Raise Const E_POINTER
    Exit Sub
  ElseIf TypeOf src Is MyLibrary.IMyInterface Then
    Dim pSrcMyInterface As MyLibrary.IMyInterface
    Set pSrcMyInterface = src
    ' m_MyMember is a class member storing value of MyMember property.
    m_MyMember = pSrcMyInterface.MyMember
  End If
End Sub

The cloner copies values from the clonee.

The Assign code above shows a shallow clone of the MyMember property. If MyMember is another object reference, you may want to perform a deep clone—if the object itself supports IClone, this is straightforward.

[Visual Basic 6]
Dim pCloned As esriSystem.IClone
Set pCloned = pSrcInterface.MyMember
Set m_MyMember = pCloned.Clone

If the member object does not support IClone, you must create a new object and set its properties from the existing MyMember property of the source object, scr.

Remember to think about whether it is more appropriate to copy just an object reference (for example, all the geometries of a FeatureClass hold a reference to the same SpatialReference), clone the object reference, or leave the member uncopied to be set by the client code as appropriate.

When coding the Assign method, you should consider the choice of shallow or deep cloning. Consider that some member variables may not be suitable for cloning.

As an example, consider how a RandomColorRamp performs an Assign. The cloner RandomColorRamp will have the same MinSaturation, MaxSaturation, MinValue, MaxValue, StartHue, EndHue, UseSeed, Seed, and Name as the clonee. However, the Assign method does not copy the value of Size or call the CreateRamp method; this means the color ramp has no array of Colors and cannot be used in a renderer at that point. After a call to Assign, the client must set up the Colors array of the RandomColorRamp by setting its Size property and calling its CreateRamp method.

Another consideration when coding your Assign method should be the current state of both the cloner and clonee objects. You may decide to clear any stateful information held by the cloner before assigning the properties from the clonee. In this case, you may want to add an internal initialization function to set the values of the class to a known initial state. This function could then also be called from your class initialization function.

You may want to clear or reinitialize any member variables before performing an Assign to ensure the result is a faithful clone.

The IsEqual method should compare the cloner (Me) and the clonee (other) to see if all the members are equal in value—return True if all the members are equal.

[Visual Basic 6]
Private Function IClone_IsEqual(ByVal other As esriSystem.IClone) As Boolean
  IClone_IsEqual = True
  If (src Is Nothing) Then
    Err.Raise Const E_POINTER
    Exit Sub
  ElseIf TypeOf other Is MyLibrary.IMyInterface Then
    Dim pSrcMyInterface As IMyInterface, pOtherMyInterface As IMyInterface
    Set pSrcMyInterface = other
    Set pOtherMyInterface = Me
    IClone_IsEqual = IClone_IsEqual And _
     (pOtherMyInterface.MyMember = pSrcMyInterface.MyMember)
    ...
  End If
End Function

If a property holds an object reference that supports IClone, use IClone::IsEqual on the member object to evaluate if it is equal to the member object of the passed-in reference, other. Don't forget to check all the members of all the interfaces that are supported by the object.

IsEqual should determine if two different objects have values that can be considered equivalent.

You decide what your class considers to be Equal values—you may decide that two IColor members are equal if they have the same RGB value, even though one is an RGBColor and one is a CMYKColor.

To implement IsIdentical, you should compare the interface pointers to see if the cloner (Me) and the clonee (other) point to the same underlying object in memory. In VB, you can compare two object references (or interface pointers) using the Is keyword.

[Visual Basic 6]
Private Function IClone_IsIdentical(ByVal other As esriSystem.IClone)_
  As Boolean
  IClone_IsIdentical = False
  If (src Is Nothing) Then
    Err.Raise Const E_POINTER
    Exit Sub
  ElseIf TypeOf other Is MyLibrary.IMyInterface Then
    Dim pOtherMyInterface As MyLibrary.IMyInterface
    Set pOtherMyInterface = other
    IClone_IsIdentical = (pOtherMyInterface Is Me)
  End If
End Function

IsIdentical should compare interface pointers to see if they reference the same underlying object.

Coding IClone in VC++

IClone can be implemented in VC++ with a similar approach to that just described for VB. The Clone method is shown below.

[Visual C++]
STDMETHODIMP CMyClass::Clone(IClone **Clone)
{
  if (!Clone) return E_POINTER;
  *Clone = 0;
  HRESULT hr;
  
  CComObject<CMyClass>* pItem = 0;
  hr = CComObject<CMyClass>::CreateInstance(&pItem);
  if (FAILED(hr)) return hr;
  
  pItem->AddRef();
  IClonePtr ipClonee = pItem;
  if (ipClonee == NULL) return E_FAIL;
  pItem->Release();
  
  IClonePtr ipCloner = this;
  if (ipCloner == NULL) return E_FAIL;
  
  hr = ipClonee->Assign(ipCloner);
  if (FAILED(hr)) return hr;
  *Clone = ipClonee.Detach();
  return S_OK;
}

Note the use of the CComObject static member function CreateInstance, rather than the normal object creation via COM—more information on private initialization can be found on page 148 of ATL Internals (see bibliography for more details).

Implement the Assign, IsEqual, and IsIdentical methods by using the same principles as shown previously for VB.

Coding IClone methods in VC++ using an ObjectStream

If your VC++ class implements IPersist and IPersistStream, you can take advantage of the persistence functionality when writing your clone methods. By temporarily saving your object to an ObjectStream, you can duplicate the object by creating a new instance of your class and loading its properties from the temporary ObjectStream.

This technique is used internally, for example, when you cut and paste graphic elements in ArcMap. This technique may result in a more efficient Clone method when working with a complex class with many object references.

This technique is not directly available to VB developers as the signature of IObjectStream::LoadObject is not usable in VB.

The code below shows how you could create an implementation of the Clone method using an ObjectStream. The enumerations used in this code (and IsEqual which follows) are part of the COM platform SDK.

Begin by checking the incoming pointer.

[Visual C++]
if (!Clone) return E_POINTER;
*Clone = 0;
HRESULT hr;

Next, create a memory stream using the COM platform SDK function, CreateStreamOnHGlobal. Also, create an ObjectStream, then aggregate the simple stream into the ObjectStream.

[Visual C++]
IStreamPtr ipStreamPtr;
::CreateStreamOnHGlobal(NULL, TRUE, &ipStreamPtr);
if (ipStreamPtr == NULL) return E_FAIL;
 
IObjectStreamPtr ipObjectStream;
hr = ipObjectStream.CreateInstance(CLSID_ObjectStream);
if (FAILED(hr)) return hr;
 
hr = ipObjectStream->putref_Stream(ipStreamPtr);
if (FAILED(hr)) return hr;

After checking for the presence of IPersistStream, persist the object to the memory stream.

[Visual C++]
IUnknownPtr ipUnknown = GetUnknown();
IPersistStreamPtr ipPersistStream = ipUnknown;
if (ipPersistStream == NULL) return E_FAIL;
hr = ipObjectStream->SaveObject(ipUnknown);
if (FAILED(hr)) return hr;

When you have finished persisting to the stream, reset it to the beginning.

[Visual C++]
ULARGE_INTEGER newPosition;
LARGE_INTEGER moveTo;
moveTo.QuadPart = 0;
ipObjectStream->Seek(moveTo, STREAM_SEEK_SET, &newPosition);

Then clone the object from the stream in memory.

[Visual C++]
IUnknownPtr ipCloneeUnk;
hr = ipObjectStream->LoadObject((GUID*)&IID_IUnknown, NULL, &ipCloneeUnk);
if (FAILED(hr)) return hr;
 
IClonePtr ipClonee = ipCloneeUnk;
if (ipClonee == NULL) return E_FAIL;
 
*Clone = ipClonee.Detach();
return S_OK;

An advantage of this approach to cloning is that the solution is generic. You can use the same code to implement cloning on many classes, as long as they already implement IPersistStream.

Using this technique, a deep clone will be performed, as each object reference will be called on to persist itself to the new stream. Check that this is a suitable operation for your class, particularly if your class holds references to objects referenced elsewhere in the application or MxDocument. For example, a custom GraphicElement implements IGraphicElement, which holds a reference to a SpatialReference object—this object is a property of the Map in which the GraphicElement resides. If the GraphicElement is cloned, the new object should also hold a reference to this same SpatialReference object, NOT a reference to a separate but equal SpatialReference.

If you do use this technique for cloning such classes, you should reset each of the members you prefer to be shallow cloned after the call to LoadObject.

The Assign method can be implemented similarly to Clone—save the supplied source object to a memory stream and use IObjectStream::ReplaceObject to assign to the object in question.

The IsEqual method can be implemented by saving each object to a separate stream, then performing a byte-by-byte comparison. One way to perform this operation is described step-by-step below.

The IsEqual method receives two parameters, a pointer to a second instance of a clonable class (pOther) and a boolean (pbEqual) to return the outcome of the IsEqual operation. Initialize pbEqual to false, and check that pOther points to a valid instance.

[Visual C++]
*pbEqual = VARIANT_FALSE;
if (!pOther)
  return S_OK;

Next, create a MemoryBlobStream and persist the current instance of the class to this stream.

[Visual C++]
IStreamPtr ipStreamPtr1;
::CreateStreamOnHGlobal(NULL, TRUE, &ipStreamPtr1);
if (ipStreamPtr1 == NULL) return E_FAIL;
 
IObjectStreamPtr ipObjectStream1;
hr = ipObjectStream.CreateInstance(CLSID_ObjectStream);
if (FAILED(hr)) return hr;
 
hr = ipObjectStream1->putref_Stream(ipStreamPtr1);
if (FAILED(hr)) return hr;
 
if (FAILED(::SaveObject((IUnknown*)((IClone*)this), ipObjectStream1, FALSE)))
  return S_OK;

Create a second memory stream and save pOther to this stream.

[Visual C++]
IStreamPtr ipStreamPtr2;
::CreateStreamOnHGlobal(NULL, TRUE, &ipStreamPtr2);
...
if (FAILED(::SaveObject(pOther, ipObjectStream2, FALSE)))
  return S_OK;

Reset both the streams to the beginning.

[Visual C++]
ULARGE_INTEGER newPosition;
LARGE_INTEGER moveTo;
moveTo.QuadPart = 0;
if (FAILED(ipObjectStream1->Seek(moveTo, STREAM_SEEK_SET, &newPosition)))
  return S_OK;
if (FAILED(ipObjectStream2->Seek(moveTo, STREAM_SEEK_SET, &newPosition)))
  return S_OK;

Now you can begin to compare the streams. First, compare their size by using the IStream::Stat method to get statistical information about each stream. If the size of the streams is not equal, exit the IsEqual method.

[Visual C++]
STATSTG ss1, ss2;
ipObjectStream1->Stat(&ss1, STATFLAG_NONAME);
ipObjectStream2->Stat(&ss2, STATFLAG_NONAME);
if (ss1.cbSize.QuadPart != ss2.cbSize.QuadPart)
  return S_OK;

Now you can compare the streams, first integer by integer. If the values are not equal, exit the IsEqual method.

[Visual C++]
long l1, l2;
long size = sizeof(l1);
long numInts = (long)ss1.cbSize.QuadPart / size;
DWORD bytesRead;
for (long i = 0; i < numInts; i++)
{
  if (FAILED(ipObjectStream1->Read(&l1, size, &bytesRead)))
    return S_OK;
  if (FAILED(ipObjectStream2->Read(&l2, size, &bytesRead)))
    return S_OK;
  if (l1 != l2)
    return S_OK;
}

Finish by comparing the streams byte by byte.

[Visual C++]
BYTE b1, b2;
long numBytes = (long)ss1.cbSize.QuadPart - (numInts * size);
for (i = 0; i < numBytes; i++)
{
  if (FAILED(ipObjectStream1->Read(&b1, 1, &bytesRead)))
    return S_OK;
  if (FAILED(ipObjectStream2->Read(&b2, 1, &bytesRead)))
    return S_OK;
  if (b1 != b2)
    return S_OK;
}

If you have reached this point, the current instance of the class and the instance referenced by pOther are equal, so set the return value to true, and exit.

[Visual C++]
*pbEqual = VARIANT_TRUE;
return S_OK;
}

The IsIdentical method can be implemented by a simple pointer comparison.


Back to top