Factor, Don’t Complicate


A reader asks the following question,


 


Sender: bv


 


re: A Question about Copy Constructors in C++/CLI


 


Ok, i have a question.


What happens if: suppose you want to make a shallow copy. Ex:


 


ClassX obj(somePtr);//internally obj.m_ptr = SomePtr;


 


Now


 


ClassX obj2(obj);


 


What problems can occur in such cases?


because now neither obj nor obj2 owns the member ptr. so none should destroy them.


And i want that, only obj should be able to modify the ptr and not obj2. How will i do it?


 


The purpose of a copy constructor (and copy assignment operator) is to allow users to overwrite the default behavior. In the C language, the default behavior of copying one aggregate object with another is bitwise copy. In the original C++ implementation, this was also the behavior of copying one class object with another. However, the greater functionality of a class over that of a C struct required changing the default behavior to that of memberwise copy – that is, to recognize the integrity of member and base class sub-objects.


 


The complexity within C++ of copying one class object with another falls into two general categories – at least into two general categories that I wish to address:


 



  1. When we use primitive members such as pointers that reflect shallow copy and fall outside the constructor as resource acquisition pattern. In effect, we have to provide our own deep copy semantics.

 



  1. When we decide to implement complex behavioral patterns, such as, for example, copy on write, reference counting, and all sorts of neat abstract relationships that fall outside the default copy behavior.

 


The default copy behavior of CLI reference types is shallow copy. That is, a reference type is a duple consisting of a named tracking handle and an object allocated on the CLI heap. The copying of one tracking handle to another results in both handles addressing the same heap object. In a garbage collection environment, the too-early destruction problem of ISO-C++ goes away.


 


 So, with that background, let’s go to the reader’s question.


 


ClassX obj(somePtr);  //internally obj.m_ptr = SomePtr;


ClassX obj2(obj);


 


What problems can occur in such cases?


Well, the first thing is, I will presume that this is an ISO-C++ class, not a C++/CLI class given the emphasis on pointers. So we are talking about a classic solution – that is, with memberwise default behavior. The minimal class we can extrapolate from this small code sample is,


 


class X


{


    T * m_ptr;


public:


 


    // ClassX obj(somePtr); 


    X( T * somePtr ) : m_ptr( somePtr ){}


 


};


 


This is all that is required to support the examples provided by the writer. An initialization of one X object with another, such as


 


X x1( myT );


X x2( x1 );


 


by default results in the bitwise copy of x2 with x1 without the explicit invocation of a synthesized copy constructor, with x2.m_ptr holding the same address as x1.m_ptr.


 


The reader then asks,


 


What problems can occur in such cases?


because now neither obj nor obj2 owns the member ptr. so none should destroy them.


And i want that, only obj should be able to modify the ptr and not obj2. How will i do it?


 


Well, because both objects manipulate the same object through their pointer member, any write may come as a surprise; moreover, if they are on separate threads, there is a need for locking on write operations.


 


As the user notes, it is not a good idea for either x1 or x2 to destroy the object addressed without somehow synchronizing it with the other – again, this is exactly the problem that garbage collection solves, removing the burden on the user.


 


To synchronize the destruction, one has to come up with some form of reference counting mechanism – the first discussion of that in C++ was James Coplien’s Advanced C++. Bjarne provides an example in his C++ Programming Language. I’m not going to go into the actual implementation.


 


The second question is, how should one restrain changes to the object that are pointed to by multiple instances of class X such that there is one `master’ and many … well, readers. The best way, in my opinion, to do this is to factor the design into two classes – and allowing the readers read-only access rather than holding a pointer to it. Otherwise, you have a rather clunky design in which the object has to ask, am I allowed to modify this guy I point to? Does the writer, or master, still exist? [probably can’t answer that] – and there is no notification semantics that one could employ, etc.


 


So, the answer to the second question is, come up with a design in which the characteristics of the one writer or many readers is built into the types; otherwise, the class is non-intuitive and will likely confuse users and be a cause of error and frustration.   


Comments (7)

  1. William says:

    A general question. My shop is and has been hardcore C++. We have waited for .NET to evolve, and it looks like it has to the point where C++ is a real option. I cannot determine however, if C++ will be a supported language by ASP.NET. ASP.NET suports the notion (like ASP) that source code can be deployed uncompiled. At first invocation the compiler run to make CLR byte codes, etc. I assume this is not a deployment option for C++ in the VS 2005 timeframe. yes/no?

  2. stan lippman says:

    have you been a practicing paralegal in your spare time? the yes/no response is yes, no C++/CLI is not going to be a codebehind ASP.NET language in the VS 2005 timeframe. speaking with a few more folks with more knowledge of this issue than i have, one of the primary stumbling blocks is the C++ compilaton model. Unless we define a /clr:safe

    subset and constrain users to that, they will otherwise need header files, libs, .rc files and the whole shootin’ match around at compile time rather than just the assemblies one is building against. we probably don’t want to require the installation of the Platform SDK on a web server just to deply your written in C++ ASP.NET page. I realize this response goes beyond yes/no, but hopefully it adds to rather than complicates the response.

  3. Norman Diamond says:

    > An initialization of one X object with

    > another, such as

    > X x1( myT );

    > X x2( x1 );

    > by default results in the bitwise copy of x2

    > with x1 without the explicit invocation of a

    > synthesized copy constructor,

    I don’t see that in either Stroustrop’s 3rd edition or in the ISO standard. Where was the reversion to bitwise copying defined?

  4. Norman Diamond says:

    I also don’t see it in the draft of the C++/CLI standard.

    There’s a chance I might see it in your book — since I have the first edition of your book and am not sure if it dates from when copy constructors used to be bitwise. But sorry I didn’t check because even if it does say that then surely it’s obsolete by now.

  5. stan lippman says:

    the primary section to read — well, study is required — is section 12.8. 3.1, 3.2, and 12.2 are also to be noted. notice that X must only contain primitive data types for this to hold, making the implicit copy constructor trivial.

    my primary activity well i wrote my books was implementing C++ compilers … the books are a side-effect, not my primary activity.

    standards are difficult to parse — they require what is typically called a `language lawyer’ mind — Andy Koenig or Josee Lajoie, for example, are my icons of the mental clarity necessary for both writing and explicating what is in a language standard.

    my writing and speech is not nearly precise enough, and i understand you find fault with its many simplifications … but i consider that a service.

    if you are going to go to the well, you have to expend a bit more work than a regular expression search.

    in any case, you need not be sorry. if you are still using the first edition, amazon has quite a number of the 3rd edition really cheap!

  6. Dating says:

    A reader asks the following question, Sender: bv re: A Question about Copy Constructors in C++/CLI Ok, i have a question. What happens if: suppose you want to make a shallow copy. Ex: ClassX obj(somePtr);//internally obj.m_ptr = SomePtr; Now ClassX obj2(obj)